From 6e76c1196ce46ef4e1fba7dfecc7e9731adfb376 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 03:00:43 +0300 Subject: [PATCH 001/409] Added my screen recorder config --- modules/utilities/cards/Record.qml | 228 ++++++++++++++++++++++---- services/Recorder.qml | 253 +++++++++++++++++++++++++---- 2 files changed, 417 insertions(+), 64 deletions(-) diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 273c64002..c4de7f1b7 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -19,6 +19,11 @@ StyledRect { radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer + property bool actuallyRecording: Recorder.running + property string lastError: "" + property string currentVideoMode: "fullscreen" + property string currentAudioMode: "none" + ColumnLayout { id: layout @@ -38,7 +43,7 @@ StyledRect { } radius: Appearance.rounding.full - color: Recorder.running ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + color: root.actuallyRecording ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { id: icon @@ -47,7 +52,7 @@ StyledRect { anchors.horizontalCenterOffset: -0.5 anchors.verticalCenterOffset: 1.5 text: "screen_record" - color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + color: root.actuallyRecording ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } @@ -65,51 +70,117 @@ StyledRect { StyledText { Layout.fillWidth: true - text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off") - color: Colours.palette.m3onSurfaceVariant + text: { + if (root.lastError !== "") return qsTr("Error: %1").arg(root.lastError); + if (Recorder.paused) return qsTr("Recording paused"); + if (root.actuallyRecording) { + const videoText = root.currentVideoMode; + const audioText = root.currentAudioMode === "none" ? "no audio" : root.currentAudioMode; + return qsTr("Recording %1 - %2").arg(videoText).arg(audioText); + } + return qsTr("Recording off"); + } + color: root.lastError !== "" ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small elide: Text.ElideRight } } SplitButton { - disabled: Recorder.running - active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0] - menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text + disabled: root.actuallyRecording + active: menuItems.find(m => root.props.recordingMode === m.mode) ?? menuItems[0] + menu.onItemSelected: item => { + root.props.recordingMode = item.mode; + root.currentVideoMode = item.videoMode; + root.currentAudioMode = item.audioMode; + } menuItems: [ MenuItem { + property string mode: "fullscreen" + property string videoMode: "fullscreen" + property string audioMode: "none" icon: "fullscreen" text: qsTr("Record fullscreen") activeText: qsTr("Fullscreen") - onClicked: Recorder.start() + onClicked: startRecording() }, MenuItem { + property string mode: "region" + property string videoMode: "region" + property string audioMode: "none" icon: "screenshot_region" text: qsTr("Record region") activeText: qsTr("Region") - onClicked: Recorder.start(["-r"]) + onClicked: startRecording() }, MenuItem { - icon: "select_to_speak" - text: qsTr("Record fullscreen with sound") - activeText: qsTr("Fullscreen") - onClicked: Recorder.start(["-s"]) + property string mode: "window" + property string videoMode: "window" + property string audioMode: "none" + icon: "web_asset" + text: qsTr("Record window") + activeText: qsTr("Window") + onClicked: startRecording() + }, + MenuItem { + property string mode: "mic" + property string videoMode: "fullscreen" + property string audioMode: "mic" + icon: "mic" + text: qsTr("Record fullscreen with mic") + activeText: qsTr("Mic") + onClicked: startRecording() }, MenuItem { + property string mode: "system" + property string videoMode: "fullscreen" + property string audioMode: "system" icon: "volume_up" - text: qsTr("Record region with sound") - activeText: qsTr("Region") - onClicked: Recorder.start(["-sr"]) + text: qsTr("Record fullscreen with system audio") + activeText: qsTr("System") + onClicked: startRecording() + }, + MenuItem { + property string mode: "combined" + property string videoMode: "fullscreen" + property string audioMode: "combined" + icon: "settings_voice" + text: qsTr("Record fullscreen with mic + system") + activeText: qsTr("Combined") + onClicked: startRecording() } ] } } + StyledRect { + id: errorBanner + Layout.fillWidth: true + visible: root.lastError !== "" + implicitHeight: visible ? errorText.implicitHeight + Appearance.padding.normal * 2 : 0 + radius: Appearance.rounding.small + color: Colours.palette.m3errorContainer + + StyledText { + id: errorText + anchors.fill: parent + anchors.margins: Appearance.padding.normal + text: root.lastError + color: Colours.palette.m3onErrorContainer + wrapMode: Text.Wrap + font.pointSize: Appearance.font.size.small + } + + Behavior on implicitHeight { + Anim { duration: Appearance.anim.durations.small } + } + } + Loader { id: listOrControls - property bool running: Recorder.running + property bool running: root.actuallyRecording Layout.fillWidth: true Layout.preferredHeight: implicitHeight @@ -117,9 +188,7 @@ StyledRect { Behavior on Layout.preferredHeight { id: locHeightAnim - enabled: false - Anim {} } @@ -175,7 +244,6 @@ StyledRect { Component { id: recordingList - RecordingList { props: root.props visibilities: root.visibilities @@ -184,7 +252,6 @@ StyledRect { Component { id: recordingControls - RowLayout { spacing: Appearance.spacing.normal @@ -197,7 +264,6 @@ StyledRect { StyledText { id: recText - anchors.centerIn: parent animate: true text: Recorder.paused ? "PAUSED" : "REC" @@ -210,10 +276,9 @@ StyledRect { } SequentialAnimation on opacity { - running: !Recorder.paused + running: !Recorder.paused && root.actuallyRecording alwaysRunToEnd: true loops: Animation.Infinite - Anim { from: 1 to: 0 @@ -232,17 +297,14 @@ StyledRect { StyledText { text: { const elapsed = Recorder.elapsed; - const hours = Math.floor(elapsed / 3600); const mins = Math.floor((elapsed % 3600) / 60); const secs = Math.floor(elapsed % 60).toString().padStart(2, "0"); - let time; if (hours > 0) time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; else time = `${mins}:${secs}`; - return qsTr("Recording for %1").arg(time); } font.pointSize: Appearance.font.size.normal @@ -261,7 +323,6 @@ StyledRect { font.pointSize: Appearance.font.size.large onClicked: { Recorder.togglePause(); - internalChecked = Recorder.paused; } } @@ -270,7 +331,116 @@ StyledRect { inactiveColour: Colours.palette.m3error inactiveOnColour: Colours.palette.m3onError font.pointSize: Appearance.font.size.large - onClicked: Recorder.stop() + onClicked: stopRecording() + } + } + } + + function startRecording() { + // Clear any previous errors + root.lastError = ""; + + const mode = root.props.recordingMode || "fullscreen"; + + // Map the combined mode to separate video and audio modes + let videoMode = "fullscreen"; + let audioMode = "none"; + + switch(mode) { + case "fullscreen": + videoMode = "fullscreen"; + audioMode = "none"; + break; + case "region": + videoMode = "region"; + audioMode = "none"; + break; + case "window": + videoMode = "window"; + audioMode = "none"; + break; + case "mic": + videoMode = "fullscreen"; + audioMode = "mic"; + break; + case "system": + videoMode = "fullscreen"; + audioMode = "system"; + break; + case "combined": + videoMode = "fullscreen"; + audioMode = "combined"; + break; + } + + root.currentVideoMode = videoMode; + root.currentAudioMode = audioMode; + + console.log("Starting recording - Video:", videoMode, "Audio:", audioMode); + + // Call Recorder service + const success = Recorder.start(videoMode, audioMode); + + if (!success) { + root.lastError = "Failed to start recording"; + } + } + + function stopRecording() { + root.lastError = ""; + Recorder.stop(); + } + + // Clear error after timeout + Timer { + id: errorTimeout + interval: 10000 + repeat: false + running: root.lastError !== "" + onTriggered: { + root.lastError = ""; + } + } + + Connections { + target: Recorder + + function onRunningChanged() { + // Sync actuallyRecording with Recorder.running + root.actuallyRecording = Recorder.running; + + if (!Recorder.running) { + console.log("Recording stopped"); + } + } + + function onErrorOccurred(errorMsg) { + console.error("Recorder error:", errorMsg); + root.lastError = errorMsg; + errorTimeout.restart(); + } + + function onRecordingStarted() { + console.log("Recording started successfully"); + root.lastError = ""; + } + + function onRecordingStopped() { + console.log("Recording stopped successfully"); + } + } + + Component.onCompleted: { + // Sync initial state + root.actuallyRecording = Recorder.running; + + // Set initial mode if saved + if (root.props.recordingMode) { + const mode = root.props.recordingMode; + const item = menuItems.find(m => m.mode === mode); + if (item) { + root.currentVideoMode = item.videoMode; + root.currentAudioMode = item.audioMode; } } } diff --git a/services/Recorder.qml b/services/Recorder.qml index e4ce6a8bd..7abbbc8ae 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -10,25 +10,86 @@ Singleton { readonly property alias running: props.running readonly property alias paused: props.paused readonly property alias elapsed: props.elapsed - property bool needsStart - property list startArgs - property bool needsStop - property bool needsPause - - function start(extraArgs: list): void { - needsStart = true; - startArgs = extraArgs; - checkProc.running = true; + + signal errorOccurred(string errorMsg) + signal recordingStarted() + signal recordingStopped() + + function start(videoMode: string, audioMode: string): bool { + if (props.running) { + console.warn("Recording already running"); + errorOccurred("Recording already in progress"); + return false; + } + + // Build command array + const args = ["caelestia", "record", "--mode", videoMode]; + + if (audioMode && audioMode !== "none") { + args.push("--audio", audioMode); + } + + console.log("Executing:", args.join(" ")); + + try { + Quickshell.execDetached(args); + props.running = true; + props.paused = false; + props.elapsed = 0; + verifyTimer.restart(); + recordingStarted(); + return true; + } catch (error) { + console.error("Failed to start recording:", error); + errorOccurred("Failed to execute recording command: " + error); + props.running = false; + return false; + } } function stop(): void { - needsStop = true; - checkProc.running = true; + if (!props.running) { + console.warn("No recording to stop"); + return; + } + + console.log("Stopping recording"); + + try { + Quickshell.execDetached(["caelestia", "record", "--stop"]); + // Don't immediately set running to false - wait for process to confirm + stopVerifyTimer.restart(); + } catch (error) { + console.error("Failed to stop recording:", error); + errorOccurred("Failed to stop recording: " + error); + // Force state reset on error + props.running = false; + props.paused = false; + props.elapsed = 0; + recordingStopped(); + } } function togglePause(): void { - needsPause = true; - checkProc.running = true; + if (!props.running) { + console.warn("No recording to pause"); + return; + } + + console.log("Toggling pause"); + + try { + Quickshell.execDetached(["caelestia", "record", "--pause"]); + props.paused = !props.paused; + } catch (error) { + console.error("Failed to toggle pause:", error); + errorOccurred("Failed to pause/resume recording: " + error); + } + } + + function verifyRunning(): bool { + statusProc.running = true; + return props.running; } PersistentProperties { @@ -36,47 +97,169 @@ Singleton { property bool running: false property bool paused: false - property real elapsed: 0 // Might get too large for int + property real elapsed: 0 reloadableId: "recorder" } + // Main process checker - runs periodically when recording Process { id: checkProc - running: true + running: false command: ["pidof", "gpu-screen-recorder"] + onExited: code => { - props.running = code === 0; + const wasRunning = props.running; + const isRunning = code === 0; - if (code === 0) { - if (root.needsStop) { - Quickshell.execDetached(["caelestia", "record"]); - props.running = false; - props.paused = false; - } else if (root.needsPause) { - Quickshell.execDetached(["caelestia", "record", "-p"]); - props.paused = !props.paused; - } - } else if (root.needsStart) { - Quickshell.execDetached(["caelestia", "record", ...root.startArgs]); - props.running = true; + // Detect unexpected stop + if (wasRunning && !isRunning) { + console.warn("Recording process stopped unexpectedly"); + props.running = false; props.paused = false; props.elapsed = 0; + recordingStopped(); } - root.needsStart = false; - root.needsStop = false; - root.needsPause = false; + // Schedule next check if still recording + if (props.running) { + statusCheckTimer.restart(); + } + } + } + + // Verification timer after start + Timer { + id: verifyTimer + interval: 1500 + repeat: false + onTriggered: { + console.log("Verifying recording started"); + statusProc.running = true; } } - Connections { - target: Time - // enabled: props.running && !props.paused + // Verification timer after stop + Timer { + id: stopVerifyTimer + interval: 500 + repeat: false + onTriggered: { + console.log("Verifying recording stopped"); + stopStatusProc.running = true; + } + } + + // Status check process for start verification + Process { + id: statusProc - function onSecondsChanged(): void { + running: false + command: ["pidof", "gpu-screen-recorder"] + + onExited: code => { + const isRunning = code === 0; + + if (!isRunning && props.running) { + console.error("Recording process failed to start"); + errorOccurred("Recording process failed to start"); + props.running = false; + props.paused = false; + props.elapsed = 0; + } else if (isRunning && props.running) { + console.log("Recording verified running"); + statusCheckTimer.restart(); + } + } + } + + // Status check process for stop verification + Process { + id: stopStatusProc + + running: false + command: ["pidof", "gpu-screen-recorder"] + + onExited: code => { + const isRunning = code === 0; + + if (!isRunning) { + console.log("Recording stopped successfully"); + props.running = false; + props.paused = false; + props.elapsed = 0; + recordingStopped(); + } else { + // Process still running, try again + console.warn("Process still running, checking again"); + stopVerifyTimer.restart(); + } + } + } + + // Elapsed time tracker + Timer { + id: elapsedTimer + interval: 1000 + repeat: true + running: props.running && !props.paused + + onTriggered: { props.elapsed++; } } + + // Periodic status check while recording + Timer { + id: statusCheckTimer + interval: 3000 + repeat: false + + onTriggered: { + if (props.running) { + checkProc.running = true; + } + } + } + + // Initialize on component completion + Component.onCompleted: { + console.log("Recorder service initialized"); + // Check initial state + initialStatusProc.running = true; + } + + // Initial status check + Process { + id: initialStatusProc + + running: false + command: ["pidof", "gpu-screen-recorder"] + + onExited: code => { + if (code === 0) { + console.log("Found existing recording process"); + props.running = true; + statusCheckTimer.restart(); + } else { + console.log("No existing recording process"); + props.running = false; + props.paused = false; + props.elapsed = 0; + } + } + } + + // Cleanup on destruction + Component.onDestruction: { + if (props.running) { + console.log("Service destroyed while recording - stopping recording"); + try { + Quickshell.execDetached(["caelestia", "record", "--stop"]); + } catch (error) { + console.error("Failed to stop recording on cleanup:", error); + } + } + } } From c289eed8ac8ba23f860d9eba6a4ba8e9b118eade Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 03:21:38 +0300 Subject: [PATCH 002/409] Updated audio options --- services/Recorder.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/Recorder.qml b/services/Recorder.qml index 7abbbc8ae..be83629ca 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -25,7 +25,7 @@ Singleton { // Build command array const args = ["caelestia", "record", "--mode", videoMode]; - if (audioMode && audioMode !== "none") { + if (audioMode) { args.push("--audio", audioMode); } From 99713ff1cd9fa17a64807ca2bc8d370113329cc4 Mon Sep 17 00:00:00 2001 From: MateusAquino Date: Tue, 6 Jan 2026 15:17:37 -0300 Subject: [PATCH 003/409] feat: add persisted audio sources dropdown --- config/Config.qml | 5 + config/UtilitiesConfig.qml | 7 + modules/utilities/Wrapper.qml | 1 + modules/utilities/cards/Record.qml | 238 +++++++++++++++++++---------- 4 files changed, 169 insertions(+), 82 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index b875eef69..484748a39 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -379,6 +379,11 @@ Singleton { vpn: { enabled: utilities.vpn.enabled, provider: utilities.vpn.provider + }, + recording: { + videoMode: utilities.recording.videoMode, + recordSystem: utilities.recording.recordSystem, + recordMicrophone: utilities.recording.recordMicrophone } }; } diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index 5779d8826..a0f287086 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -7,6 +7,13 @@ JsonObject { property Sizes sizes: Sizes {} property Toasts toasts: Toasts {} property Vpn vpn: Vpn {} + property Recording recording: Recording {} + + component Recording: JsonObject { + property string videoMode: "fullscreen" + property bool recordSystem: false + property bool recordMicrophone: false + } component Sizes: JsonObject { property int width: 430 diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 77178e36e..842a550f9 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -14,6 +14,7 @@ Item { readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false + property bool recordingAudioExpanded: false property string recordingConfirmDelete property string recordingMode diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index c4de7f1b7..7caccc83f 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -21,8 +21,17 @@ StyledRect { property bool actuallyRecording: Recorder.running property string lastError: "" - property string currentVideoMode: "fullscreen" - property string currentAudioMode: "none" + property string currentVideoMode: Config.utilities.recording.videoMode + + // Computed audio mode based on settings + readonly property string currentAudioMode: { + const recordSystem = Config.utilities.recording.recordSystem; + const recordMic = Config.utilities.recording.recordMicrophone; + if (recordSystem && recordMic) return "combined"; + if (recordSystem) return "system"; + if (recordMic) return "mic"; + return "none"; + } ColumnLayout { id: layout @@ -88,18 +97,16 @@ StyledRect { SplitButton { disabled: root.actuallyRecording - active: menuItems.find(m => root.props.recordingMode === m.mode) ?? menuItems[0] + active: menuItems.find(m => m.mode === Config.utilities.recording.videoMode) ?? menuItems[0] menu.onItemSelected: item => { - root.props.recordingMode = item.mode; - root.currentVideoMode = item.videoMode; - root.currentAudioMode = item.audioMode; + Config.utilities.recording.videoMode = item.mode; + root.currentVideoMode = item.mode; + Config.save(); } menuItems: [ MenuItem { property string mode: "fullscreen" - property string videoMode: "fullscreen" - property string audioMode: "none" icon: "fullscreen" text: qsTr("Record fullscreen") activeText: qsTr("Fullscreen") @@ -107,8 +114,6 @@ StyledRect { }, MenuItem { property string mode: "region" - property string videoMode: "region" - property string audioMode: "none" icon: "screenshot_region" text: qsTr("Record region") activeText: qsTr("Region") @@ -116,39 +121,10 @@ StyledRect { }, MenuItem { property string mode: "window" - property string videoMode: "window" - property string audioMode: "none" icon: "web_asset" text: qsTr("Record window") activeText: qsTr("Window") onClicked: startRecording() - }, - MenuItem { - property string mode: "mic" - property string videoMode: "fullscreen" - property string audioMode: "mic" - icon: "mic" - text: qsTr("Record fullscreen with mic") - activeText: qsTr("Mic") - onClicked: startRecording() - }, - MenuItem { - property string mode: "system" - property string videoMode: "fullscreen" - property string audioMode: "system" - icon: "volume_up" - text: qsTr("Record fullscreen with system audio") - activeText: qsTr("System") - onClicked: startRecording() - }, - MenuItem { - property string mode: "combined" - property string videoMode: "fullscreen" - property string audioMode: "combined" - icon: "settings_voice" - text: qsTr("Record fullscreen with mic + system") - activeText: qsTr("Combined") - onClicked: startRecording() } ] } @@ -177,6 +153,144 @@ StyledRect { } } + // Audio Sources Section + ColumnLayout { + Layout.fillWidth: true + visible: !root.actuallyRecording + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Audio Sources") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { Layout.fillWidth: true } + + IconButton { + icon: root.props.recordingAudioExpanded ? "expand_less" : "expand_more" + type: IconButton.Tonal + font.pointSize: Appearance.font.size.small + onClicked: { + root.props.recordingAudioExpanded = !root.props.recordingAudioExpanded; + } + } + } + + ColumnLayout { + Layout.fillWidth: true + visible: root.props.recordingAudioExpanded + spacing: Appearance.spacing.smaller + + // System Audio (Default Sink) + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledSwitch { + checked: Config.utilities.recording.recordSystem + onToggled: { + Config.utilities.recording.recordSystem = checked; + Config.save(); + } + } + + StyledText { + Layout.preferredWidth: 85 + text: qsTr("System") + font.pointSize: Appearance.font.size.small + elide: Text.ElideRight + } + + StyledSlider { + id: systemVolumeSlider + Layout.fillWidth: true + implicitHeight: 24 + opacity: Config.utilities.recording.recordSystem ? 1.0 : 0.5 + from: 0 + to: 1 + value: Audio.volume + onMoved: { + Audio.setVolume(value); + } + } + + StyledText { + text: Math.round(Audio.volume * 100) + "%" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + Layout.preferredWidth: 40 + } + + IconButton { + icon: Audio.muted ? "volume_off" : "volume_up" + type: Audio.muted ? IconButton.Filled : IconButton.Tonal + font.pointSize: Appearance.font.size.small + onClicked: { + if (Audio.sink?.audio) { + Audio.sink.audio.muted = !Audio.sink.audio.muted; + } + } + } + } + + // Microphone (Default Source) + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledSwitch { + checked: Config.utilities.recording.recordMicrophone + onToggled: { + Config.utilities.recording.recordMicrophone = checked; + Config.save(); + } + } + + StyledText { + Layout.preferredWidth: 85 + text: qsTr("Microphone") + font.pointSize: Appearance.font.size.small + elide: Text.ElideRight + } + + StyledSlider { + id: micVolumeSlider + Layout.fillWidth: true + implicitHeight: 24 + opacity: Config.utilities.recording.recordMicrophone ? 1.0 : 0.5 + from: 0 + to: 1 + value: Audio.sourceVolume + onMoved: { + Audio.setSourceVolume(value); + } + } + + StyledText { + text: Math.round(Audio.sourceVolume * 100) + "%" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + Layout.preferredWidth: 40 + } + + IconButton { + icon: Audio.sourceMuted ? "mic_off" : "mic" + type: Audio.sourceMuted ? IconButton.Filled : IconButton.Tonal + font.pointSize: Appearance.font.size.small + onClicked: { + if (Audio.source?.audio) { + Audio.source.audio.muted = !Audio.source.audio.muted; + } + } + } + } + } + } + Loader { id: listOrControls @@ -340,41 +454,10 @@ StyledRect { // Clear any previous errors root.lastError = ""; - const mode = root.props.recordingMode || "fullscreen"; - - // Map the combined mode to separate video and audio modes - let videoMode = "fullscreen"; - let audioMode = "none"; - - switch(mode) { - case "fullscreen": - videoMode = "fullscreen"; - audioMode = "none"; - break; - case "region": - videoMode = "region"; - audioMode = "none"; - break; - case "window": - videoMode = "window"; - audioMode = "none"; - break; - case "mic": - videoMode = "fullscreen"; - audioMode = "mic"; - break; - case "system": - videoMode = "fullscreen"; - audioMode = "system"; - break; - case "combined": - videoMode = "fullscreen"; - audioMode = "combined"; - break; - } + const videoMode = Config.utilities.recording.videoMode || "fullscreen"; + const audioMode = root.currentAudioMode; root.currentVideoMode = videoMode; - root.currentAudioMode = audioMode; console.log("Starting recording - Video:", videoMode, "Audio:", audioMode); @@ -433,15 +516,6 @@ StyledRect { Component.onCompleted: { // Sync initial state root.actuallyRecording = Recorder.running; - - // Set initial mode if saved - if (root.props.recordingMode) { - const mode = root.props.recordingMode; - const item = menuItems.find(m => m.mode === mode); - if (item) { - root.currentVideoMode = item.videoMode; - root.currentAudioMode = item.audioMode; - } - } + root.currentVideoMode = Config.utilities.recording.videoMode || "fullscreen"; } } From 39e38e8e76bd1f60970775d112f77dfc9781f82d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Pra=C5=BE=C3=A1k?= Date: Fri, 30 Jan 2026 01:31:18 +0100 Subject: [PATCH 004/409] plugin: fix build on NixOS (#1128) * plugin fix libcava for NixOS * plugin: fix cava by try found --- plugin/src/Caelestia/CMakeLists.txt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index e4a020124..1b7d0e493 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -3,7 +3,10 @@ find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) -pkg_check_modules(Cava IMPORTED_TARGET libcava REQUIRED) +pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) +if(NOT Cava_FOUND) + pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) +endif() set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") qt_standard_project_setup(REQUIRES 6.9) From 45b87645e20d9b472d0449415cd9f277dce21364 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Feb 2026 02:06:25 +0000 Subject: [PATCH 005/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 5b7fbd218..8f621b4ae 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1769226332, - "narHash": "sha256-JKD9M2+/J4e6nRtcY2XRfpLlOHaGXT4aUHyIG/20qlw=", + "lastModified": 1769740633, + "narHash": "sha256-W4gMgX8RsDeJioRPQHhUgXD/TxqAQxdZjkhjHRX70Pk=", "owner": "caelestia-dots", "repo": "cli", - "rev": "52a3a3c50ef55e3561057e8a74c85cf16f83039f", + "rev": "90fc2a981e587d38edc5a899011eca7979ecf124", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769018530, - "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", + "lastModified": 1769461804, + "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", + "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1768985439, - "narHash": "sha256-qkU4r+l+UPz4dutMMRZSin64HuVZkEv9iFpu9yMWVY0=", + "lastModified": 1769593411, + "narHash": "sha256-WW00FaBiUmQyxvSbefvgxIjwf/WmRrEGBbwMHvW/7uQ=", "ref": "refs/heads/master", - "rev": "191085a8821b35680bba16ce5411fc9dbe912237", - "revCount": 731, + "rev": "1e4d804e7f3fa7465811030e8da2bf10d544426a", + "revCount": 732, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 4c72e3e06bd58a31e16cc1588d94543069fbd00a Mon Sep 17 00:00:00 2001 From: Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> Date: Tue, 3 Feb 2026 20:40:20 +0800 Subject: [PATCH 006/409] desktopclock: background blur GameMode support (#1145) --- modules/background/DesktopClock.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 77fe447fe..f9a06a2aa 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -16,7 +16,7 @@ Item { property real scale: Config.background.desktopClock.scale readonly property bool bgEnabled: Config.background.desktopClock.background.enabled - readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur + readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled readonly property bool invertColors: Config.background.desktopClock.invertColors readonly property bool useLightSet: Colours.light ? !invertColors : invertColors readonly property color safePrimary: useLightSet ? Colours.palette.m3primaryContainer : Colours.palette.m3primary From 7a41a85954a40366bd25ed4e33d1cd9146507ad4 Mon Sep 17 00:00:00 2001 From: Evertiro Date: Wed, 4 Feb 2026 02:04:10 -0600 Subject: [PATCH 007/409] config: allow adjusting the speed of gifs (#1147) Signed-off-by: Dan Griffiths --- README.md | 2 ++ config/AppearanceConfig.qml | 2 ++ config/Config.qml | 2 ++ modules/dashboard/Media.qml | 2 +- modules/dashboard/dash/Media.qml | 2 +- modules/session/Content.qml | 2 +- 6 files changed, 9 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 6bbc8db9c..25f5a272d 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,8 @@ default, you must create it manually. ```json { "appearance": { + "mediaGifSpeedAdjustment": 300, + "sessionGifSpeed": 0.7, "anim": { "durations": { "scale": 1 diff --git a/config/AppearanceConfig.qml b/config/AppearanceConfig.qml index b25945b15..3d590dca2 100644 --- a/config/AppearanceConfig.qml +++ b/config/AppearanceConfig.qml @@ -80,6 +80,8 @@ JsonObject { } component Anim: JsonObject { + property real mediaGifSpeedAdjustment: 300 + property real sessionGifSpeed: 0.7 property AnimCurves curves: AnimCurves {} property AnimDurations durations: AnimDurations {} } diff --git a/config/Config.qml b/config/Config.qml index b32ebda76..3d7cca40c 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -122,6 +122,8 @@ Singleton { } }, anim: { + mediaGifSpeedAdjustment: 300, + sessionGifSpeed: 0.7, durations: { scale: appearance.anim.durations.scale } diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 37d12263a..ce5db35ad 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -380,7 +380,7 @@ Item { height: visualiser.height * 0.75 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / 300 + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 3a2b685ef..ad8733520 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -213,7 +213,7 @@ Item { anchors.margins: Appearance.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / 300 + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 6c56d4429..900683f4b 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -53,7 +53,7 @@ Column { playing: visible asynchronous: true - speed: 0.7 + speed: Appearance.anim.sessionGifSpeed source: Paths.absolutePath(Config.paths.sessionGif) } From 614e66a45e33410402a1e98b3cc850b56a3dc4f3 Mon Sep 17 00:00:00 2001 From: Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> Date: Sat, 7 Feb 2026 21:51:54 +0800 Subject: [PATCH 008/409] fix: serialize excludedScreens config (#1151) --- config/Config.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/Config.qml b/config/Config.qml index 3d7cca40c..74e3f458a 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -241,7 +241,8 @@ Singleton { batteryWidth: bar.sizes.batteryWidth, networkWidth: bar.sizes.networkWidth }, - entries: bar.entries + entries: bar.entries, + excludedScreens: bar.excludedScreens }; } From 6ad2c9bbf9151265d6cbda4c34a2397776a3fd5b Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Feb 2026 02:10:18 +0000 Subject: [PATCH 009/409] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index 8f621b4ae..e479bd249 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1769740633, - "narHash": "sha256-W4gMgX8RsDeJioRPQHhUgXD/TxqAQxdZjkhjHRX70Pk=", + "lastModified": 1770345569, + "narHash": "sha256-aXzEWD44Htg0kHdrT/j2Odxt1EXqdJR9s8fDpEAEZtY=", "owner": "caelestia-dots", "repo": "cli", - "rev": "90fc2a981e587d38edc5a899011eca7979ecf124", + "rev": "2395347d36dc12c4ad7471bcec030d75538c128c", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769461804, - "narHash": "sha256-msG8SU5WsBUfVVa/9RPLaymvi5bI8edTavbIq3vRlhI=", + "lastModified": 1770197578, + "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", "owner": "nixos", "repo": "nixpkgs", - "rev": "bfc1b8a4574108ceef22f02bafcf6611380c100d", + "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", "type": "github" }, "original": { From d5f5985db6079bfe048635506203dca89cc57dba Mon Sep 17 00:00:00 2001 From: Evertiro Date: Sat, 7 Feb 2026 22:57:01 -0600 Subject: [PATCH 010/409] chore: logo update (#1152) * Update logo Signed-off-by: Dan Griffiths * Match old logo colors Signed-off-by: Dan Griffiths --------- Signed-off-by: Dan Griffiths --- assets/logo.svg | 121 ++++++++++++++++++++++++------------------------ 1 file changed, 60 insertions(+), 61 deletions(-) diff --git a/assets/logo.svg b/assets/logo.svg index 712d1e36e..6879c92b9 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,21 +1,19 @@ - - - - + inkscape:zoom="1.28" + inkscape:cx="43.359375" + inkscape:cy="183.98438" + inkscape:window-width="1824" + inkscape:window-height="1034" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="Layer_2" /> + id="defs1"> + + - - - - - - - - - - + id="dark-4" + transform="matrix(0.08939103,0,0,0.08939103,-3.899948e-4,18.839439)"> + + + + + From 5b2e1a6231af24472fb8ff3bfa7183a4f63c6ba7 Mon Sep 17 00:00:00 2001 From: Evertiro Date: Sat, 7 Feb 2026 23:13:51 -0600 Subject: [PATCH 011/409] fix: bluetooth battery bar (#1153) Signed-off-by: Dan Griffiths --- modules/controlcenter/bluetooth/Details.qml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 529904546..bc276e097 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -358,18 +358,12 @@ StyledFlickable { } RowLayout { + id: batteryPercent Layout.topMargin: Appearance.spacing.small / 2 Layout.fillWidth: true Layout.preferredHeight: Appearance.padding.smaller spacing: Appearance.spacing.small / 2 - StyledRect { - Layout.fillHeight: true - implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 - radius: Appearance.rounding.full - color: Colours.palette.m3primary - } - StyledRect { Layout.fillWidth: true Layout.fillHeight: true @@ -377,12 +371,12 @@ StyledFlickable { color: Colours.palette.m3secondaryContainer StyledRect { - anchors.right: parent.right + anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: parent.height * 0.25 - implicitWidth: height + implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 radius: Appearance.rounding.full color: Colours.palette.m3primary } From 2e22a21defc26b7a24fb0a01a0882f8d33e344be Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 11 Feb 2026 01:44:23 +0100 Subject: [PATCH 012/409] shortcuts: add special workspace cycle (#1158) * [CI] chore: update flake * [CI] chore: update flake * [CI] chore: update flake * [CI] chore: update flake * shortcuts: special workspace cycle IPC, reopen last * Moved implementation into Hypr service --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- services/Hypr.qml | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/services/Hypr.qml b/services/Hypr.qml index a654fdd34..a26c24df4 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -34,6 +34,7 @@ Singleton { readonly property alias devices: extras.devices property bool hadKeyboard + property string lastSpecialWorkspace: "" signal configReloaded @@ -41,6 +42,39 @@ Singleton { Hyprland.dispatch(request); } + function cycleSpecialWorkspace(direction: string): void { + const openSpecials = workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0); + + if (openSpecials.length === 0) + return; + + const activeSpecial = focusedMonitor.lastIpcObject.specialWorkspace.name ?? ""; + + if (!activeSpecial) { + if (lastSpecialWorkspace) { + const workspace = workspaces.values.find(w => w.name === lastSpecialWorkspace); + if (workspace && workspace.lastIpcObject.windows > 0) { + dispatch(`workspace ${lastSpecialWorkspace}`); + return; + } + } + dispatch(`workspace ${openSpecials[0].name}`); + return; + } + + const currentIndex = openSpecials.findIndex(w => w.name === activeSpecial); + let nextIndex = 0; + + if (currentIndex !== -1) { + if (direction === "next") + nextIndex = (currentIndex + 1) % openSpecials.length; + else + nextIndex = (currentIndex - 1 + openSpecials.length) % openSpecials.length; + } + + dispatch(`workspace ${openSpecials[nextIndex].name}`); + } + function monitorFor(screen: ShellScreen): HyprlandMonitor { return Hyprland.monitorFor(screen); } @@ -105,6 +139,18 @@ Singleton { } } + Connections { + target: root.focusedMonitor + + function onLastIpcObjectChanged(): void { + const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; + + if (specialName && specialName.startsWith("special:")) { + root.lastSpecialWorkspace = specialName; + } + } + } + FileView { id: kbLayoutFile @@ -144,6 +190,14 @@ Singleton { function refreshDevices(): void { extras.refreshDevices(); } + + function cycleSpecialWorkspace(direction: string): void { + root.cycleSpecialWorkspace(direction); + } + + function listSpecialWorkspaces(): string { + return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); + } } CustomShortcut { From 93e8880842b03e251bf59d1ba316f2393c68574f Mon Sep 17 00:00:00 2001 From: Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> Date: Fri, 13 Feb 2026 10:20:35 +0800 Subject: [PATCH 013/409] shortcuts: Sidebar and Utilities shortcuts (#1160) * shortcuts: Added shortcut to toggle the sidebar * shortcuts: added utilities shortcut to toggle utilities --- modules/Shortcuts.qml | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index a62b827eb..3bf20a4f6 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -69,6 +69,29 @@ Scope { onPressed: root.launcherInterrupted = true } + + CustomShortcut { + name: "sidebar" + description: "Toggle sidebar" + onPressed: { + if (root.hasFullscreen) + return; + const visibilities = Visibilities.getForActive(); + visibilities.sidebar = !visibilities.sidebar; + } + } + + CustomShortcut { + name: "utilities" + description: "Toggle utilities" + onPressed: { + if (root.hasFullscreen) + return; + const visibilities = Visibilities.getForActive(); + visibilities.utilities = !visibilities.utilities; + } + } + IpcHandler { target: "drawers" From 3a7309294cd4575e60aeb5e153d346313b16f7d9 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 15 Feb 2026 02:02:14 +0000 Subject: [PATCH 014/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index e479bd249..a8b0053ac 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1770345569, - "narHash": "sha256-aXzEWD44Htg0kHdrT/j2Odxt1EXqdJR9s8fDpEAEZtY=", + "lastModified": 1771075454, + "narHash": "sha256-5GlUpibCTqcXq/kCwkLHQGfjuBk2r+ZlWY0MjZo0xtE=", "owner": "caelestia-dots", "repo": "cli", - "rev": "2395347d36dc12c4ad7471bcec030d75538c128c", + "rev": "d890f7c3af4e7a900338bdf6400c2cf76de89a19", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770197578, - "narHash": "sha256-AYqlWrX09+HvGs8zM6ebZ1pwUqjkfpnv8mewYwAo+iM=", + "lastModified": 1771008912, + "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", "owner": "nixos", "repo": "nixpkgs", - "rev": "00c21e4c93d963c50d4c0c89bfa84ed6e0694df2", + "rev": "a82ccc39b39b621151d6732718e3e250109076fa", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1769593411, - "narHash": "sha256-WW00FaBiUmQyxvSbefvgxIjwf/WmRrEGBbwMHvW/7uQ=", + "lastModified": 1770693276, + "narHash": "sha256-ngXnN5YXu+f45+QGYNN/VEBMQmcBCYGRCqwaK8cxY1s=", "ref": "refs/heads/master", - "rev": "1e4d804e7f3fa7465811030e8da2bf10d544426a", - "revCount": 732, + "rev": "dacfa9de829ac7cb173825f593236bf2c21f637e", + "revCount": 735, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 40a255283083301b9503e1cbb9f0ea7db83e069a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bora=20G=C3=BClerman?= <49169566+eratoriele@users.noreply.github.com> Date: Thu, 19 Feb 2026 13:26:10 +0300 Subject: [PATCH 015/409] launcher: add favorite apps (#946) * launcher: add favorite apps Favorite apps always appear above non-favorite apps Accepts regex, same logic as #920 Added the same regex logic to hidden apps Added util file may need to be relocated * addressed requested changes * fix: Renamed newly added util singleton Also added a null check to favorite icon loader in AppItem.qml * controlCenter/launcherPane: added favorite apps added icons to the app list to indicate if they are favorited/hidden marking as favorite/hidden is desabled if the other is selected * favouriteApps: renamed from favorite to favourite Also disabled favorite/hidden switch for entries added as regex * appDb: added notify and emit to favoriteApps * controlCentre/Launcher: Fixed bug with favourite switch not enabling itself when no hiddenApps exist Added a comment to explain the enabled state of the switches icon loader is now a single loader rather than two, hidden icon has priority * spelling mistakes * fixed warning * formatting fixes --- README.md | 1 + config/Config.qml | 1 + config/LauncherConfig.qml | 1 + .../controlcenter/launcher/LauncherPane.qml | 92 ++++++++++++++++--- modules/drawers/Drawers.qml | 16 +--- modules/launcher/items/AppItem.qml | 19 +++- modules/launcher/services/Apps.qml | 3 +- plugin/src/Caelestia/appdb.cpp | 49 +++++++++- plugin/src/Caelestia/appdb.hpp | 10 ++ utils/Strings.qml | 20 ++++ 10 files changed, 180 insertions(+), 32 deletions(-) create mode 100644 utils/Strings.qml diff --git a/README.md b/README.md index 25f5a272d..0c3ca82da 100644 --- a/README.md +++ b/README.md @@ -562,6 +562,7 @@ default, you must create it manually. "wallpapers": false }, "showOnHover": false, + "favouriteApps": [], "hiddenApps": [] }, "lock": { diff --git a/config/Config.qml b/config/Config.qml index 74e3f458a..7851c3bd6 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -297,6 +297,7 @@ Singleton { enableDangerousActions: launcher.enableDangerousActions, dragThreshold: launcher.dragThreshold, vimKeybinds: launcher.vimKeybinds, + favouriteApps: launcher.favouriteApps, hiddenApps: launcher.hiddenApps, useFuzzy: { apps: launcher.useFuzzy.apps, diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml index 7f9c78812..d9e3a7384 100644 --- a/config/LauncherConfig.qml +++ b/config/LauncherConfig.qml @@ -10,6 +10,7 @@ JsonObject { property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout property int dragThreshold: 50 property bool vimKeybinds: false + property list favouriteApps: [] property list hiddenApps: [] property UseFuzzy useFuzzy: UseFuzzy {} property Sizes sizes: Sizes {} diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 0dd464f16..b236cf9e3 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -24,6 +24,7 @@ Item { property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false + property bool favouriteChecked: false anchors.fill: parent @@ -43,16 +44,14 @@ Item { function updateToggleState() { if (!root.selectedApp) { root.hideFromLauncherChecked = false; + root.favouriteChecked = false; return; } const appId = root.selectedApp.id || root.selectedApp.entry?.id; - if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { - root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); - } else { - root.hideFromLauncherChecked = false; - } + root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); + root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); } function saveHiddenApps(isHidden) { @@ -83,6 +82,7 @@ Item { id: allAppsDb path: `${Paths.state}/apps.sqlite` + favouriteApps: Config.launcher.favouriteApps entries: DesktopEntries.applications.values } @@ -286,6 +286,7 @@ Item { id: appsListLoader Layout.fillWidth: true Layout.fillHeight: true + asynchronous: true active: true sourceComponent: StyledListView { @@ -305,7 +306,8 @@ Item { delegate: StyledRect { required property var modelData - width: parent ? parent.width : 0 + width: parent ? parent.width : 0 + implicitHeight: 40 readonly property bool isSelected: root.selectedApp === modelData @@ -353,9 +355,34 @@ Item { text: modelData.name || modelData.entry?.name || qsTr("Unknown") font.pointSize: Appearance.font.size.normal } - } - implicitHeight: 40 + Loader { + Layout.alignment: Qt.AlignVCenter + readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false + readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false + active: isHidden || isFav + + sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) + } + + Component { + id: hiddenIcon + MaterialIcon { + text: "visibility_off" + fill: 1 + color: Colours.palette.m3primary + } + } + + Component { + id: favouriteIcon + MaterialIcon { + text: "favorite" + fill: 1 + color: Colours.palette.m3primary + } + } + } } } } @@ -440,13 +467,11 @@ Item { onDisplayedAppChanged: { if (displayedApp) { const appId = displayedApp.id || displayedApp.entry?.id; - if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { - root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); - } else { - root.hideFromLauncherChecked = false; - } + root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); + root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); } else { root.hideFromLauncherChecked = false; + root.favouriteChecked = false; } } } @@ -559,12 +584,51 @@ Item { anchors.top: parent.top spacing: Appearance.spacing.normal + SwitchRow { + Layout.topMargin: Appearance.spacing.normal + visible: appDetailsLayout.displayedApp !== null + label: qsTr("Mark as favourite") + checked: root.favouriteChecked + // disabled if: + // * app is hidden + // * app isn't in favouriteApps array but marked as favourite anyway + // ^^^ This means that this app is favourited because of a regex check + // this button can not toggle regexed apps + enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) + opacity: enabled ? 1 : 0.6 + onToggled: checked => { + root.favouriteChecked = checked; + const app = appDetailsLayout.displayedApp; + if (app) { + const appId = app.id || app.entry?.id; + const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : []; + if (checked) { + if (!favouriteApps.includes(appId)) { + favouriteApps.push(appId); + } + } else { + const index = favouriteApps.indexOf(appId); + if (index !== -1) { + favouriteApps.splice(index, 1); + } + } + Config.launcher.favouriteApps = favouriteApps; + Config.save(); + } + } + } SwitchRow { Layout.topMargin: Appearance.spacing.normal visible: appDetailsLayout.displayedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked - enabled: appDetailsLayout.displayedApp !== null + // disabled if: + // * app is favourited + // * app isn't in hiddenApps array but marked as hidden anyway + // ^^^ This means that this app is hidden because of a regex check + // this button can not toggle regexed apps + enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) + opacity: enabled ? 1 : 0.6 onToggled: checked => { root.hideFromLauncherChecked = checked; const app = appDetailsLayout.displayedApp; diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 00f9596a4..93534ec21 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -4,6 +4,7 @@ import qs.components import qs.components.containers import qs.services import qs.config +import qs.utils import qs.modules.bar import Quickshell import Quickshell.Wayland @@ -18,20 +19,7 @@ Variants { id: scope required property ShellScreen modelData - readonly property bool barDisabled: { - const regexChecker = /^\^.*\$$/; - for (const filter of Config.bar.excludedScreens) { - // If filter is a regex - if (regexChecker.test(filter)) { - if ((new RegExp(filter)).test(modelData.name)) - return true; - } else { - if (filter === modelData.name) - return true; - } - } - return false; - } + readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name) Exclusions { screen: scope.modelData diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 48aace76d..2bd818d60 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -2,6 +2,7 @@ import "../services" import qs.components import qs.services import qs.config +import qs.utils import Quickshell import Quickshell.Widgets import QtQuick @@ -46,7 +47,7 @@ Item { anchors.leftMargin: Appearance.spacing.normal anchors.verticalCenter: icon.verticalCenter - implicitWidth: parent.width - icon.width + implicitWidth: parent.width - icon.width - favouriteIcon.width implicitHeight: name.implicitHeight + comment.implicitHeight StyledText { @@ -64,10 +65,24 @@ Item { color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - Appearance.rounding.normal * 2 + width: root.width - icon.width - favouriteIcon.width - Appearance.rounding.normal * 2 anchors.top: name.bottom } } + + Loader { + id: favouriteIcon + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + active: modelData && Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) + + sourceComponent: MaterialIcon { + text: "favorite" + fill: 1 + color: Colours.palette.m3primary + } + } } } diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index c409a7bb0..7f2d64556 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -72,6 +72,7 @@ Searcher { id: appDb path: `${Paths.state}/apps.sqlite` - entries: DesktopEntries.applications.values.filter(a => !Config.launcher.hiddenApps.includes(a.id)) + favouriteApps: Config.launcher.favouriteApps + entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(Config.launcher.hiddenApps, a.id)) } } diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 6e37e16f3..b074cf481 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -162,6 +162,39 @@ void AppDb::setEntries(const QObjectList& entries) { m_timer->start(); } +QStringList AppDb::favouriteApps() const { + return m_favouriteApps; +} + +void AppDb::setFavouriteApps(const QStringList& favApps) { + if (m_favouriteApps == favApps) { + return; + } + + m_favouriteApps = favApps; + emit favouriteAppsChanged(); + m_favouriteAppsRegex.clear(); + m_favouriteAppsRegex.reserve(m_favouriteApps.size()); + for (const QString& item : std::as_const(m_favouriteApps)) { + const QRegularExpression re(regexifyString(item)); + if (re.isValid()) { + m_favouriteAppsRegex << re; + } else { + qWarning() << "AppDb::setFavouriteApps: Regular expression is not valid: " << re.pattern(); + } + } + + emit appsChanged(); +} + +QString AppDb::regexifyString(const QString& original) const { + if (original.startsWith('^') && original.endsWith('$')) + return original; + + const QString escaped = QRegularExpression::escape(original); + return QStringLiteral("^%1$").arg(escaped); +} + QQmlListProperty AppDb::apps() { return QQmlListProperty(this, &getSortedApps()); } @@ -192,7 +225,12 @@ void AppDb::incrementFrequency(const QString& id) { QList& AppDb::getSortedApps() const { m_sortedApps = m_apps.values(); - std::sort(m_sortedApps.begin(), m_sortedApps.end(), [](AppEntry* a, AppEntry* b) { + std::sort(m_sortedApps.begin(), m_sortedApps.end(), [this](AppEntry* a, AppEntry* b) { + bool aIsFav = isFavourite(a); + bool bIsFav = isFavourite(b); + if (aIsFav != bIsFav) { + return aIsFav; + } if (a->frequency() != b->frequency()) { return a->frequency() > b->frequency(); } @@ -201,6 +239,15 @@ QList& AppDb::getSortedApps() const { return m_sortedApps; } +bool AppDb::isFavourite(const AppEntry* app) const { + for (const QRegularExpression& re : m_favouriteAppsRegex) { + if (re.match(app->id()).hasMatch()) { + return true; + } + } + return false; +} + quint32 AppDb::getFrequency(const QString& id) const { auto db = QSqlDatabase::database(m_uuid); QSqlQuery query(db); diff --git a/plugin/src/Caelestia/appdb.hpp b/plugin/src/Caelestia/appdb.hpp index 5f9b96044..ce5f27062 100644 --- a/plugin/src/Caelestia/appdb.hpp +++ b/plugin/src/Caelestia/appdb.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace caelestia { @@ -66,6 +67,7 @@ class AppDb : public QObject { Q_PROPERTY(QString uuid READ uuid CONSTANT) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED) Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED) + Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED) Q_PROPERTY(QQmlListProperty apps READ apps NOTIFY appsChanged) public: @@ -79,6 +81,9 @@ class AppDb : public QObject { [[nodiscard]] QObjectList entries() const; void setEntries(const QObjectList& entries); + [[nodiscard]] QStringList favouriteApps() const; + void setFavouriteApps(const QStringList& favApps); + [[nodiscard]] QQmlListProperty apps(); Q_INVOKABLE void incrementFrequency(const QString& id); @@ -86,6 +91,7 @@ class AppDb : public QObject { signals: void pathChanged(); void entriesChanged(); + void favouriteAppsChanged(); void appsChanged(); private: @@ -94,10 +100,14 @@ class AppDb : public QObject { const QString m_uuid; QString m_path; QObjectList m_entries; + QStringList m_favouriteApps; // unedited string list from qml + QList m_favouriteAppsRegex; // pre-regexified m_favouriteApps list QHash m_apps; mutable QList m_sortedApps; + QString regexifyString(const QString& original) const; QList& getSortedApps() const; + bool isFavourite(const AppEntry* app) const; quint32 getFrequency(const QString& id) const; void updateAppFrequencies(); void updateApps(); diff --git a/utils/Strings.qml b/utils/Strings.qml new file mode 100644 index 000000000..1d0cc766b --- /dev/null +++ b/utils/Strings.qml @@ -0,0 +1,20 @@ +pragma Singleton + +import Quickshell + +Singleton { + function testRegexList(filterList: list, target: string): bool { + const regexChecker = /^\^.*\$$/; + for (const filter of filterList) { + // If filter is a regex + if (regexChecker.test(filter)) { + if ((new RegExp(filter)).test(target)) + return true; + } else { + if (filter === target) + return true; + } + } + return false; + } +} From 69adb9208723467014cf21f401f2ba1804f38376 Mon Sep 17 00:00:00 2001 From: Unrectified <83581717+Unrectified@users.noreply.github.com> Date: Thu, 19 Feb 2026 12:34:21 +0100 Subject: [PATCH 016/409] feat: add wallpaperEnabled option (#1187) * fix: change background color to none allowing other wallpaper engine and background enabled * feat: add wallpaperEnabled property and toggle in appearance settings * fix background: Make it "black" if wallpaper is enabled, otherwise "transparent" * fix: separate Visualiser from Wallpaper (hope I didn't made more shit buh) * fix: transparency not working & layer position * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- config/BackgroundConfig.qml | 1 + config/Config.qml | 1 + modules/background/Background.qml | 12 +++++++++--- modules/background/Visualiser.qml | 2 +- modules/background/Wallpaper.qml | 2 -- modules/controlcenter/appearance/AppearancePane.qml | 3 +++ .../appearance/sections/BackgroundSection.qml | 9 +++++++++ 7 files changed, 24 insertions(+), 6 deletions(-) diff --git a/config/BackgroundConfig.qml b/config/BackgroundConfig.qml index b8a8ad92e..8383f5248 100644 --- a/config/BackgroundConfig.qml +++ b/config/BackgroundConfig.qml @@ -2,6 +2,7 @@ import Quickshell.Io JsonObject { property bool enabled: true + property bool wallpaperEnabled: true property DesktopClock desktopClock: DesktopClock {} property Visualiser visualiser: Visualiser {} diff --git a/config/Config.qml b/config/Config.qml index 7851c3bd6..1ec47c197 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -160,6 +160,7 @@ Singleton { function serializeBackground(): var { return { enabled: background.enabled, + wallpaperEnabled: background.wallpaperEnabled, desktopClock: { enabled: background.desktopClock.enabled, scale: background.desktopClock.scale, diff --git a/modules/background/Background.qml b/modules/background/Background.qml index f8484e167..682da624c 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -22,8 +22,9 @@ Loader { screen: modelData name: "background" WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Background - color: "black" + WlrLayershell.layer: Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom + color: Config.background.wallpaperEnabled ? "black" : "transparent" + surfaceFormat.opaque: false anchors.top: true anchors.bottom: true @@ -35,8 +36,13 @@ Loader { anchors.fill: parent - Wallpaper { + Loader { id: wallpaper + + anchors.fill: parent + active: Config.background.wallpaperEnabled + + sourceComponent: Wallpaper {} } Visualiser { diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index c9bb9efb0..35a086b92 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -13,7 +13,7 @@ Item { id: root required property ShellScreen screen - required property Wallpaper wallpaper + required property Item wallpaper readonly property bool shouldBeActive: Config.background.visualiser.enabled && (!Config.background.visualiser.autoHide || (Hypr.monitorFor(screen)?.activeWorkspace?.toplevels?.values.every(t => t.lastIpcObject?.floating) ?? true)) property real offset: shouldBeActive ? 0 : screen.height * 0.2 diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index b5d7d4af4..39a48fc8f 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -14,8 +14,6 @@ Item { property string source: Wallpapers.current property Image current: one - anchors.fill: parent - onSourceChanged: { if (!source) current = null; diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 42511677a..f29f7ab3d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -48,6 +48,7 @@ Item { property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false property bool backgroundEnabled: Config.background.enabled ?? true + property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true property bool visualiserEnabled: Config.background.visualiser.enabled ?? false property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true property real visualiserRounding: Config.background.visualiser.rounding ?? 1 @@ -83,6 +84,8 @@ Item { Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; Config.background.desktopClock.invertColors = root.desktopClockInvertColors; + Config.background.wallpaperEnabled = root.wallpaperEnabled; + Config.background.visualiser.enabled = root.visualiserEnabled; Config.background.visualiser.autoHide = root.visualiserAutoHide; Config.background.visualiser.rounding = root.visualiserRounding; diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 2f75c9e0e..9d6bc6ebc 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -27,6 +27,15 @@ CollapsibleSection { } } + SwitchRow { + label: qsTr("Wallpaper enabled") + checked: rootPane.wallpaperEnabled + onToggled: checked => { + rootPane.wallpaperEnabled = checked; + rootPane.saveConfig(); + } + } + StyledText { Layout.topMargin: Appearance.spacing.normal text: qsTr("Desktop Clock") From 46174d1934370b2f4a7da43a3dbc0289c14a5a2d Mon Sep 17 00:00:00 2001 From: Thanh Minh <112760114+tmih06@users.noreply.github.com> Date: Thu, 19 Feb 2026 18:53:22 +0700 Subject: [PATCH 017/409] dashboard/performance: new design, configurable, controlcenter support (#975) * feat(dashboard): add configurable performance resources - Add config options to show/hide Battery, GPU, CPU, Memory, Storage - Make dashboard responsive based on number of visible resources - Scale resource sizes and spacing dynamically for 3, 4, or 5 items - Battery shows charge status and time remaining/to full - Each resource can be individually toggled via config * fix(dashboard): add dynamic right margin for last visible resource Ensures the rightmost resource always has proper margin to prevent content from being cut off at the edge * fix(performance): comment out duplicated value2 properties for memory and storage resources * controlcenter: add settings for dashboard * feat: handle readonly properties and re-usable codes * Feature/performance tab rework (#5) * dashboard/performance: rework tab with card-based grid layout - Replace circular arc meters with card-based grid layout - CPU/GPU cards show hardware name, usage and temperature with horizontal bars - Memory card with 3/4 arc indicator and used/total at bottom - Storage card shows physical disks from lsblk with aggregated partition usage - Add cpuName, gpuName, cpuFreq, cpuMaxFreq, disks properties to SystemUsage - Clean hardware names (remove Intel/AMD/NVIDIA prefixes, TM/R symbols) * dashboard/performance: new hero card design * dashboard/performance: update storage indicators to be reponsive to the physical disks count * dashboard/performance: fix the overlay bounding issue * dashboard/perfromance: refactor code * dashboard/performance: add battery gauge * dashboard/performance: correct battery icon * dashboard/performance: configurable battery * dashboard/performance: update layout * dashboard/performance: move the "Usage" text on top and smaller the font size * dashboard/performance: add a lot of configurations * dashboard/performance: add network metrics * fix: issue with hot reload * chore: update default vaule for mainValueSpacing to 0 * chore: group settings into collapasible sections * chore: making GPU & Battery toggle not showing if not found * chore: fix network widget spacing & text * chore: remove old disk bars configs, add update interval * chore: remove old & unused value, functions * chore: network graph update smoothly when data points change * chore: refactor settings - de-flood settings, most of the font & size setting now follow the global Appearance config - Most of sliders are not needed anymore, only keep the update interval slider - clean up * chore: remove readonly properties from the controlcenter/dashboard. * chore: minor fix * fix: fix warning about onPercChange() * fix: network metrics negative number * fix: add minimal height & width, placeholder for none toggled * fix: network graph move smoothly (#6) * fix: network graph move smoothly * clean up * fix: graph animation even more smooth * fix: padding issue * chore: network icons short description * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- .gitignore | 1 + config/Config.qml | 10 +- config/DashboardConfig.qml | 11 + modules/controlcenter/PaneRegistry.qml | 6 + modules/controlcenter/Panes.qml | 1 + .../ConnectedButtonGroup.qml | 0 .../components/ReadonlySlider.qml | 67 ++ .../controlcenter/dashboard/DashboardPane.qml | 123 ++ .../dashboard/GeneralSection.qml | 81 ++ .../dashboard/PerformanceSection.qml | 85 ++ modules/dashboard/Media.qml | 24 +- modules/dashboard/Performance.qml | 1036 ++++++++++++++--- modules/dashboard/Tabs.qml | 4 +- modules/dashboard/dash/Media.qml | 2 +- services/NetworkUsage.qml | 233 ++++ services/SystemUsage.qml | 169 ++- 16 files changed, 1663 insertions(+), 190 deletions(-) rename modules/controlcenter/{taskbar => components}/ConnectedButtonGroup.qml (100%) create mode 100644 modules/controlcenter/components/ReadonlySlider.qml create mode 100644 modules/controlcenter/dashboard/DashboardPane.qml create mode 100644 modules/controlcenter/dashboard/GeneralSection.qml create mode 100644 modules/controlcenter/dashboard/PerformanceSection.qml create mode 100644 services/NetworkUsage.qml diff --git a/.gitignore b/.gitignore index a114b1bcd..c30a6d926 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.qmlls.ini build/ .cache/ +logs \ No newline at end of file diff --git a/config/Config.qml b/config/Config.qml index 1ec47c197..1c01719fa 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -258,8 +258,16 @@ Singleton { return { enabled: dashboard.enabled, showOnHover: dashboard.showOnHover, - mediaUpdateInterval: dashboard.mediaUpdateInterval, + updateInterval: dashboard.updateInterval, dragThreshold: dashboard.dragThreshold, + performance: { + showBattery: dashboard.performance.showBattery, + showGpu: dashboard.performance.showGpu, + showCpu: dashboard.performance.showCpu, + showMemory: dashboard.performance.showMemory, + showStorage: dashboard.performance.showStorage, + showNetwork: dashboard.performance.showNetwork + }, sizes: { tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, diff --git a/config/DashboardConfig.qml b/config/DashboardConfig.qml index 030292b14..e0895509e 100644 --- a/config/DashboardConfig.qml +++ b/config/DashboardConfig.qml @@ -4,8 +4,19 @@ JsonObject { property bool enabled: true property bool showOnHover: true property int mediaUpdateInterval: 500 + property int resourceUpdateInterval: 1000 property int dragThreshold: 50 property Sizes sizes: Sizes {} + property Performance performance: Performance {} + + component Performance: JsonObject { + property bool showBattery: true + property bool showGpu: true + property bool showCpu: true + property bool showMemory: true + property bool showStorage: true + property bool showNetwork: true + } component Sizes: JsonObject { readonly property int tabIndicatorHeight: 3 diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml index c2a0f3840..ca48551fc 100644 --- a/modules/controlcenter/PaneRegistry.qml +++ b/modules/controlcenter/PaneRegistry.qml @@ -41,6 +41,12 @@ QtObject { readonly property string label: "launcher" readonly property string icon: "apps" readonly property string component: "launcher/LauncherPane.qml" + }, + QtObject { + readonly property string id: "dashboard" + readonly property string label: "dashboard" + readonly property string icon: "dashboard" + readonly property string component: "dashboard/DashboardPane.qml" } ] diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 4a4460ca4..ab2f808e9 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -6,6 +6,7 @@ import "audio" import "appearance" import "taskbar" import "launcher" +import "dashboard" import qs.components import qs.services import qs.config diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml similarity index 100% rename from modules/controlcenter/taskbar/ConnectedButtonGroup.qml rename to modules/controlcenter/components/ConnectedButtonGroup.qml diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml new file mode 100644 index 000000000..169d63653 --- /dev/null +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -0,0 +1,67 @@ +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + property string label: "" + property real value: 0 + property real from: 0 + property real to: 100 + property string suffix: "" + property bool readonly: false + + spacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledText { + visible: root.label !== "" + text: root.label + font.pointSize: Appearance.font.size.normal + color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + visible: root.readonly + text: "lock" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + StyledText { + text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") + font.pointSize: Appearance.font.size.normal + color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal + radius: Appearance.rounding.full + color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) + opacity: root.readonly ? 0.5 : 1.0 + + StyledRect { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * ((root.value - root.from) / (root.to - root.from)) + radius: parent.radius + color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary + } + } +} diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml new file mode 100644 index 000000000..72e3e6e82 --- /dev/null +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -0,0 +1,123 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import qs.utils +import Quickshell +import Quickshell.Widgets +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + + // General Settings + property bool enabled: Config.dashboard.enabled ?? true + property bool showOnHover: Config.dashboard.showOnHover ?? true + property int updateInterval: Config.dashboard.updateInterval ?? 1000 + property int dragThreshold: Config.dashboard.dragThreshold ?? 50 + + // Performance Resources + property bool showBattery: Config.dashboard.performance.showBattery ?? false + property bool showGpu: Config.dashboard.performance.showGpu ?? true + property bool showCpu: Config.dashboard.performance.showCpu ?? true + property bool showMemory: Config.dashboard.performance.showMemory ?? true + property bool showStorage: Config.dashboard.performance.showStorage ?? true + property bool showNetwork: Config.dashboard.performance.showNetwork ?? true + + anchors.fill: parent + + function saveConfig() { + Config.dashboard.enabled = root.enabled; + Config.dashboard.showOnHover = root.showOnHover; + Config.dashboard.updateInterval = root.updateInterval; + Config.dashboard.dragThreshold = root.dragThreshold; + Config.dashboard.performance.showBattery = root.showBattery; + Config.dashboard.performance.showGpu = root.showGpu; + Config.dashboard.performance.showCpu = root.showCpu; + Config.dashboard.performance.showMemory = root.showMemory; + Config.dashboard.performance.showStorage = root.showStorage; + Config.dashboard.performance.showNetwork = root.showNetwork; + // Note: sizes properties are readonly and cannot be modified + Config.save(); + } + + ClippingRectangle { + id: dashboardClippingRect + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal + + radius: dashboardBorder.innerRadius + color: "transparent" + + Loader { + id: dashboardLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + sourceComponent: dashboardContentComponent + } + } + + InnerBorder { + id: dashboardBorder + leftThickness: 0 + rightThickness: Appearance.padding.normal + } + + Component { + id: dashboardContentComponent + + StyledFlickable { + id: dashboardFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: dashboardLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: dashboardFlickable + } + + ColumnLayout { + id: dashboardLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + + spacing: Appearance.spacing.normal + + RowLayout { + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Dashboard") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + } + + // General Settings Section + GeneralSection { + rootItem: root + } + + // Performance Resources Section + PerformanceSection { + rootItem: root + } + } + } + } +} diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml new file mode 100644 index 000000000..bf54e9760 --- /dev/null +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -0,0 +1,81 @@ +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +SectionContainer { + id: root + + required property var rootItem + + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("General Settings") + font.pointSize: Appearance.font.size.normal + } + + SwitchRow { + label: qsTr("Enabled") + checked: root.rootItem.enabled + onToggled: checked => { + root.rootItem.enabled = checked; + root.rootItem.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Show on hover") + checked: root.rootItem.showOnHover + onToggled: checked => { + root.rootItem.showOnHover = checked; + root.rootItem.saveConfig(); + } + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Update interval") + value: root.rootItem.updateInterval + from: 100 + to: 10000 + stepSize: 100 + suffix: "ms" + validator: IntValidator { bottom: 100; top: 10000 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + root.rootItem.updateInterval = Math.round(newValue); + root.rootItem.saveConfig(); + } + } + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Drag threshold") + value: root.rootItem.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { bottom: 0; top: 100 } + formatValueFunction: (val) => Math.round(val).toString() + parseValueFunction: (text) => parseInt(text) + + onValueModified: (newValue) => { + root.rootItem.dragThreshold = Math.round(newValue); + root.rootItem.saveConfig(); + } + } + } +} diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml new file mode 100644 index 000000000..7e72782b1 --- /dev/null +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -0,0 +1,85 @@ +import ".." +import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.UPower +import qs.components +import qs.components.controls +import qs.config +import qs.services + +SectionContainer { + id: root + + required property var rootItem + // GPU toggle is hidden when gpuType is "NONE" (no GPU data available) + readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" + // Battery toggle is hidden when no laptop battery is present + readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery + + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Performance Resources") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root.rootItem + options: { + let opts = []; + if (root.batteryAvailable) + opts.push({ + "label": qsTr("Battery"), + "propertyName": "showBattery", + "onToggled": function(checked) { + root.rootItem.showBattery = checked; + root.rootItem.saveConfig(); + } + }); + + if (root.gpuAvailable) + opts.push({ + "label": qsTr("GPU"), + "propertyName": "showGpu", + "onToggled": function(checked) { + root.rootItem.showGpu = checked; + root.rootItem.saveConfig(); + } + }); + + opts.push({ + "label": qsTr("CPU"), + "propertyName": "showCpu", + "onToggled": function(checked) { + root.rootItem.showCpu = checked; + root.rootItem.saveConfig(); + } + }, { + "label": qsTr("Memory"), + "propertyName": "showMemory", + "onToggled": function(checked) { + root.rootItem.showMemory = checked; + root.rootItem.saveConfig(); + } + }, { + "label": qsTr("Storage"), + "propertyName": "showStorage", + "onToggled": function(checked) { + root.rootItem.showStorage = checked; + root.rootItem.saveConfig(); + } + }, { + "label": qsTr("Network"), + "propertyName": "showNetwork", + "onToggled": function(checked) { + root.rootItem.showNetwork = checked; + root.rootItem.saveConfig(); + } + }); + return opts; + } + } + +} diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index ce5db35ad..722bc9332 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -1,14 +1,12 @@ pragma ComponentBehavior: Bound import qs.components -import qs.components.effects import qs.components.controls import qs.services import qs.utils import qs.config import Caelestia.Services import Quickshell -import Quickshell.Widgets import Quickshell.Services.Mpris import QtQuick import QtQuick.Layouts @@ -142,7 +140,7 @@ Item { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" + source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width @@ -324,7 +322,7 @@ Item { disabled: !Players.list.length active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null - menu.onItemSelected: item => Players.manualActive = item.modelData + menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData menuItems: playerList.instances fallbackIcon: "music_off" @@ -341,13 +339,7 @@ Item { model: Players.list - MenuItem { - required property MprisPlayer modelData - - icon: modelData === Players.active ? "check" : "" - text: Players.getIdentity(modelData) - activeIcon: "animated_images" - } + PlayerItem {} } } @@ -380,13 +372,21 @@ Item { height: visualiser.height * 0.75 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit } } + component PlayerItem: MenuItem { + required property MprisPlayer modelData + + icon: modelData === Players.active ? "check" : "" + text: Players.getIdentity(modelData) + activeIcon: "animated_images" + } + component PlayerControl: IconButton { Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2 diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 5e00d8920..a4e24c40e 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -1,227 +1,967 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.UPower import qs.components import qs.components.misc -import qs.services import qs.config -import QtQuick -import QtQuick.Layouts +import qs.services -RowLayout { +Item { id: root - readonly property int padding: Appearance.padding.large + readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2 function displayTemp(temp: real): string { return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`; } - spacing: Appearance.spacing.large * 3 + implicitWidth: Math.max(minWidth, content.implicitWidth) + implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight - Ref { - service: SystemUsage - } + StyledRect { + id: placeholder + + anchors.centerIn: parent + width: 400 + height: 350 + radius: Appearance.rounding.large + color: Colours.tPalette.m3surfaceContainer + visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) - Resource { - Layout.alignment: Qt.AlignVCenter - Layout.topMargin: root.padding - Layout.bottomMargin: root.padding - Layout.leftMargin: root.padding * 2 + ColumnLayout { + anchors.centerIn: parent + spacing: Appearance.spacing.normal - value1: Math.min(1, SystemUsage.gpuTemp / 90) - value2: SystemUsage.gpuPerc + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "tune" + font.pointSize: Appearance.font.size.extraLarge * 2 + color: Colours.palette.m3onSurfaceVariant + } - label1: root.displayTemp(SystemUsage.gpuTemp) - label2: `${Math.round(SystemUsage.gpuPerc * 100)}%` + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("No widgets enabled") + font.pointSize: Appearance.font.size.large + color: Colours.palette.m3onSurface + } - sublabel1: qsTr("GPU temp") - sublabel2: qsTr("Usage") + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enable widgets in dashboard settings") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } } - Resource { - Layout.alignment: Qt.AlignVCenter - Layout.topMargin: root.padding - Layout.bottomMargin: root.padding + RowLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + visible: !placeholder.visible - primary: true + Ref { + service: SystemUsage + } - value1: Math.min(1, SystemUsage.cpuTemp / 90) - value2: SystemUsage.cpuPerc + ColumnLayout { + id: mainColumn + + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") + + HeroCard { + Layout.fillWidth: true + Layout.minimumWidth: 400 + Layout.preferredHeight: 150 + visible: Config.dashboard.performance.showCpu + icon: "memory" + title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU") + mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%` + mainLabel: qsTr("Usage") + secondaryValue: root.displayTemp(SystemUsage.cpuTemp) + secondaryLabel: qsTr("Temp") + usage: SystemUsage.cpuPerc + temperature: SystemUsage.cpuTemp + accentColor: Colours.palette.m3primary + } + + HeroCard { + Layout.fillWidth: true + Layout.minimumWidth: 400 + Layout.preferredHeight: 150 + visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE" + icon: "desktop_windows" + title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU") + mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%` + mainLabel: qsTr("Usage") + secondaryValue: root.displayTemp(SystemUsage.gpuTemp) + secondaryLabel: qsTr("Temp") + usage: SystemUsage.gpuPerc + temperature: SystemUsage.gpuTemp + accentColor: Colours.palette.m3secondary + } + } - label1: root.displayTemp(SystemUsage.cpuTemp) - label2: `${Math.round(SystemUsage.cpuPerc * 100)}%` + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork + + GaugeCard { + Layout.minimumWidth: 250 + Layout.preferredHeight: 220 + Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork + icon: "memory_alt" + title: qsTr("Memory") + percentage: SystemUsage.memPerc + subtitle: { + const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed); + const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal); + return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; + } + accentColor: Colours.palette.m3tertiary + visible: Config.dashboard.performance.showMemory + } + + StorageGaugeCard { + Layout.minimumWidth: 250 + Layout.preferredHeight: 220 + Layout.fillWidth: !Config.dashboard.performance.showNetwork + visible: Config.dashboard.performance.showStorage + } + + NetworkCard { + Layout.fillWidth: true + Layout.minimumWidth: 200 + Layout.preferredHeight: 220 + visible: Config.dashboard.performance.showNetwork + } + } + } - sublabel1: qsTr("CPU temp") - sublabel2: qsTr("Usage") + BatteryTank { + Layout.preferredWidth: 120 + Layout.preferredHeight: mainColumn.implicitHeight + visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery + } } - Resource { - Layout.alignment: Qt.AlignVCenter - Layout.topMargin: root.padding - Layout.bottomMargin: root.padding - Layout.rightMargin: root.padding * 3 + component BatteryTank: StyledClippingRect { + id: batteryTank + + property real percentage: UPower.displayDevice.percentage + property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging + property color accentColor: Colours.palette.m3primary + property real animatedPercentage: 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.large + Component.onCompleted: animatedPercentage = percentage + onPercentageChanged: animatedPercentage = percentage + + // Background Fill + StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height * batteryTank.animatedPercentage + color: Qt.alpha(batteryTank.accentColor, 0.15) + } - value1: SystemUsage.memPerc - value2: SystemUsage.storagePerc + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.small + + // Header Section + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + MaterialIcon { + text: { + if (!UPower.displayDevice.isLaptopBattery) { + if (PowerProfiles.profile === PowerProfile.PowerSaver) + return "energy_savings_leaf"; + + if (PowerProfiles.profile === PowerProfile.Performance) + return "rocket_launch"; + + return "balance"; + } + if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) + return "battery_full"; + + const perc = UPower.displayDevice.percentage; + const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); + if (perc >= 0.99) + return "battery_full"; + + let level = Math.floor(perc * 7); + if (charging && (level === 4 || level === 1)) + level--; + + return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; + } + font.pointSize: Appearance.font.size.large + color: batteryTank.accentColor + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Battery") + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurface + } + } - label1: { - const fmt = SystemUsage.formatKib(SystemUsage.memUsed); - return `${+fmt.value.toFixed(1)}${fmt.unit}`; - } - label2: { - const fmt = SystemUsage.formatKib(SystemUsage.storageUsed); - return `${Math.floor(fmt.value)}${fmt.unit}`; + Item { + Layout.fillHeight: true + } + + // Bottom Info Section + ColumnLayout { + Layout.fillWidth: true + spacing: -4 + + StyledText { + Layout.alignment: Qt.AlignRight + text: `${Math.round(batteryTank.percentage * 100)}%` + font.pointSize: Appearance.font.size.extraLarge + font.weight: Font.Medium + color: batteryTank.accentColor + } + + StyledText { + Layout.alignment: Qt.AlignRight + text: { + if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) + return qsTr("Full"); + + if (batteryTank.isCharging) + return qsTr("Charging"); + + const s = UPower.displayDevice.timeToEmpty; + if (s === 0) + return qsTr("..."); + + const hr = Math.floor(s / 3600); + const min = Math.floor((s % 3600) / 60); + if (hr > 0) + return `${hr}h ${min}m`; + + return `${min}m`; + } + font.pointSize: Appearance.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + } } - sublabel1: qsTr("Memory") - sublabel2: qsTr("Storage") + Behavior on animatedPercentage { + Anim { + duration: Appearance.anim.durations.large + } + } } - component Resource: Item { - id: res + component CardHeader: RowLayout { + property string icon + property string title + property color accentColor: Colours.palette.m3primary - required property real value1 - required property real value2 - required property string sublabel1 - required property string sublabel2 - required property string label1 - required property string label2 + Layout.fillWidth: true + spacing: Appearance.spacing.small - property bool primary - readonly property real primaryMult: primary ? 1.2 : 1 + MaterialIcon { + text: parent.icon + fill: 1 + color: parent.accentColor + font.pointSize: Appearance.spacing.large + } - readonly property real thickness: Config.dashboard.sizes.resourceProgessThickness * primaryMult + StyledText { + Layout.fillWidth: true + text: parent.title + font.pointSize: Appearance.font.size.normal + elide: Text.ElideRight + } + } - property color fg1: Colours.palette.m3primary - property color fg2: Colours.palette.m3secondary - property color bg1: Colours.palette.m3primaryContainer - property color bg2: Colours.palette.m3secondaryContainer + component ProgressBar: StyledRect { + id: progressBar + + property real value: 0 + property color fgColor: Colours.palette.m3primary + property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + property real animatedValue: 0 + + color: bgColor + radius: Appearance.rounding.full + Component.onCompleted: animatedValue = value + onValueChanged: animatedValue = value + + StyledRect { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * progressBar.animatedValue + color: progressBar.fgColor + radius: Appearance.rounding.full + } - implicitWidth: Config.dashboard.sizes.resourceSize * primaryMult - implicitHeight: Config.dashboard.sizes.resourceSize * primaryMult + Behavior on animatedValue { + Anim { + duration: Appearance.anim.durations.large + } + } + } + + component HeroCard: StyledClippingRect { + id: heroCard + + property string icon + property string title + property string mainValue + property string mainLabel + property string secondaryValue + property string secondaryLabel + property real usage: 0 + property real temperature: 0 + property color accentColor: Colours.palette.m3primary + readonly property real maxTemp: 100 + readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp)) + property real animatedUsage: 0 + property real animatedTemp: 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.large + Component.onCompleted: { + animatedUsage = usage; + animatedTemp = tempProgress; + } + onUsageChanged: animatedUsage = usage + onTempProgressChanged: animatedTemp = tempProgress + + StyledRect { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * heroCard.animatedUsage + color: Qt.alpha(heroCard.accentColor, 0.15) + } - onValue1Changed: canvas.requestPaint() - onValue2Changed: canvas.requestPaint() - onFg1Changed: canvas.requestPaint() - onFg2Changed: canvas.requestPaint() - onBg1Changed: canvas.requestPaint() - onBg2Changed: canvas.requestPaint() + ColumnLayout { + anchors.fill: parent + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + anchors.topMargin: Appearance.padding.normal + anchors.bottomMargin: Appearance.padding.normal + spacing: Appearance.spacing.small + + CardHeader { + icon: heroCard.icon + title: heroCard.title + accentColor: heroCard.accentColor + } + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + spacing: Appearance.spacing.normal + + Column { + Layout.alignment: Qt.AlignBottom + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Row { + spacing: Appearance.spacing.small + + StyledText { + text: heroCard.secondaryValue + font.pointSize: Appearance.font.size.normal + font.weight: Font.Medium + } + + StyledText { + text: heroCard.secondaryLabel + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + anchors.baseline: parent.children[0].baseline + } + } + + ProgressBar { + width: parent.width * 0.5 + height: 6 + value: heroCard.tempProgress + fgColor: heroCard.accentColor + bgColor: Qt.alpha(heroCard.accentColor, 0.2) + } + } + + Item { + Layout.fillWidth: true + } + } + } Column { - anchors.centerIn: parent + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + anchors.rightMargin: 32 + spacing: 0 StyledText { - anchors.horizontalCenter: parent.horizontalCenter - - text: res.label1 - font.pointSize: Appearance.font.size.extraLarge * res.primaryMult + anchors.right: parent.right + text: heroCard.mainLabel + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant } StyledText { - anchors.horizontalCenter: parent.horizontalCenter + anchors.right: parent.right + text: heroCard.mainValue + font.pointSize: Appearance.font.size.extraLarge + font.weight: Font.Medium + color: heroCard.accentColor + } + } - text: res.sublabel1 - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.smaller * res.primaryMult + Behavior on animatedUsage { + Anim { + duration: Appearance.anim.durations.large } } - Column { - anchors.horizontalCenter: parent.right - anchors.top: parent.verticalCenter - anchors.horizontalCenterOffset: -res.thickness / 2 - anchors.topMargin: res.thickness / 2 + Appearance.spacing.small + Behavior on animatedTemp { + Anim { + duration: Appearance.anim.durations.large + } + } + } - StyledText { - anchors.horizontalCenter: parent.horizontalCenter + component GaugeCard: StyledRect { + id: gaugeCard + + property string icon + property string title + property real percentage: 0 + property string subtitle + property color accentColor: Colours.palette.m3primary + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property real animatedPercentage: 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.large + clip: true + Component.onCompleted: animatedPercentage = percentage + onPercentageChanged: animatedPercentage = percentage + + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.smaller - text: res.label2 - font.pointSize: Appearance.font.size.smaller * res.primaryMult + CardHeader { + icon: gaugeCard.icon + title: gaugeCard.title + accentColor: gaugeCard.accentColor } - StyledText { - anchors.horizontalCenter: parent.horizontalCenter + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Canvas { + id: gaugeCanvas + + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: width + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + const cx = width / 2; + const cy = height / 2; + const radius = (Math.min(width, height) - 12) / 2; + const lineWidth = 10; + ctx.beginPath(); + ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); + ctx.stroke(); + if (gaugeCard.animatedPercentage > 0) { + ctx.beginPath(); + ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep * gaugeCard.animatedPercentage); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = gaugeCard.accentColor; + ctx.stroke(); + } + } + Component.onCompleted: requestPaint() + + Connections { + function onAnimatedPercentageChanged() { + gaugeCanvas.requestPaint(); + } + + target: gaugeCard + } + + Connections { + function onPaletteChanged() { + gaugeCanvas.requestPaint(); + } + + target: Colours + } + } + + StyledText { + anchors.centerIn: parent + text: `${Math.round(gaugeCard.percentage * 100)}%` + font.pointSize: Appearance.font.size.extraLarge + font.weight: Font.Medium + color: gaugeCard.accentColor + } + } - text: res.sublabel2 + StyledText { + Layout.alignment: Qt.AlignHCenter + text: gaugeCard.subtitle + font.pointSize: Appearance.font.size.smaller color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small * res.primaryMult } } - Canvas { - id: canvas + Behavior on animatedPercentage { + Anim { + duration: Appearance.anim.durations.large + } + } + } - readonly property real centerX: width / 2 - readonly property real centerY: height / 2 + component StorageGaugeCard: StyledRect { + id: storageGaugeCard + + property int currentDiskIndex: 0 + readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null + property int diskCount: 0 + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property real animatedPercentage: 0 + property color accentColor: Colours.palette.m3secondary + + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.large + clip: true + Component.onCompleted: { + diskCount = SystemUsage.disks.length; + if (currentDisk) + animatedPercentage = currentDisk.perc; + } + onCurrentDiskChanged: { + if (currentDisk) + animatedPercentage = currentDisk.perc; + } - readonly property real arc1Start: degToRad(45) - readonly property real arc1End: degToRad(220) - readonly property real arc2Start: degToRad(230) - readonly property real arc2End: degToRad(360) + // Update diskCount and animatedPercentage when disks data changes + Connections { + function onDisksChanged() { + if (SystemUsage.disks.length !== storageGaugeCard.diskCount) + storageGaugeCard.diskCount = SystemUsage.disks.length; - function degToRad(deg: int): real { - return deg * Math.PI / 180; + // Update animated percentage when disk data refreshes + if (storageGaugeCard.currentDisk) + storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; } + target: SystemUsage + } + + MouseArea { anchors.fill: parent + onWheel: wheel => { + if (wheel.angleDelta.y > 0) + storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; + else if (wheel.angleDelta.y < 0) + storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; + } + } - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.smaller + + CardHeader { + icon: "hard_disk" + title: { + const base = qsTr("Storage"); + if (!storageGaugeCard.currentDisk) + return base; + + return `${base} - ${storageGaugeCard.currentDisk.mount}`; + } + accentColor: storageGaugeCard.accentColor + + // Scroll hint icon + MaterialIcon { + text: "unfold_more" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + visible: storageGaugeCard.diskCount > 1 + opacity: 0.7 + ToolTip.visible: hintHover.hovered + ToolTip.text: qsTr("Scroll to switch disks") + ToolTip.delay: 500 + + HoverHandler { + id: hintHover + } + } + } - ctx.lineWidth = res.thickness; - ctx.lineCap = Appearance.rounding.scale === 0 ? "square" : "round"; + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Canvas { + id: storageGaugeCanvas + + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: width + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + const cx = width / 2; + const cy = height / 2; + const radius = (Math.min(width, height) - 12) / 2; + const lineWidth = 10; + ctx.beginPath(); + ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); + ctx.stroke(); + if (storageGaugeCard.animatedPercentage > 0) { + ctx.beginPath(); + ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep * storageGaugeCard.animatedPercentage); + ctx.lineWidth = lineWidth; + ctx.lineCap = "round"; + ctx.strokeStyle = storageGaugeCard.accentColor; + ctx.stroke(); + } + } + Component.onCompleted: requestPaint() + + Connections { + function onAnimatedPercentageChanged() { + storageGaugeCanvas.requestPaint(); + } + + target: storageGaugeCard + } + + Connections { + function onPaletteChanged() { + storageGaugeCanvas.requestPaint(); + } + + target: Colours + } + } + + StyledText { + anchors.centerIn: parent + text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" + font.pointSize: Appearance.font.size.extraLarge + font.weight: Font.Medium + color: storageGaugeCard.accentColor + } + } - const radius = (Math.min(width, height) - ctx.lineWidth) / 2; - const cx = centerX; - const cy = centerY; - const a1s = arc1Start; - const a1e = arc1End; - const a2s = arc2Start; - const a2e = arc2End; + StyledText { + Layout.alignment: Qt.AlignHCenter + text: { + if (!storageGaugeCard.currentDisk) + return "—"; + + const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); + const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); + return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; + } + font.pointSize: Appearance.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + } - ctx.beginPath(); - ctx.arc(cx, cy, radius, a1s, a1e, false); - ctx.strokeStyle = res.bg1; - ctx.stroke(); + Behavior on animatedPercentage { + Anim { + duration: Appearance.anim.durations.large + } + } + } - ctx.beginPath(); - ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false); - ctx.strokeStyle = res.fg1; - ctx.stroke(); + component NetworkCard: StyledRect { + id: networkCard - ctx.beginPath(); - ctx.arc(cx, cy, radius, a2s, a2e, false); - ctx.strokeStyle = res.bg2; - ctx.stroke(); + property color accentColor: Colours.palette.m3primary - ctx.beginPath(); - ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false); - ctx.strokeStyle = res.fg2; - ctx.stroke(); - } - } + color: Colours.tPalette.m3surfaceContainer + radius: Appearance.rounding.large + clip: true - Behavior on value1 { - Anim {} + Ref { + service: NetworkUsage } - Behavior on value2 { - Anim {} - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.small - Behavior on fg1 { - CAnim {} - } + CardHeader { + icon: "swap_vert" + title: qsTr("Network") + accentColor: networkCard.accentColor + } - Behavior on fg2 { - CAnim {} - } + // Sparkline graph + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + Canvas { + id: sparklineCanvas + + property var downHistory: NetworkUsage.downloadHistory + property var upHistory: NetworkUsage.uploadHistory + property real targetMax: 1024 + property real smoothMax: targetMax + property real slideProgress: 0 + property int _tickCount: 0 + property int _lastTickCount: -1 + + function checkAndAnimate(): void { + const currentLength = (downHistory || []).length; + if (currentLength > 0 && _tickCount !== _lastTickCount) { + _lastTickCount = _tickCount; + updateMax(); + } + } + + function updateMax(): void { + const downHist = downHistory || []; + const upHist = upHistory || []; + const allValues = downHist.concat(upHist); + targetMax = Math.max(...allValues, 1024); + requestPaint(); + } + + anchors.fill: parent + onDownHistoryChanged: checkAndAnimate() + onUpHistoryChanged: checkAndAnimate() + onSmoothMaxChanged: requestPaint() + onSlideProgressChanged: requestPaint() + + onPaint: { + const ctx = getContext("2d"); + ctx.reset(); + const w = width; + const h = height; + const downHist = downHistory || []; + const upHist = upHistory || []; + if (downHist.length < 2 && upHist.length < 2) + return; + + const maxVal = smoothMax; + + const drawLine = (history, color, fillAlpha) => { + if (history.length < 2) + return; + + const len = history.length; + const stepX = w / (NetworkUsage.historyLength - 1); + const startX = w - (len - 1) * stepX - stepX * slideProgress + stepX; + ctx.beginPath(); + ctx.moveTo(startX, h - (history[0] / maxVal) * h); + for (let i = 1; i < len; i++) { + const x = startX + i * stepX; + const y = h - (history[i] / maxVal) * h; + ctx.lineTo(x, y); + } + ctx.strokeStyle = color; + ctx.lineWidth = 2; + ctx.lineCap = "round"; + ctx.lineJoin = "round"; + ctx.stroke(); + ctx.lineTo(startX + (len - 1) * stepX, h); + ctx.lineTo(startX, h); + ctx.closePath(); + ctx.fillStyle = Qt.rgba(Qt.color(color).r, Qt.color(color).g, Qt.color(color).b, fillAlpha); + ctx.fill(); + }; + + drawLine(upHist, Colours.palette.m3secondary.toString(), 0.15); + drawLine(downHist, Colours.palette.m3tertiary.toString(), 0.2); + } + + Component.onCompleted: updateMax() + + Connections { + function onPaletteChanged() { + sparklineCanvas.requestPaint(); + } + + target: Colours + } + + Timer { + interval: Config.dashboard.resourceUpdateInterval + running: true + repeat: true + onTriggered: sparklineCanvas._tickCount++ + } + + NumberAnimation on slideProgress { + from: 0 + to: 1 + duration: Config.dashboard.resourceUpdateInterval + loops: Animation.Infinite + running: true + } + + Behavior on smoothMax { + Anim { + duration: Appearance.anim.durations.large + } + } + } + + // "No data" placeholder + StyledText { + anchors.centerIn: parent + text: qsTr("Collecting data...") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + visible: NetworkUsage.downloadHistory.length < 2 + opacity: 0.6 + } + } - Behavior on bg1 { - CAnim {} - } + // Download row + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "download" + color: Colours.palette.m3tertiary + font.pointSize: Appearance.font.size.normal + } + + StyledText { + text: qsTr("Download") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font.pointSize: Appearance.font.size.normal + font.weight: Font.Medium + color: Colours.palette.m3tertiary + } + } - Behavior on bg2 { - CAnim {} + // Upload row + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "upload" + color: Colours.palette.m3secondary + font.pointSize: Appearance.font.size.normal + } + + StyledText { + text: qsTr("Upload") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font.pointSize: Appearance.font.size.normal + font.weight: Font.Medium + color: Colours.palette.m3secondary + } + } + + // Session totals + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "history" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.normal + } + + StyledText { + text: qsTr("Total") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); + const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); + return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; + } + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } } } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 98ea880e5..1d50d2693 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -60,10 +60,10 @@ Item { id: indicator anchors.top: bar.bottom - anchors.topMargin: Config.dashboard.sizes.tabIndicatorSpacing + anchors.topMargin: 5 implicitWidth: bar.currentItem.implicitWidth - implicitHeight: Config.dashboard.sizes.tabIndicatorHeight + implicitHeight: 3 x: { const tab = bar.currentItem; diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index ad8733520..d65066954 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -106,7 +106,7 @@ Item { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" + source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml new file mode 100644 index 000000000..502ec3ae8 --- /dev/null +++ b/services/NetworkUsage.qml @@ -0,0 +1,233 @@ +pragma Singleton + +import qs.config + +import Quickshell +import Quickshell.Io + +import QtQuick + +Singleton { + id: root + + property int refCount: 0 + + // Current speeds in bytes per second + readonly property real downloadSpeed: _downloadSpeed + readonly property real uploadSpeed: _uploadSpeed + + // Total bytes transferred since tracking started + readonly property real downloadTotal: _downloadTotal + readonly property real uploadTotal: _uploadTotal + + // History of speeds for sparkline (most recent at end) + readonly property var downloadHistory: _downloadHistory + readonly property var uploadHistory: _uploadHistory + readonly property int historyLength: 30 + + // Private properties + property real _downloadSpeed: 0 + property real _uploadSpeed: 0 + property real _downloadTotal: 0 + property real _uploadTotal: 0 + property var _downloadHistory: [] + property var _uploadHistory: [] + + // Previous readings for calculating speed + property real _prevRxBytes: 0 + property real _prevTxBytes: 0 + property real _prevTimestamp: 0 + + // Initial readings for calculating totals + property real _initialRxBytes: 0 + property real _initialTxBytes: 0 + property bool _initialized: false + + function formatBytes(bytes: real): var { + // Handle negative or invalid values + if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { + return { + value: 0, + unit: "B/s" + }; + } + + if (bytes < 1024) { + return { + value: bytes, + unit: "B/s" + }; + } else if (bytes < 1024 * 1024) { + return { + value: bytes / 1024, + unit: "KB/s" + }; + } else if (bytes < 1024 * 1024 * 1024) { + return { + value: bytes / (1024 * 1024), + unit: "MB/s" + }; + } else { + return { + value: bytes / (1024 * 1024 * 1024), + unit: "GB/s" + }; + } + } + + function formatBytesTotal(bytes: real): var { + // Handle negative or invalid values + if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { + return { + value: 0, + unit: "B" + }; + } + + if (bytes < 1024) { + return { + value: bytes, + unit: "B" + }; + } else if (bytes < 1024 * 1024) { + return { + value: bytes / 1024, + unit: "KB" + }; + } else if (bytes < 1024 * 1024 * 1024) { + return { + value: bytes / (1024 * 1024), + unit: "MB" + }; + } else { + return { + value: bytes / (1024 * 1024 * 1024), + unit: "GB" + }; + } + } + + function parseNetDev(content: string): var { + const lines = content.split("\n"); + let totalRx = 0; + let totalTx = 0; + + for (let i = 2; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) + continue; + + const parts = line.split(/\s+/); + if (parts.length < 10) + continue; + + const iface = parts[0].replace(":", ""); + // Skip loopback interface + if (iface === "lo") + continue; + + const rxBytes = parseFloat(parts[1]) || 0; + const txBytes = parseFloat(parts[9]) || 0; + + totalRx += rxBytes; + totalTx += txBytes; + } + + return { + rx: totalRx, + tx: totalTx + }; + } + + FileView { + id: netDevFile + path: "/proc/net/dev" + } + + Timer { + interval: Config.dashboard.resourceUpdateInterval + running: root.refCount > 0 + repeat: true + triggeredOnStart: true + + onTriggered: { + netDevFile.reload(); + const content = netDevFile.text(); + if (!content) + return; + + const data = root.parseNetDev(content); + const now = Date.now(); + + if (!root._initialized) { + root._initialRxBytes = data.rx; + root._initialTxBytes = data.tx; + root._prevRxBytes = data.rx; + root._prevTxBytes = data.tx; + root._prevTimestamp = now; + root._initialized = true; + return; + } + + const timeDelta = (now - root._prevTimestamp) / 1000; // seconds + if (timeDelta > 0) { + // Calculate byte deltas + let rxDelta = data.rx - root._prevRxBytes; + let txDelta = data.tx - root._prevTxBytes; + + // Handle counter overflow (when counters wrap around from max to 0) + // This happens when counters exceed 32-bit or 64-bit limits + if (rxDelta < 0) { + // Counter wrapped around - assume 64-bit counter + rxDelta += Math.pow(2, 64); + } + if (txDelta < 0) { + txDelta += Math.pow(2, 64); + } + + // Calculate speeds + root._downloadSpeed = rxDelta / timeDelta; + root._uploadSpeed = txDelta / timeDelta; + + const maxHistory = root.historyLength + 1; + + if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) { + let newDownHist = root._downloadHistory.slice(); + newDownHist.push(root._downloadSpeed); + if (newDownHist.length > maxHistory) { + newDownHist.shift(); + } + root._downloadHistory = newDownHist; + } + + if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) { + let newUpHist = root._uploadHistory.slice(); + newUpHist.push(root._uploadSpeed); + if (newUpHist.length > maxHistory) { + newUpHist.shift(); + } + root._uploadHistory = newUpHist; + } + } + + // Calculate totals with overflow handling + let downTotal = data.rx - root._initialRxBytes; + let upTotal = data.tx - root._initialTxBytes; + + // Handle counter overflow for totals + if (downTotal < 0) { + downTotal += Math.pow(2, 64); + } + if (upTotal < 0) { + upTotal += Math.pow(2, 64); + } + + root._downloadTotal = downTotal; + root._uploadTotal = upTotal; + + root._prevRxBytes = data.rx; + root._prevTxBytes = data.tx; + root._prevTimestamp = now; + } + } +} diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index bd02da362..114493233 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -8,24 +8,50 @@ import QtQuick Singleton { id: root + // CPU properties + property string cpuName: "" property real cpuPerc property real cpuTemp + + // GPU properties readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType property string autoGpuType: "NONE" + property string gpuName: "" property real gpuPerc property real gpuTemp + + // Memory properties property real memUsed property real memTotal readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 - property real storageUsed - property real storageTotal - property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 + + // Storage properties (aggregated) + readonly property real storagePerc: { + let totalUsed = 0; + let totalSize = 0; + for (const disk of disks) { + totalUsed += disk.used; + totalSize += disk.total; + } + return totalSize > 0 ? totalUsed / totalSize : 0; + } + + // Individual disks: Array of { mount, used, total, free, perc } + property var disks: [] property real lastCpuIdle property real lastCpuTotal property int refCount + function cleanCpuName(name: string): string { + return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim(); + } + + function cleanGpuName(name: string): string { + return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim(); + } + function formatKib(kib: real): var { const mib = 1024; const gib = 1024 ** 2; @@ -54,7 +80,7 @@ Singleton { Timer { running: root.refCount > 0 - interval: 3000 + interval: Config.dashboard.resourceUpdateInterval repeat: true triggeredOnStart: true onTriggered: { @@ -66,6 +92,18 @@ Singleton { } } + // One-time CPU info detection (name) + FileView { + id: cpuinfoInit + + path: "/proc/cpuinfo" + onLoaded: { + const nameMatch = text().match(/model name\s*:\s*(.+)/); + if (nameMatch) + root.cpuName = root.cleanCpuName(nameMatch[1]); + } + } + FileView { id: stat @@ -101,41 +139,120 @@ Singleton { Process { id: storage - command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] + // Get physical disks with aggregated usage from their partitions + // lsblk outputs: NAME SIZE TYPE FSUSED FSSIZE in bytes + command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"] stdout: StdioCollector { onStreamFinished: { - const deviceMap = new Map(); + const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal } + const lines = text.trim().split("\n"); - for (const line of text.trim().split("\n")) { + for (const line of lines) { if (line.trim() === "") continue; - const parts = line.trim().split(/\s+/); - if (parts.length >= 3) { - const device = parts[0]; - const used = parseInt(parts[1], 10) || 0; - const avail = parseInt(parts[2], 10) || 0; - - // Only keep the entry with the largest total space for each device - if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { - deviceMap.set(device, { - used: used, - avail: avail - }); + // Parse KEY="VALUE" format + const nameMatch = line.match(/NAME="([^"]+)"/); + const sizeMatch = line.match(/SIZE="([^"]+)"/); + const typeMatch = line.match(/TYPE="([^"]+)"/); + const fsusedMatch = line.match(/FSUSED="([^"]*)"/); + const fssizeMatch = line.match(/FSSIZE="([^"]*)"/); + + if (!nameMatch || !typeMatch) + continue; + + const name = nameMatch[1]; + const type = typeMatch[1]; + const size = parseInt(sizeMatch?.[1] || "0", 10); + const fsused = parseInt(fsusedMatch?.[1] || "0", 10); + const fssize = parseInt(fssizeMatch?.[1] || "0", 10); + + if (type === "disk") { + // Skip zram (swap) devices + if (name.startsWith("zram")) + continue; + + // Initialize disk entry + if (!diskMap[name]) { + diskMap[name] = { + name: name, + totalSize: size, + used: 0, + fsTotal: 0 + }; + } + } else if (type === "part") { + // Find parent disk (remove trailing numbers/p+numbers) + let parentDisk = name.replace(/p?\d+$/, ""); + // For nvme devices like nvme0n1p1, parent is nvme0n1 + if (name.match(/nvme\d+n\d+p\d+/)) + parentDisk = name.replace(/p\d+$/, ""); + + // Aggregate partition usage to parent disk + if (diskMap[parentDisk]) { + diskMap[parentDisk].used += fsused; + diskMap[parentDisk].fsTotal += fssize; } } } + // Convert map to sorted array + const diskList = []; let totalUsed = 0; - let totalAvail = 0; - - for (const [device, stats] of deviceMap) { - totalUsed += stats.used; - totalAvail += stats.avail; + let totalSize = 0; + + for (const diskName of Object.keys(diskMap).sort()) { + const disk = diskMap[diskName]; + // Use filesystem total if available, otherwise use disk size + const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize; + const used = disk.used; + const perc = total > 0 ? used / total : 0; + + // Convert bytes to KiB for consistency with formatKib + diskList.push({ + mount: disk.name // Using 'mount' property for compatibility + , + used: used / 1024, + total: total / 1024, + free: (total - used) / 1024, + perc: perc + }); + + totalUsed += used; + totalSize += total; } - root.storageUsed = totalUsed; - root.storageTotal = totalUsed + totalAvail; + root.disks = diskList; + } + } + } + + // GPU name detection (one-time) + Process { + id: gpuNameDetect + + running: true + command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"] + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output) + return; + + // Check if it's from nvidia-smi (clean GPU name) + if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { + root.gpuName = root.cleanGpuName(output); + } else { + // Parse lspci output: extract name from brackets or after colon + const bracketMatch = output.match(/\[([^\]]+)\]/); + if (bracketMatch) { + root.gpuName = root.cleanGpuName(bracketMatch[1]); + } else { + const colonMatch = output.match(/:\s*(.+)/); + if (colonMatch) + root.gpuName = root.cleanGpuName(colonMatch[1]); + } + } } } } From 1313b899ad9e0aba73aadedb249da1e5dfbf1486 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=A5=88=E8=A6=8B=20=E3=83=AC=E3=82=A4?= Date: Thu, 19 Feb 2026 17:30:02 +0530 Subject: [PATCH 018/409] config: added option to set session icons (#1189) --- README.md | 6 ++++++ config/Config.qml | 6 ++++++ config/SessionConfig.qml | 8 ++++++++ modules/session/Content.qml | 8 ++++---- 4 files changed, 24 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0c3ca82da..c46f42375 100644 --- a/README.md +++ b/README.md @@ -604,6 +604,12 @@ default, you must create it manually. "dragThreshold": 30, "enabled": true, "vimKeybinds": false, + "icons": { + "logout": "logout", + "shutdown": "power_settings_new", + "hibernate": "downloading", + "reboot": "cached" + }, "commands": { "logout": ["loginctl", "terminate-user", ""], "shutdown": ["systemctl", "poweroff"], diff --git a/config/Config.qml b/config/Config.qml index 1c01719fa..a70da5452 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -359,6 +359,12 @@ Singleton { enabled: session.enabled, dragThreshold: session.dragThreshold, vimKeybinds: session.vimKeybinds, + icons: { + logout: session.icons.logout, + shutdown: session.icons.shutdown, + hibernate: session.icons.hibernate, + reboot: session.icons.reboot + }, commands: { logout: session.commands.logout, shutdown: session.commands.shutdown, diff --git a/config/SessionConfig.qml b/config/SessionConfig.qml index f65ec6d84..414f821a2 100644 --- a/config/SessionConfig.qml +++ b/config/SessionConfig.qml @@ -4,10 +4,18 @@ JsonObject { property bool enabled: true property int dragThreshold: 30 property bool vimKeybinds: false + property Icons icons: Icons {} property Commands commands: Commands {} property Sizes sizes: Sizes {} + component Icons: JsonObject { + property string logout: "logout" + property string shutdown: "power_settings_new" + property string hibernate: "downloading" + property string reboot: "cached" + } + component Commands: JsonObject { property list logout: ["loginctl", "terminate-user", ""] property list shutdown: ["systemctl", "poweroff"] diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 900683f4b..45152e28a 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -18,7 +18,7 @@ Column { SessionButton { id: logout - icon: "logout" + icon: Config.session.icons.logout command: Config.session.commands.logout KeyNavigation.down: shutdown @@ -38,7 +38,7 @@ Column { SessionButton { id: shutdown - icon: "power_settings_new" + icon: Config.session.icons.shutdown command: Config.session.commands.shutdown KeyNavigation.up: logout @@ -60,7 +60,7 @@ Column { SessionButton { id: hibernate - icon: "downloading" + icon: Config.session.icons.hibernate command: Config.session.commands.hibernate KeyNavigation.up: shutdown @@ -70,7 +70,7 @@ Column { SessionButton { id: reboot - icon: "cached" + icon: Config.session.icons.reboot command: Config.session.commands.reboot KeyNavigation.up: hibernate From 4be8fc9693e439c487f091413289b782d78130e7 Mon Sep 17 00:00:00 2001 From: Evertiro Date: Fri, 20 Feb 2026 00:32:09 -0600 Subject: [PATCH 019/409] feat: allow different systems for weather/performance (#1109) * Allow different systems for weather/performance Signed-off-by: Dan Griffiths * readme: update options Signed-off-by: Dan Griffiths --------- Signed-off-by: Dan Griffiths --- README.md | 1 + config/Config.qml | 1 + config/ServiceConfig.qml | 1 + modules/dashboard/Performance.qml | 2 +- 4 files changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c46f42375..30c5c167a 100644 --- a/README.md +++ b/README.md @@ -596,6 +596,7 @@ default, you must create it manually. "playerAliases": [{ "from": "com.github.th_ch.youtube_music", "to": "YT Music" }], "weatherLocation": "", "useFahrenheit": false, + "useFahrenheitPerformance": false, "useTwelveHourClock": false, "smartScheme": true, "visualiserBars": 45 diff --git a/config/Config.qml b/config/Config.qml index a70da5452..8c010146f 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -441,6 +441,7 @@ Singleton { return { weatherLocation: services.weatherLocation, useFahrenheit: services.useFahrenheit, + useFahrenheitPerformance: services.useFahrenheitPerformance, useTwelveHourClock: services.useTwelveHourClock, gpuType: services.gpuType, visualiserBars: services.visualiserBars, diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml index d083b7a11..29600cc54 100644 --- a/config/ServiceConfig.qml +++ b/config/ServiceConfig.qml @@ -4,6 +4,7 @@ import QtQuick JsonObject { property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) + property bool useFahrenheitPerformance: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") property string gpuType: "" property int visualiserBars: 45 diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index a4e24c40e..e73d8ed3d 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -13,7 +13,7 @@ Item { readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2 function displayTemp(temp: real): string { - return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`; + return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? "F" : "C"}`; } implicitWidth: Math.max(minWidth, content.implicitWidth) From dc7af39c909e47b19ac582a1eec39ee60f0dcce1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 22 Feb 2026 01:51:11 +0000 Subject: [PATCH 020/409] [CI] chore: update flake --- flake.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/flake.lock b/flake.lock index a8b0053ac..966a84aed 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1771075454, - "narHash": "sha256-5GlUpibCTqcXq/kCwkLHQGfjuBk2r+ZlWY0MjZo0xtE=", + "lastModified": 1771641231, + "narHash": "sha256-ztwtXtU3xKJhwr69N+tUbnMUv9Bo/p6kdogBo9Yd36s=", "owner": "caelestia-dots", "repo": "cli", - "rev": "d890f7c3af4e7a900338bdf6400c2cf76de89a19", + "rev": "a6defd292136ac3a52fb0d39f045a0882dda6354", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771008912, - "narHash": "sha256-gf2AmWVTs8lEq7z/3ZAsgnZDhWIckkb+ZnAo5RzSxJg=", + "lastModified": 1771369470, + "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", "owner": "nixos", "repo": "nixpkgs", - "rev": "a82ccc39b39b621151d6732718e3e250109076fa", + "rev": "0182a361324364ae3f436a63005877674cf45efb", "type": "github" }, "original": { From 71f291f79bf7c35ad7db2c0061efc80cf768426a Mon Sep 17 00:00:00 2001 From: kizo_aria Date: Tue, 24 Feb 2026 07:05:19 +0000 Subject: [PATCH 021/409] fix: add general.logo to example config (#1186) documenting the ability to change the main logo, affects the bar, dashboard and lock screen --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 30c5c167a..886f0611c 100644 --- a/README.md +++ b/README.md @@ -253,6 +253,7 @@ default, you must create it manually. } }, "general": { + "logo": "caelestia", "apps": { "terminal": ["foot"], "audio": ["pavucontrol"], From 278fd4a4ed1bfb42c3fe197ff38b587539c012aa Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 1 Mar 2026 02:04:17 +0000 Subject: [PATCH 022/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 966a84aed..eb5d304ab 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1771641231, - "narHash": "sha256-ztwtXtU3xKJhwr69N+tUbnMUv9Bo/p6kdogBo9Yd36s=", + "lastModified": 1771987897, + "narHash": "sha256-5pNQFGxG3fxS9pGnNBJjT76veotKIKq2XpAVFGAhCdI=", "owner": "caelestia-dots", "repo": "cli", - "rev": "a6defd292136ac3a52fb0d39f045a0882dda6354", + "rev": "b0d68f0a1c48fa138d6fde94dcbecea801a86a01", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1771369470, - "narHash": "sha256-0NBlEBKkN3lufyvFegY4TYv5mCNHbi5OmBDrzihbBMQ=", + "lastModified": 1772198003, + "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "0182a361324364ae3f436a63005877674cf45efb", + "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1770693276, - "narHash": "sha256-ngXnN5YXu+f45+QGYNN/VEBMQmcBCYGRCqwaK8cxY1s=", + "lastModified": 1771926182, + "narHash": "sha256-QbXuSLhiSxOq6ydBL3+KGe1aiYWBW+e3J6qjJZaRMq0=", "ref": "refs/heads/master", - "rev": "dacfa9de829ac7cb173825f593236bf2c21f637e", - "revCount": 735, + "rev": "cddb4f061bab495f4473ca5f2c571b6c710efef7", + "revCount": 744, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 658e09f89664978497a81f744a8f9186ee32c518 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 8 Mar 2026 01:55:47 +0000 Subject: [PATCH 023/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index eb5d304ab..9f1b2ea0e 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1771987897, - "narHash": "sha256-5pNQFGxG3fxS9pGnNBJjT76veotKIKq2XpAVFGAhCdI=", + "lastModified": 1772764582, + "narHash": "sha256-hSwjmpXHFqzSXrndVekA0IheKrbC7wi0IbfZTYwlmXw=", "owner": "caelestia-dots", "repo": "cli", - "rev": "b0d68f0a1c48fa138d6fde94dcbecea801a86a01", + "rev": "4bcd42f482d038b98145b0b03388244b68b7d35d", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772198003, - "narHash": "sha256-I45esRSssFtJ8p/gLHUZ1OUaaTaVLluNkABkk6arQwE=", + "lastModified": 1772773019, + "narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=", "owner": "nixos", "repo": "nixpkgs", - "rev": "dd9b079222d43e1943b6ebd802f04fd959dc8e61", + "rev": "aca4d95fce4914b3892661bcb80b8087293536c6", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1771926182, - "narHash": "sha256-QbXuSLhiSxOq6ydBL3+KGe1aiYWBW+e3J6qjJZaRMq0=", + "lastModified": 1772925576, + "narHash": "sha256-mMoiXABDtkSJxCYDrkhJ/TrrJf5M46oUfIlJvv2gkZ0=", "ref": "refs/heads/master", - "rev": "cddb4f061bab495f4473ca5f2c571b6c710efef7", - "revCount": 744, + "rev": "15a84097653593dd15fad59a56befc2b7bdc270d", + "revCount": 750, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 2b73eac175f386e2eaaa6beabd196ce9ddde2ea3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chlo=C3=A9=20Legu=C3=A9?= Date: Sun, 8 Mar 2026 03:57:47 -0400 Subject: [PATCH 024/409] picker: fix large screenshot not opening/copying to clipboard (#1250) --- modules/areapicker/Picker.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 35b35a2e4..08f46dfb1 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -80,8 +80,8 @@ MouseArea { } else { Quickshell.execDetached(["swappy", "-f", path]); } + closeAnim.start(); }); - closeAnim.start(); } onClientsChanged: checkClientRects(mouseX, mouseY) From 5bde26496e0394cd7267966adb08e56c304bc24e Mon Sep 17 00:00:00 2001 From: Evertiro Date: Sun, 8 Mar 2026 04:33:31 -0500 Subject: [PATCH 025/409] bar: allow hiding tray icons (#1227) * First pass at hiding systray icons Signed-off-by: Dan Griffiths * Don't dump all IDs for no reason >_< Signed-off-by: Dan Griffiths * Better handling for hiding tray icons Signed-off-by: Dan Griffiths * Re-add EOF newline Signed-off-by: Dan Griffiths * Hide popouts too Signed-off-by: Dan Griffiths * Hide the expand icon if no icons are visible Signed-off-by: Dan Griffiths * Update modules/bar/components/Tray.qml Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> * Update modules/bar/components/Tray.qml Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> * Update modules/bar/components/Tray.qml Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> * That needs to be inverted * Clean up Signed-off-by: Dan Griffiths * fix --------- Signed-off-by: Dan Griffiths Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- config/BarConfig.qml | 1 + modules/bar/components/Tray.qml | 7 +++++-- modules/bar/popouts/Content.qml | 2 +- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/config/BarConfig.qml b/config/BarConfig.qml index cf33fd21d..36a5f784b 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -89,6 +89,7 @@ JsonObject { property bool recolour: false property bool compact: false property list iconSubs: [] + property list hiddenIcons: [] } component Status: JsonObject { diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 96956f6f4..7bafda16f 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config +import Quickshell import Quickshell.Services.SystemTray import QtQuick @@ -66,7 +67,9 @@ StyledRect { Repeater { id: items - model: SystemTray.items + model: ScriptModel { + values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id)) + } TrayItem {} } @@ -82,7 +85,7 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - active: Config.bar.tray.compact + active: Config.bar.tray.compact && items.count > 0 sourceComponent: Item { implicitWidth: expandIconInner.implicitWidth diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 40768444f..6543e584d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -128,7 +128,7 @@ Item { Repeater { model: ScriptModel { - values: [...SystemTray.items.values] + values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id)) } Popout { From e183599ce9e2c8d30a14631d53eb9947220c0812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas?= <67807483+Mestane@users.noreply.github.com> Date: Sun, 8 Mar 2026 12:36:09 +0300 Subject: [PATCH 026/409] fix: unify wifi toggle state with Nmcli service to prevent desync (#1232) --- modules/utilities/cards/Toggles.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 5b57528bc..dd4a687ba 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -38,8 +38,8 @@ StyledRect { Toggle { icon: "wifi" - checked: Network.wifiEnabled - onClicked: Network.toggleWifi() + checked: Nmcli.wifiEnabled + onClicked: Nmcli.toggleWifi() } Toggle { From 1bb7afe7b02dbf6114687d7fede00fb980a5f5be Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Mon, 9 Mar 2026 14:27:43 +0100 Subject: [PATCH 027/409] feat: add Logo shape component (#1247) * Logo Shape component - Added Logo.qml component with scaling - Updated OsIcon to use Logo component * missed removing tour * [CI] chore: update flake * Colours.palette defaults * fixed import, added logo to Fetch.qml * single shape parent, prop changes * prop changes --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- components/Logo.qml | 69 +++++++++++++++++++++++++++++++ modules/bar/components/OsIcon.qml | 30 +++++++++++--- modules/lock/Fetch.qml | 31 ++++++++++---- 3 files changed, 115 insertions(+), 15 deletions(-) create mode 100644 components/Logo.qml diff --git a/components/Logo.qml b/components/Logo.qml new file mode 100644 index 000000000..3ab4f2b25 --- /dev/null +++ b/components/Logo.qml @@ -0,0 +1,69 @@ +import QtQuick +import QtQuick.Shapes +import qs.services + +Item { + id: root + implicitWidth: designWidth + implicitHeight: designHeight + + readonly property real designWidth: 128 + readonly property real designHeight: 90.38 + + property color topColour: Colours.palette.m3primary + property color bottomColour: Colours.palette.m3onSurface + + Shape { + anchors.centerIn: parent + width: root.designWidth + height: root.designHeight + scale: Math.min(root.width / width, root.height / height) + transformOrigin: Item.Center + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" + } + } + + ShapePath { + fillColor: root.bottomColour + strokeColor: "transparent" + + PathSvg { + path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" + } + } + } +} diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index 2bc386441..a61500abb 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -3,9 +3,13 @@ import qs.services import qs.config import qs.utils import QtQuick +import qs.components Item { id: root + + implicitWidth: Appearance.font.size.large * 1.2 + implicitHeight: Appearance.font.size.large * 1.2 MouseArea { anchors.fill: parent @@ -16,13 +20,27 @@ Item { } } - ColouredIcon { + Loader { anchors.centerIn: parent - source: SysInfo.osLogo - implicitSize: Appearance.font.size.large * 1.2 - colour: Colours.palette.m3tertiary + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } - implicitWidth: Appearance.font.size.large * 1.2 - implicitHeight: Appearance.font.size.large * 1.2 + Component { + id: caelestiaLogo + + Logo { + implicitWidth: Appearance.font.size.large * 1.8 + implicitHeight: Appearance.font.size.large * 1.8 + } + } + + Component { + id: distroIcon + + ColouredIcon { + source: SysInfo.osLogo + implicitSize: Appearance.font.size.large * 1.2 + colour: Colours.palette.m3tertiary + } + } } diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index ded56084b..55d6aa764 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -51,7 +51,7 @@ ColumnLayout { Layout.fillHeight: true active: !iconLoader.active - sourceComponent: OsLogo {} + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } } @@ -66,7 +66,7 @@ ColumnLayout { Layout.fillHeight: true active: root.width > 320 - sourceComponent: OsLogo {} + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } ColumnLayout { @@ -142,15 +142,28 @@ ColumnLayout { } } - component WrappedLoader: Loader { - visible: active + Component { + id: caelestiaLogo + + Logo { + width: height + height: height + } + } + + Component { + id: distroIcon + + ColouredIcon { + source: SysInfo.osLogo + implicitSize: height + colour: Colours.palette.m3primary + layer.enabled: Config.lock.recolourLogo + } } - component OsLogo: ColouredIcon { - source: SysInfo.osLogo - implicitSize: height - colour: Colours.palette.m3primary - layer.enabled: Config.lock.recolourLogo || SysInfo.isDefaultLogo + component WrappedLoader: Loader { + visible: active } component FetchText: MonoText { From 0d56db3b6cd28083f4dfd19815fef2730668a78f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bilal=20=C3=96zel?= <112190455+Shinobu420@users.noreply.github.com> Date: Mon, 9 Mar 2026 15:33:33 +0100 Subject: [PATCH 028/409] systemusage: improve GPU detection for AMD RX series GPU (#1246) * SystemUsage:improve GPU-Detection for AMD RX series GPU updated the gpuNameDetect command with glxinfo to fix gpu name detection * SystemUsage: adjust lspci command to detect graphics card better * SystemUsage: adjust regex to extract name out of last bracket * clean less * no need xargs --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- services/SystemUsage.qml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 114493233..ce6201792 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -49,7 +49,7 @@ Singleton { } function cleanGpuName(name: string): string { - return name.replace(/NVIDIA GeForce /gi, "").replace(/NVIDIA /gi, "").replace(/AMD Radeon /gi, "").replace(/AMD /gi, "").replace(/Intel /gi, "").replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim(); + return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim(); } function formatKib(kib: real): var { @@ -232,7 +232,7 @@ Singleton { id: gpuNameDetect running: true - command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || lspci 2>/dev/null | grep -i 'vga\\|3d\\|display' | head -1"] + command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"] stdout: StdioCollector { onStreamFinished: { const output = text.trim(); @@ -242,9 +242,12 @@ Singleton { // Check if it's from nvidia-smi (clean GPU name) if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { root.gpuName = root.cleanGpuName(output); + } else if (output.toLowerCase().includes("rx")) { + root.gpuName = root.cleanGpuName(output); } else { // Parse lspci output: extract name from brackets or after colon - const bracketMatch = output.match(/\[([^\]]+)\]/); + // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0) + const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/); if (bracketMatch) { root.gpuName = root.cleanGpuName(bracketMatch[1]); } else { From 3e0360401bbbb0f640958998f6625495e5b3fdff Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Tue, 10 Mar 2026 15:22:23 +0100 Subject: [PATCH 029/409] dashboard: dynamic dashboard tabs + fix performance settings updating (#1253) * [CI] chore: update flake * Dashboard perf settings save, visibility on none enabled * Dashboard heigh stutter fixed, persist current tab * restore binding * wrapper async=false * ScriptModel, centralized tabs/panes, individual toggle * fixes, missed mediaUpdateInterval, passing values --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- config/Config.qml | 3 +- config/DashboardConfig.qml | 4 + .../controlcenter/dashboard/DashboardPane.qml | 20 +++- .../dashboard/GeneralSection.qml | 105 ++++++++++++----- .../dashboard/PerformanceSection.qml | 45 ++++++-- modules/dashboard/Content.qml | 108 ++++++++++++------ modules/dashboard/Tabs.qml | 36 +++--- modules/dashboard/Wrapper.qml | 1 - 8 files changed, 219 insertions(+), 103 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index 8c010146f..fe0728657 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -258,7 +258,8 @@ Singleton { return { enabled: dashboard.enabled, showOnHover: dashboard.showOnHover, - updateInterval: dashboard.updateInterval, + mediaUpdateInterval: dashboard.mediaUpdateInterval, + resourceUpdateInterval: dashboard.resourceUpdateInterval, dragThreshold: dashboard.dragThreshold, performance: { showBattery: dashboard.performance.showBattery, diff --git a/config/DashboardConfig.qml b/config/DashboardConfig.qml index e0895509e..0a16cc1f9 100644 --- a/config/DashboardConfig.qml +++ b/config/DashboardConfig.qml @@ -6,6 +6,10 @@ JsonObject { property int mediaUpdateInterval: 500 property int resourceUpdateInterval: 1000 property int dragThreshold: 50 + property bool showDashboard: true + property bool showMedia: true + property bool showPerformance: true + property bool showWeather: true property Sizes sizes: Sizes {} property Performance performance: Performance {} diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 72e3e6e82..df29f0964 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -22,15 +22,22 @@ Item { // General Settings property bool enabled: Config.dashboard.enabled ?? true property bool showOnHover: Config.dashboard.showOnHover ?? true - property int updateInterval: Config.dashboard.updateInterval ?? 1000 + property int mediaUpdateInterval: Config.dashboard.mediaUpdateInterval ?? 1000 + property int resourceUpdateInterval: Config.dashboard.resourceUpdateInterval ?? 1000 property int dragThreshold: Config.dashboard.dragThreshold ?? 50 - + + // Dashboard Tabs + property bool showDashboard: Config.dashboard.showDashboard ?? true + property bool showMedia: Config.dashboard.showMedia ?? true + property bool showPerformance: Config.dashboard.showPerformance ?? true + property bool showWeather: Config.dashboard.showWeather ?? true + // Performance Resources property bool showBattery: Config.dashboard.performance.showBattery ?? false property bool showGpu: Config.dashboard.performance.showGpu ?? true property bool showCpu: Config.dashboard.performance.showCpu ?? true property bool showMemory: Config.dashboard.performance.showMemory ?? true - property bool showStorage: Config.dashboard.performance.showStorage ?? true + property bool showStorage: Config.dashboard.performance.showStorage ?? true property bool showNetwork: Config.dashboard.performance.showNetwork ?? true anchors.fill: parent @@ -38,8 +45,13 @@ Item { function saveConfig() { Config.dashboard.enabled = root.enabled; Config.dashboard.showOnHover = root.showOnHover; - Config.dashboard.updateInterval = root.updateInterval; + Config.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; + Config.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; Config.dashboard.dragThreshold = root.dragThreshold; + Config.dashboard.showDashboard = root.showDashboard; + Config.dashboard.showMedia = root.showMedia; + Config.dashboard.showPerformance = root.showPerformance; + Config.dashboard.showWeather = root.showWeather; Config.dashboard.performance.showBattery = root.showBattery; Config.dashboard.performance.showGpu = root.showGpu; Config.dashboard.performance.showCpu = root.showCpu; diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index bf54e9760..95e7531ed 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -38,44 +38,91 @@ SectionContainer { } } - SectionContainer { - contentSpacing: Appearance.spacing.normal + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal - SliderInput { + SwitchRow { Layout.fillWidth: true - - label: qsTr("Update interval") - value: root.rootItem.updateInterval - from: 100 - to: 10000 - stepSize: 100 - suffix: "ms" - validator: IntValidator { bottom: 100; top: 10000 } - formatValueFunction: (val) => Math.round(val).toString() - parseValueFunction: (text) => parseInt(text) - - onValueModified: (newValue) => { - root.rootItem.updateInterval = Math.round(newValue); + label: qsTr("Show Dashboard tab") + checked: root.rootItem.showDashboard + onToggled: checked => { + root.rootItem.showDashboard = checked; root.rootItem.saveConfig(); } } - SliderInput { + SwitchRow { Layout.fillWidth: true - - label: qsTr("Drag threshold") - value: root.rootItem.dragThreshold - from: 0 - to: 100 - suffix: "px" - validator: IntValidator { bottom: 0; top: 100 } - formatValueFunction: (val) => Math.round(val).toString() - parseValueFunction: (text) => parseInt(text) - - onValueModified: (newValue) => { - root.rootItem.dragThreshold = Math.round(newValue); + label: qsTr("Show Media tab") + checked: root.rootItem.showMedia + onToggled: checked => { + root.rootItem.showMedia = checked; root.rootItem.saveConfig(); } } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Show Performance tab") + checked: root.rootItem.showPerformance + onToggled: checked => { + root.rootItem.showPerformance = checked; + root.rootItem.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Show Weather tab") + checked: root.rootItem.showWeather + onToggled: checked => { + root.rootItem.showWeather = checked; + root.rootItem.saveConfig(); + } + } + } + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Media update interval") + value: root.rootItem.mediaUpdateInterval + from: 100 + to: 10000 + stepSize: 100 + suffix: "ms" + validator: IntValidator { + bottom: 100 + top: 10000 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + + onValueModified: newValue => { + root.rootItem.mediaUpdateInterval = Math.round(newValue); + root.rootItem.saveConfig(); + } + } + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Drag threshold") + value: root.rootItem.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { + bottom: 0 + top: 100 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + + onValueModified: newValue => { + root.rootItem.dragThreshold = Math.round(newValue); + root.rootItem.saveConfig(); + } } } diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index 7e72782b1..ac84752b6 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -33,7 +33,7 @@ SectionContainer { opts.push({ "label": qsTr("Battery"), "propertyName": "showBattery", - "onToggled": function(checked) { + "onToggled": function (checked) { root.rootItem.showBattery = checked; root.rootItem.saveConfig(); } @@ -41,39 +41,39 @@ SectionContainer { if (root.gpuAvailable) opts.push({ - "label": qsTr("GPU"), - "propertyName": "showGpu", - "onToggled": function(checked) { - root.rootItem.showGpu = checked; - root.rootItem.saveConfig(); - } - }); + "label": qsTr("GPU"), + "propertyName": "showGpu", + "onToggled": function (checked) { + root.rootItem.showGpu = checked; + root.rootItem.saveConfig(); + } + }); opts.push({ "label": qsTr("CPU"), "propertyName": "showCpu", - "onToggled": function(checked) { + "onToggled": function (checked) { root.rootItem.showCpu = checked; root.rootItem.saveConfig(); } }, { "label": qsTr("Memory"), "propertyName": "showMemory", - "onToggled": function(checked) { + "onToggled": function (checked) { root.rootItem.showMemory = checked; root.rootItem.saveConfig(); } }, { "label": qsTr("Storage"), "propertyName": "showStorage", - "onToggled": function(checked) { + "onToggled": function (checked) { root.rootItem.showStorage = checked; root.rootItem.saveConfig(); } }, { "label": qsTr("Network"), "propertyName": "showNetwork", - "onToggled": function(checked) { + "onToggled": function (checked) { root.rootItem.showNetwork = checked; root.rootItem.saveConfig(); } @@ -82,4 +82,25 @@ SectionContainer { } } + SliderInput { + Layout.fillWidth: true + + label: qsTr("Resource update interval") + value: root.rootItem.resourceUpdateInterval + from: 100 + to: 10000 + stepSize: 100 + suffix: "ms" + validator: IntValidator { + bottom: 100 + top: 10000 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + + onValueModified: newValue => { + root.rootItem.resourceUpdateInterval = Math.round(newValue); + root.rootItem.saveConfig(); + } + } } diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 1cc960a47..bbb42724c 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -14,6 +14,37 @@ Item { required property PersistentProperties visibilities required property PersistentProperties state required property FileDialog facePicker + + readonly property var dashboardTabs: { + const allTabs = [ + { + component: dashComponent, + iconName: "dashboard", + text: qsTr("Dashboard"), + enabled: Config.dashboard.showDashboard + }, + { + component: mediaComponent, + iconName: "queue_music", + text: qsTr("Media"), + enabled: Config.dashboard.showMedia + }, + { + component: performanceComponent, + iconName: "speed", + text: qsTr("Performance"), + enabled: Config.dashboard.showPerformance && (Config.dashboard.performance.showCpu || Config.dashboard.performance.showGpu || Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork || Config.dashboard.performance.showBattery) + }, + { + component: weatherComponent, + iconName: "cloud", + text: qsTr("Weather"), + enabled: Config.dashboard.showWeather + } + ]; + return allTabs.filter(tab => tab.enabled); + } + readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2 @@ -31,6 +62,7 @@ Item { nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 state: root.state + tabs: root.dashboardTabs } ClippingRectangle { @@ -86,33 +118,58 @@ Item { RowLayout { id: row - Pane { - index: 0 - sourceComponent: Dash { - visibilities: root.visibilities - state: root.state - facePicker: root.facePicker + Repeater { + model: ScriptModel { + values: root.dashboardTabs } - } - Pane { - index: 1 - sourceComponent: Media { - visibilities: root.visibilities + delegate: Loader { + id: paneLoader + + required property int index + required property var modelData + + Layout.alignment: Qt.AlignTop + + sourceComponent: modelData.component + + Component.onCompleted: active = Qt.binding(() => { + if (index === view.currentIndex) + return true; + const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); + const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); + return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); + }) } } + } - Pane { - index: 2 - sourceComponent: Performance {} + Component { + id: dashComponent + Dash { + visibilities: root.visibilities + state: root.state + facePicker: root.facePicker } + } - Pane { - index: 3 - sourceComponent: Weather {} + Component { + id: mediaComponent + Media { + visibilities: root.visibilities } } + Component { + id: performanceComponent + Performance {} + } + + Component { + id: weatherComponent + Weather {} + } + Behavior on contentX { Anim {} } @@ -132,21 +189,4 @@ Item { easing.bezierCurve: Appearance.anim.curves.emphasized } } - - component Pane: Loader { - id: pane - - required property int index - - Layout.alignment: Qt.AlignTop - - Component.onCompleted: active = Qt.binding(() => { - // Always keep current tab loaded - if (pane.index === view.currentIndex) - return true; - const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); - const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); - return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); - }) - } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 1d50d2693..ed4613d9a 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -14,6 +14,8 @@ Item { required property real nonAnimWidth required property PersistentProperties state + required property var tabs + readonly property alias count: bar.count implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight @@ -30,30 +32,18 @@ Item { onCurrentIndexChanged: root.state.currentTab = currentIndex - Tab { - iconName: "dashboard" - text: qsTr("Dashboard") - } - - Tab { - iconName: "queue_music" - text: qsTr("Media") - } + Repeater { + model: ScriptModel { + values: root.tabs + } - Tab { - iconName: "speed" - text: qsTr("Performance") - } + delegate: Tab { + required property var modelData - Tab { - iconName: "cloud" - text: qsTr("Weather") + iconName: modelData.iconName + text: modelData.text + } } - - // Tab { - // iconName: "workspaces" - // text: qsTr("Workspaces") - // } } Item { @@ -62,11 +52,13 @@ Item { anchors.top: bar.bottom anchors.topMargin: 5 - implicitWidth: bar.currentItem.implicitWidth + implicitWidth: bar.currentItem?.implicitWidth ?? 0 implicitHeight: 3 x: { const tab = bar.currentItem; + if (!tab) + return 0; const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; } diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 0e37909e8..81bfcd328 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -82,7 +82,6 @@ Item { running: true interval: Appearance.anim.durations.extraLarge onTriggered: { - content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); content.visible = true; } } From 96f54e7b70297528dc2781385b2b955cfde3d544 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:28:36 +1100 Subject: [PATCH 030/409] picker: use hyprctl proc to get cursor on init --- modules/areapicker/Picker.qml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 08f46dfb1..f4f4a3679 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -5,6 +5,7 @@ import qs.services import qs.config import Caelestia import Quickshell +import Quickshell.Io import Quickshell.Wayland import QtQuick import QtQuick.Effects @@ -191,6 +192,17 @@ MouseArea { } } + Process { + running: true + command: ["hyprctl", "cursorpos", "-j"] + stdout: StdioCollector { + onStreamFinished: { + const pos = JSON.parse(text); + root.checkClientRects(pos.x - root.screen.x, pos.y - root.screen.y); + } + } + } + Loader { id: screencopy From b4514a7820ed5dcf845fc1ce068d1ee50a89927e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:28:44 +1100 Subject: [PATCH 031/409] extras: fix typo --- extras/version.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/extras/version.cpp b/extras/version.cpp index e1a0cf302..d63434170 100644 --- a/extras/version.cpp +++ b/extras/version.cpp @@ -10,7 +10,8 @@ int main(int argc, char* argv[]) { std::cout << GIT_REVISION << std::endl; std::cout << DISTRIBUTOR << std::endl; } else if (arg == "-s" || arg == "--short") { - std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION << ", distrubuted by: " << DISTRIBUTOR << std::endl; + std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR << std::endl; } else { std::cout << "Usage: " << argv[0] << " [-t | --terse] [-s | --short]" << std::endl; return arg != "-h" && arg != "--help"; From 30c36e356d4a478cdf4f06fbad48306fa9b99b35 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:49:55 +1100 Subject: [PATCH 032/409] notifs: use adaptive timer for timeStr instead of reactive binding Replace the per-second reactive binding with an imperative timer that adapts its interval based on notification age: 5s for <1min, 30s for <10min, 60s for <1h, 5min for <1d, 1h for older. --- services/Notifs.qml | 40 ++++++++++++++++++++++++++++------------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/services/Notifs.qml b/services/Notifs.qml index 2ebc32db1..aa440feb1 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -146,21 +146,37 @@ Singleton { property var locks: new Set() property date time: new Date() - readonly property string timeStr: { - const diff = Time.date.getTime() - time.getTime(); - const m = Math.floor(diff / 60000); + property string timeStr: qsTr("now") - if (m < 1) - return qsTr("now"); + function updateTimeStr(): void { + const diff = Date.now() - time.getTime(); + const m = Math.floor(diff / 60000); - const h = Math.floor(m / 60); - const d = Math.floor(h / 24); + if (m < 1) { + timeStr = qsTr("now"); + timeStrTimer.interval = 5000; + } else { + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); + + if (d > 0) { + timeStr = `${d}d`; + timeStrTimer.interval = 3600000; + } else if (h > 0) { + timeStr = `${h}h`; + timeStrTimer.interval = 300000; + } else { + timeStr = `${m}m`; + timeStrTimer.interval = m < 10 ? 30000 : 60000; + } + } + } - if (d > 0) - return `${d}d`; - if (h > 0) - return `${h}h`; - return `${m}m`; + readonly property Timer timeStrTimer: Timer { + running: !notif.closed + repeat: true + interval: 5000 + onTriggered: notif.updateTimeStr() } property Notification notification From 11f5a2ee45e73bdb7b18ec06330bc5a3ecb4c78a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:50:26 +1100 Subject: [PATCH 033/409] network: debounce nmcli monitor events Batch rapid nmcli monitor events with a 200ms debounce timer instead of spawning processes on every event line. --- services/Network.qml | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/services/Network.qml b/services/Network.qml index f3dfc3ea1..ede37c802 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -309,16 +309,23 @@ Singleton { return octets.join("."); } + Timer { + id: monitorDebounce + + interval: 200 + onTriggered: { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + getEthernetDevices(); + } + } + Process { running: true command: ["nmcli", "m"] stdout: SplitParser { - onRead: { - Nmcli.getNetworks(() => { - syncNetworksFromNmcli(); - }); - getEthernetDevices(); - } + onRead: monitorDebounce.start() } } } From 9464c72f7a16154a9cdf1bc95588e07adc4a0870 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:51:44 +1100 Subject: [PATCH 034/409] nmcli: use Map-based lookups for network deduplication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace O(n²) nested .filter()/.find() loops with Map-keyed lookups for both removal and update passes in getNetworks(). --- services/Nmcli.qml | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 36bd3e6df..812387f1e 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -751,17 +751,25 @@ Singleton { const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; - const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); - for (const network of destroyed) { - const index = rNetworks.indexOf(network); - if (index >= 0) { - rNetworks.splice(index, 1); - network.destroy(); + const newMap = new Map(); + for (const n of networks) + newMap.set(`${n.frequency}:${n.ssid}:${n.bssid}`, n); + + for (let i = rNetworks.length - 1; i >= 0; i--) { + const rn = rNetworks[i]; + const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; + if (!newMap.has(key)) { + rNetworks.splice(i, 1); + rn.destroy(); } } - for (const network of networks) { - const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); + const existingMap = new Map(); + for (const rn of rNetworks) + existingMap.set(`${rn.frequency}:${rn.ssid}:${rn.bssid}`, rn); + + for (const [key, network] of newMap) { + const match = existingMap.get(key); if (match) { match.lastIpcObject = network; } else { From c14db315f64b041b3e87c2997777f1ef2da83905 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 21:55:22 +1100 Subject: [PATCH 035/409] audio: replace reactive reduce with imperative node categorisation --- services/Audio.qml | 48 +++++++++++++++++++++++++++------------------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/services/Audio.qml b/services/Audio.qml index 908d1563c..14d0a4e81 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -13,26 +13,9 @@ Singleton { property string previousSinkName: "" property string previousSourceName: "" - readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { - if (!node.isStream) { - if (node.isSink) - acc.sinks.push(node); - else if (node.audio) - acc.sources.push(node); - } else if (node.isStream && node.audio) { - // Application streams (output streams) - acc.streams.push(node); - } - return acc; - }, { - sources: [], - sinks: [], - streams: [] - }) - - readonly property list sinks: nodes.sinks - readonly property list sources: nodes.sources - readonly property list streams: nodes.streams + property list sinks: [] + property list sources: [] + property list streams: [] readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode source: Pipewire.defaultAudioSource @@ -141,6 +124,31 @@ Singleton { previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); } + Connections { + target: Pipewire.nodes + + function onValuesChanged(): void { + const newSinks = []; + const newSources = []; + const newStreams = []; + + for (const node of Pipewire.nodes.values) { + if (!node.isStream) { + if (node.isSink) + newSinks.push(node); + else if (node.audio) + newSources.push(node); + } else if (node.audio) { + newStreams.push(node); + } + } + + root.sinks = newSinks; + root.sources = newSources; + root.streams = newStreams; + } + } + PwObjectTracker { objects: [...root.sinks, ...root.sources, ...root.streams] } From ca06d13d271d7a6ae6e1becb0f094c76dd790c56 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:08:43 +1100 Subject: [PATCH 036/409] appdb: cache favourite status during sort and avoid double sort --- plugin/src/Caelestia/appdb.cpp | 29 ++++++++++++++++++----------- 1 file changed, 18 insertions(+), 11 deletions(-) diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index b074cf481..6952c0e08 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -212,10 +212,9 @@ void AppDb::incrementFrequency(const QString& id) { auto* app = m_apps.value(id); if (app) { const auto before = getSortedApps(); - app->incrementFrequency(); - - if (before != getSortedApps()) { + getSortedApps(); + if (before != m_sortedApps) { emit appsChanged(); } } else { @@ -225,15 +224,22 @@ void AppDb::incrementFrequency(const QString& id) { QList& AppDb::getSortedApps() const { m_sortedApps = m_apps.values(); - std::sort(m_sortedApps.begin(), m_sortedApps.end(), [this](AppEntry* a, AppEntry* b) { - bool aIsFav = isFavourite(a); - bool bIsFav = isFavourite(b); - if (aIsFav != bIsFav) { + + // Pre-compute favourite status to avoid repeated regex matching during sort + QSet favSet; + favSet.reserve(m_sortedApps.size()); + for (const auto* app : std::as_const(m_sortedApps)) { + if (isFavourite(app)) + favSet.insert(app->id()); + } + + std::sort(m_sortedApps.begin(), m_sortedApps.end(), [&favSet](AppEntry* a, AppEntry* b) { + const bool aIsFav = favSet.contains(a->id()); + const bool bIsFav = favSet.contains(b->id()); + if (aIsFav != bIsFav) return aIsFav; - } - if (a->frequency() != b->frequency()) { + if (a->frequency() != b->frequency()) return a->frequency() > b->frequency(); - } return a->name().localeAwareCompare(b->name()) < 0; }); return m_sortedApps; @@ -269,7 +275,8 @@ void AppDb::updateAppFrequencies() { app->setFrequency(getFrequency(app->id())); } - if (before != getSortedApps()) { + getSortedApps(); + if (before != m_sortedApps) { emit appsChanged(); } } From 51579474c51bcf0daff0dd6fd56a256a8f6611c4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:13:20 +1100 Subject: [PATCH 037/409] workspaces: replace reduce with for loop for occupied map --- modules/bar/components/workspaces/Workspaces.qml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index bfa80ab68..b9fe87faf 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -16,10 +16,12 @@ StyledClippingRect { readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId - readonly property var occupied: Hypr.workspaces.values.reduce((acc, curr) => { - acc[curr.id] = curr.lastIpcObject.windows > 0; - return acc; - }, {}) + readonly property var occupied: { + const occ = {}; + for (const ws of Hypr.workspaces.values) + occ[ws.id] = ws.lastIpcObject.windows > 0; + return occ; + } readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown property real blur: onSpecial ? 1 : 0 From 314301157e6f2d9859cc51272b5f8a4f70cc05dd Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:21:05 +1100 Subject: [PATCH 038/409] notifs: single-pass derived properties in NotifGroup --- modules/lock/NotifGroup.qml | 27 ++++++++++++++++++++++++--- modules/sidebar/NotifGroup.qml | 34 ++++++++++++++++++++++++++++++---- 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 779609065..85c5ec448 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -17,9 +17,30 @@ StyledRect { required property string modelData readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) - readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" - readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" - readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" + readonly property var props: { + let img = ""; + let icon = ""; + let hasCritical = false; + let hasNormal = false; + for (const n of notifs) { + if (!img && n.image.length > 0) + img = n.image; + if (!icon && n.appIcon.length > 0) + icon = n.appIcon; + if (n.urgency === NotificationUrgency.Critical) + hasCritical = true; + else if (n.urgency === NotificationUrgency.Normal) + hasNormal = true; + } + return { + img, + icon, + urgency: hasCritical ? "critical" : hasNormal ? "normal" : "low" + }; + } + readonly property string image: props.img + readonly property string appIcon: props.icon + readonly property string urgency: props.urgency property bool expanded diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 16aac33ba..4e338da99 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -19,10 +19,36 @@ StyledRect { required property var visibilities readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) - readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) - readonly property string image: notifs.find(n => !n.closed && n.image.length > 0)?.image ?? "" - readonly property string appIcon: notifs.find(n => !n.closed && n.appIcon.length > 0)?.appIcon ?? "" - readonly property int urgency: notifs.some(n => !n.closed && n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low + readonly property var groupProps: { + let count = 0; + let img = ""; + let icon = ""; + let hasCritical = false; + let hasNormal = false; + for (const n of notifs) { + if (!n.closed) { + count++; + if (!img && n.image.length > 0) + img = n.image; + if (!icon && n.appIcon.length > 0) + icon = n.appIcon; + if (n.urgency === NotificationUrgency.Critical) + hasCritical = true; + else if (n.urgency === NotificationUrgency.Normal) + hasNormal = true; + } + } + return { + count, + img, + icon, + urgency: hasCritical ? NotificationUrgency.Critical : hasNormal ? NotificationUrgency.Normal : NotificationUrgency.Low + }; + } + readonly property int notifCount: groupProps.count + readonly property string image: groupProps.img + readonly property string appIcon: groupProps.icon + readonly property int urgency: groupProps.urgency readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0); From 8812073ab1f5ec370ec02d172addb2b3e70e51b8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:22:41 +1100 Subject: [PATCH 039/409] systemusage: combine chained replace calls into single regex --- services/SystemUsage.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index ce6201792..1b07454a6 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -45,11 +45,11 @@ Singleton { property int refCount function cleanCpuName(name: string): string { - return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/CPU/gi, "").replace(/\d+th Gen /gi, "").replace(/\d+nd Gen /gi, "").replace(/\d+rd Gen /gi, "").replace(/\d+st Gen /gi, "").replace(/Core /gi, "").replace(/Processor/gi, "").replace(/\s+/g, " ").trim(); + return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim(); } function cleanGpuName(name: string): string { - return name.replace(/\(R\)/gi, "").replace(/\(TM\)/gi, "").replace(/Graphics/gi, "").replace(/\s+/g, " ").trim(); + return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim(); } function formatKib(kib: real): var { From 11b30a0f696e3fce498f8d5fe1194b81746bcd83 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:22:45 +1100 Subject: [PATCH 040/409] strings: cache compiled RegExp objects --- utils/Strings.qml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/utils/Strings.qml b/utils/Strings.qml index 1d0cc766b..a91a0c082 100644 --- a/utils/Strings.qml +++ b/utils/Strings.qml @@ -3,12 +3,18 @@ pragma Singleton import Quickshell Singleton { + property var _regexCache: ({}) + function testRegexList(filterList: list, target: string): bool { const regexChecker = /^\^.*\$$/; for (const filter of filterList) { - // If filter is a regex if (regexChecker.test(filter)) { - if ((new RegExp(filter)).test(target)) + let re = _regexCache[filter]; + if (!re) { + re = new RegExp(filter); + _regexCache[filter] = re; + } + if (re.test(target)) return true; } else { if (filter === target) From ce7fe9609721d63ab37ff8ae18d94346b18e5f7f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:22:50 +1100 Subject: [PATCH 041/409] networkusage: avoid intermediate array copy for history updates --- services/NetworkUsage.qml | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 502ec3ae8..a9406480b 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -192,21 +192,13 @@ Singleton { const maxHistory = root.historyLength + 1; if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) { - let newDownHist = root._downloadHistory.slice(); - newDownHist.push(root._downloadSpeed); - if (newDownHist.length > maxHistory) { - newDownHist.shift(); - } - root._downloadHistory = newDownHist; + const dh = root._downloadHistory; + root._downloadHistory = dh.length >= maxHistory ? [...dh.slice(1), root._downloadSpeed] : [...dh, root._downloadSpeed]; } if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) { - let newUpHist = root._uploadHistory.slice(); - newUpHist.push(root._uploadSpeed); - if (newUpHist.length > maxHistory) { - newUpHist.shift(); - } - root._uploadHistory = newUpHist; + const uh = root._uploadHistory; + root._uploadHistory = uh.length >= maxHistory ? [...uh.slice(1), root._uploadSpeed] : [...uh, root._uploadSpeed]; } } From 5c4e1bc64d0957c2872cac9b66a0d766cea2dd7b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:23:04 +1100 Subject: [PATCH 042/409] filesystemmodel: use static QMimeDatabase instance --- plugin/src/Caelestia/Models/filesystemmodel.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp index e387ecd00..4eb94cd49 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.cpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp @@ -57,8 +57,8 @@ bool FileSystemEntry::isImage() const { QString FileSystemEntry::mimeType() const { if (!m_mimeTypeInitialised) { - const QMimeDatabase db; - m_mimeType = db.mimeTypeForFile(m_path).name(); + static const QMimeDatabase s_db; + m_mimeType = s_db.mimeTypeForFile(m_path).name(); m_mimeTypeInitialised = true; } return m_mimeType; From 4b18073e5cd90a140eb665de0020121b3b1cec36 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:25:50 +1100 Subject: [PATCH 043/409] circularindicator: guard signal emissions with change checks --- .../Caelestia/Internal/circularindicatormanager.cpp | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp index 434b75620..212499792 100644 --- a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp +++ b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp @@ -153,8 +153,10 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) * SPIN_ROTATION_DEGREES; } + const auto oldRotation = m_rotation; m_rotation = constantRotation + spinRotation; - emit rotationChanged(); + if (!qFuzzyCompare(m_rotation + 1.0, oldRotation + 1.0)) + emit rotationChanged(); // Grow active indicator. qreal fraction = @@ -182,6 +184,8 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { void CircularIndicatorManager::updateAdvance(qreal progress) { using namespace advance; const auto playtime = progress * TOTAL_DURATION_IN_MS; + const auto oldStart = m_startFraction; + const auto oldEnd = m_endFraction; // Adds constant rotation to segment positions. m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET; @@ -204,8 +208,10 @@ void CircularIndicatorManager::updateAdvance(qreal progress) { m_startFraction /= 360.0; m_endFraction /= 360.0; - emit startFractionChanged(); - emit endFractionChanged(); + if (!qFuzzyCompare(m_startFraction + 1.0, oldStart + 1.0)) + emit startFractionChanged(); + if (!qFuzzyCompare(m_endFraction + 1.0, oldEnd + 1.0)) + emit endFractionChanged(); } } // namespace caelestia::internal From c4239001f2049049fafbf71a486dd466e0ecc043 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:25:55 +1100 Subject: [PATCH 044/409] dashboard: gate network sparkline timers on visibility --- modules/dashboard/Performance.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index e73d8ed3d..306f9b81f 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -836,7 +836,7 @@ Item { Timer { interval: Config.dashboard.resourceUpdateInterval - running: true + running: networkCard.visible repeat: true onTriggered: sparklineCanvas._tickCount++ } @@ -846,7 +846,7 @@ Item { to: 1 duration: Config.dashboard.resourceUpdateInterval loops: Animation.Infinite - running: true + running: networkCard.visible } Behavior on smoothMax { From 229d17be35ff624c84af183a917e009e50d6e24b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:28:56 +1100 Subject: [PATCH 045/409] notifs: add sourceSize to notification images --- modules/lock/NotifGroup.qml | 2 ++ modules/notifications/Notification.qml | 2 ++ modules/sidebar/NotifGroup.qml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 85c5ec448..7fcb108ec 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -73,6 +73,8 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop + sourceSize.width: Config.notifs.sizes.image + sourceSize.height: Config.notifs.sizes.image cache: false asynchronous: true width: Config.notifs.sizes.image diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 8c2d3ec22..1e8f8feda 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -124,6 +124,8 @@ StyledRect { anchors.fill: parent source: Qt.resolvedUrl(root.modelData.image) fillMode: Image.PreserveAspectCrop + sourceSize.width: Config.notifs.sizes.image + sourceSize.height: Config.notifs.sizes.image cache: false asynchronous: true } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 4e338da99..2c032aa7d 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -100,6 +100,8 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop + sourceSize.width: Config.notifs.sizes.image + sourceSize.height: Config.notifs.sizes.image cache: false asynchronous: true width: Config.notifs.sizes.image From 6987809a7620d44577d072c5d9418a7f5bf1afe8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:30:28 +1100 Subject: [PATCH 046/409] brightness: use map lookup for DDC monitor matching --- services/Brightness.qml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/services/Brightness.qml b/services/Brightness.qml index 12920eedd..7eb7d51f7 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -11,6 +11,12 @@ Singleton { id: root property list ddcMonitors: [] + readonly property var ddcMonitorMap: { + const map = {}; + for (const m of ddcMonitors) + map[m.connector] = m; + return map; + } readonly property list monitors: variants.instances property bool appleDisplayPresent: false @@ -155,8 +161,9 @@ Singleton { id: monitor required property ShellScreen modelData - readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name) - readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" + readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null + readonly property bool isDdc: ddcInfo !== null + readonly property string busNum: ddcInfo?.busNum ?? "" readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") property real brightness property real queuedBrightness: NaN From fe5195ca2e51df5d8e26f46ef212a8b755ee854c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:33:15 +1100 Subject: [PATCH 047/409] hyprextras: avoid arg() overhead in applyOptions string building --- plugin/src/Caelestia/Internal/hyprextras.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Internal/hyprextras.cpp b/plugin/src/Caelestia/Internal/hyprextras.cpp index 5308524d9..ab18d2f42 100644 --- a/plugin/src/Caelestia/Internal/hyprextras.cpp +++ b/plugin/src/Caelestia/Internal/hyprextras.cpp @@ -84,9 +84,11 @@ void HyprExtras::applyOptions(const QVariantHash& options) { return; } - QString request = "[[BATCH]]"; + QString request; + request.reserve(12 + options.size() * 40); + request += QLatin1String("[[BATCH]]"); for (auto it = options.constBegin(); it != options.constEnd(); ++it) { - request += QString("keyword %1 %2;").arg(it.key(), it.value().toString()); + request += QLatin1String("keyword ") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';'); } makeRequest(request, [this](bool success, const QByteArray& res) { From 8e7ce4c6a64f53d262835af5f770e625e9bca9e7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:34:16 +1100 Subject: [PATCH 048/409] notifs: skip markdown parsing for plain text bodies --- modules/notifications/Notification.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 1e8f8feda..728cd9f7e 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -17,6 +17,7 @@ StyledRect { required property Notifs.Notif modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 + readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 property bool expanded: Config.notifs.openExpanded @@ -347,7 +348,7 @@ StyledRect { anchors.rightMargin: Appearance.spacing.small animate: true - textFormat: Text.MarkdownText + textFormat: root.bodyTextFormat text: bodyPreviewMetrics.elidedText color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small @@ -378,7 +379,7 @@ StyledRect { anchors.rightMargin: Appearance.spacing.small animate: true - textFormat: Text.MarkdownText + textFormat: root.bodyTextFormat text: root.modelData.body color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small From 27cb290423ba5c30c1856427609938c230fa9f32 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 12 Mar 2026 22:51:07 +1100 Subject: [PATCH 049/409] feat: replace canvas -> c++ component Also add c++ ring buffer --- modules/dashboard/Performance.qml | 192 +++------------- plugin/src/Caelestia/Internal/CMakeLists.txt | 3 + plugin/src/Caelestia/Internal/arcgauge.cpp | 119 ++++++++++ plugin/src/Caelestia/Internal/arcgauge.hpp | 61 +++++ .../src/Caelestia/Internal/circularbuffer.cpp | 94 ++++++++ .../src/Caelestia/Internal/circularbuffer.hpp | 44 ++++ .../src/Caelestia/Internal/sparklineitem.cpp | 212 ++++++++++++++++++ .../src/Caelestia/Internal/sparklineitem.hpp | 90 ++++++++ services/NetworkUsage.qml | 34 +-- 9 files changed, 668 insertions(+), 181 deletions(-) create mode 100644 plugin/src/Caelestia/Internal/arcgauge.cpp create mode 100644 plugin/src/Caelestia/Internal/arcgauge.hpp create mode 100644 plugin/src/Caelestia/Internal/circularbuffer.cpp create mode 100644 plugin/src/Caelestia/Internal/circularbuffer.hpp create mode 100644 plugin/src/Caelestia/Internal/sparklineitem.cpp create mode 100644 plugin/src/Caelestia/Internal/sparklineitem.hpp diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 306f9b81f..618d4e66e 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -2,6 +2,7 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.UPower +import Caelestia.Internal import qs.components import qs.components.misc import qs.config @@ -486,51 +487,15 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - Canvas { - id: gaugeCanvas - + ArcGauge { anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const cx = width / 2; - const cy = height / 2; - const radius = (Math.min(width, height) - 12) / 2; - const lineWidth = 10; - ctx.beginPath(); - ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); - ctx.stroke(); - if (gaugeCard.animatedPercentage > 0) { - ctx.beginPath(); - ctx.arc(cx, cy, radius, gaugeCard.arcStartAngle, gaugeCard.arcStartAngle + gaugeCard.arcSweep * gaugeCard.animatedPercentage); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = gaugeCard.accentColor; - ctx.stroke(); - } - } - Component.onCompleted: requestPaint() - - Connections { - function onAnimatedPercentageChanged() { - gaugeCanvas.requestPaint(); - } - - target: gaugeCard - } - - Connections { - function onPaletteChanged() { - gaugeCanvas.requestPaint(); - } - - target: Colours - } + percentage: gaugeCard.animatedPercentage + accentColor: gaugeCard.accentColor + trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + startAngle: gaugeCard.arcStartAngle + sweepAngle: gaugeCard.arcSweep } StyledText { @@ -642,51 +607,15 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - Canvas { - id: storageGaugeCanvas - + ArcGauge { anchors.centerIn: parent width: Math.min(parent.width, parent.height) height: width - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const cx = width / 2; - const cy = height / 2; - const radius = (Math.min(width, height) - 12) / 2; - const lineWidth = 10; - ctx.beginPath(); - ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); - ctx.stroke(); - if (storageGaugeCard.animatedPercentage > 0) { - ctx.beginPath(); - ctx.arc(cx, cy, radius, storageGaugeCard.arcStartAngle, storageGaugeCard.arcStartAngle + storageGaugeCard.arcSweep * storageGaugeCard.animatedPercentage); - ctx.lineWidth = lineWidth; - ctx.lineCap = "round"; - ctx.strokeStyle = storageGaugeCard.accentColor; - ctx.stroke(); - } - } - Component.onCompleted: requestPaint() - - Connections { - function onAnimatedPercentageChanged() { - storageGaugeCanvas.requestPaint(); - } - - target: storageGaugeCard - } - - Connections { - function onPaletteChanged() { - storageGaugeCanvas.requestPaint(); - } - - target: Colours - } + percentage: storageGaugeCard.animatedPercentage + accentColor: storageGaugeCard.accentColor + trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + startAngle: storageGaugeCard.arcStartAngle + sweepAngle: storageGaugeCard.arcSweep } StyledText { @@ -749,96 +678,27 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - Canvas { - id: sparklineCanvas + SparklineItem { + id: sparkline - property var downHistory: NetworkUsage.downloadHistory - property var upHistory: NetworkUsage.uploadHistory - property real targetMax: 1024 + property real targetMax: Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024) property real smoothMax: targetMax - property real slideProgress: 0 - property int _tickCount: 0 - property int _lastTickCount: -1 - - function checkAndAnimate(): void { - const currentLength = (downHistory || []).length; - if (currentLength > 0 && _tickCount !== _lastTickCount) { - _lastTickCount = _tickCount; - updateMax(); - } - } - - function updateMax(): void { - const downHist = downHistory || []; - const upHist = upHistory || []; - const allValues = downHist.concat(upHist); - targetMax = Math.max(...allValues, 1024); - requestPaint(); - } anchors.fill: parent - onDownHistoryChanged: checkAndAnimate() - onUpHistoryChanged: checkAndAnimate() - onSmoothMaxChanged: requestPaint() - onSlideProgressChanged: requestPaint() - - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); - const w = width; - const h = height; - const downHist = downHistory || []; - const upHist = upHistory || []; - if (downHist.length < 2 && upHist.length < 2) - return; - - const maxVal = smoothMax; - - const drawLine = (history, color, fillAlpha) => { - if (history.length < 2) - return; - - const len = history.length; - const stepX = w / (NetworkUsage.historyLength - 1); - const startX = w - (len - 1) * stepX - stepX * slideProgress + stepX; - ctx.beginPath(); - ctx.moveTo(startX, h - (history[0] / maxVal) * h); - for (let i = 1; i < len; i++) { - const x = startX + i * stepX; - const y = h - (history[i] / maxVal) * h; - ctx.lineTo(x, y); - } - ctx.strokeStyle = color; - ctx.lineWidth = 2; - ctx.lineCap = "round"; - ctx.lineJoin = "round"; - ctx.stroke(); - ctx.lineTo(startX + (len - 1) * stepX, h); - ctx.lineTo(startX, h); - ctx.closePath(); - ctx.fillStyle = Qt.rgba(Qt.color(color).r, Qt.color(color).g, Qt.color(color).b, fillAlpha); - ctx.fill(); - }; - - drawLine(upHist, Colours.palette.m3secondary.toString(), 0.15); - drawLine(downHist, Colours.palette.m3tertiary.toString(), 0.2); - } - - Component.onCompleted: updateMax() - - Connections { - function onPaletteChanged() { - sparklineCanvas.requestPaint(); - } - - target: Colours - } + line1: NetworkUsage.uploadBuffer + line1Color: Colours.palette.m3secondary + line1FillAlpha: 0.15 + line2: NetworkUsage.downloadBuffer + line2Color: Colours.palette.m3tertiary + line2FillAlpha: 0.2 + maxValue: smoothMax + historyLength: NetworkUsage.historyLength Timer { interval: Config.dashboard.resourceUpdateInterval running: networkCard.visible repeat: true - onTriggered: sparklineCanvas._tickCount++ + onTriggered: sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024) } NumberAnimation on slideProgress { @@ -862,7 +722,7 @@ Item { text: qsTr("Collecting data...") font.pointSize: Appearance.font.size.small color: Colours.palette.m3onSurfaceVariant - visible: NetworkUsage.downloadHistory.length < 2 + visible: NetworkUsage.downloadBuffer.count < 2 opacity: 0.6 } } diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index bdc58dbf7..85e85c856 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -1,11 +1,14 @@ qml_module(caelestia-internal URI Caelestia.Internal SOURCES + arcgauge.hpp arcgauge.cpp cachingimagemanager.hpp cachingimagemanager.cpp + circularbuffer.hpp circularbuffer.cpp circularindicatormanager.hpp circularindicatormanager.cpp hyprdevices.hpp hyprdevices.cpp hyprextras.hpp hyprextras.cpp logindmanager.hpp logindmanager.cpp + sparklineitem.hpp sparklineitem.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/plugin/src/Caelestia/Internal/arcgauge.cpp b/plugin/src/Caelestia/Internal/arcgauge.cpp new file mode 100644 index 000000000..d534f5f73 --- /dev/null +++ b/plugin/src/Caelestia/Internal/arcgauge.cpp @@ -0,0 +1,119 @@ +#include "arcgauge.hpp" + +#include +#include +#include + +namespace caelestia::internal { + +ArcGauge::ArcGauge(QQuickItem* parent) + : QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void ArcGauge::paint(QPainter* painter) { + const qreal w = width(); + const qreal h = height(); + const qreal side = qMin(w, h); + const qreal radius = (side - m_lineWidth - 2.0) / 2.0; + const qreal cx = w / 2.0; + const qreal cy = h / 2.0; + + const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0); + + // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees) + const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0); + const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0); + + painter->setRenderHint(QPainter::Antialiasing, true); + + // Draw track arc + QPen trackPen(m_trackColor, m_lineWidth); + trackPen.setCapStyle(Qt::RoundCap); + painter->setPen(trackPen); + painter->setBrush(Qt::NoBrush); + painter->drawArc(arcRect, startAngle16, sweepAngle16); + + // Draw value arc + if (m_percentage > 0.0) { + const int valueSweep16 = qRound(static_cast(sweepAngle16) * m_percentage); + QPen valuePen(m_accentColor, m_lineWidth); + valuePen.setCapStyle(Qt::RoundCap); + painter->setPen(valuePen); + painter->drawArc(arcRect, startAngle16, valueSweep16); + } +} + +qreal ArcGauge::percentage() const { + return m_percentage; +} + +void ArcGauge::setPercentage(qreal percentage) { + if (qFuzzyCompare(m_percentage, percentage)) + return; + m_percentage = percentage; + emit percentageChanged(); + update(); +} + +QColor ArcGauge::accentColor() const { + return m_accentColor; +} + +void ArcGauge::setAccentColor(const QColor& color) { + if (m_accentColor == color) + return; + m_accentColor = color; + emit accentColorChanged(); + update(); +} + +QColor ArcGauge::trackColor() const { + return m_trackColor; +} + +void ArcGauge::setTrackColor(const QColor& color) { + if (m_trackColor == color) + return; + m_trackColor = color; + emit trackColorChanged(); + update(); +} + +qreal ArcGauge::startAngle() const { + return m_startAngle; +} + +void ArcGauge::setStartAngle(qreal angle) { + if (qFuzzyCompare(m_startAngle, angle)) + return; + m_startAngle = angle; + emit startAngleChanged(); + update(); +} + +qreal ArcGauge::sweepAngle() const { + return m_sweepAngle; +} + +void ArcGauge::setSweepAngle(qreal angle) { + if (qFuzzyCompare(m_sweepAngle, angle)) + return; + m_sweepAngle = angle; + emit sweepAngleChanged(); + update(); +} + +qreal ArcGauge::lineWidth() const { + return m_lineWidth; +} + +void ArcGauge::setLineWidth(qreal width) { + if (qFuzzyCompare(m_lineWidth, width)) + return; + m_lineWidth = width; + emit lineWidthChanged(); + update(); +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/arcgauge.hpp b/plugin/src/Caelestia/Internal/arcgauge.hpp new file mode 100644 index 000000000..4ccb1fd01 --- /dev/null +++ b/plugin/src/Caelestia/Internal/arcgauge.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +namespace caelestia::internal { + +class ArcGauge : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged) + Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) + Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged) + Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged) + Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged) + Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) + +public: + explicit ArcGauge(QQuickItem* parent = nullptr); + + void paint(QPainter* painter) override; + + [[nodiscard]] qreal percentage() const; + void setPercentage(qreal percentage); + + [[nodiscard]] QColor accentColor() const; + void setAccentColor(const QColor& color); + + [[nodiscard]] QColor trackColor() const; + void setTrackColor(const QColor& color); + + [[nodiscard]] qreal startAngle() const; + void setStartAngle(qreal angle); + + [[nodiscard]] qreal sweepAngle() const; + void setSweepAngle(qreal angle); + + [[nodiscard]] qreal lineWidth() const; + void setLineWidth(qreal width); + +signals: + void percentageChanged(); + void accentColorChanged(); + void trackColorChanged(); + void startAngleChanged(); + void sweepAngleChanged(); + void lineWidthChanged(); + +private: + qreal m_percentage = 0.0; + QColor m_accentColor; + QColor m_trackColor; + qreal m_startAngle = 0.75 * M_PI; + qreal m_sweepAngle = 1.5 * M_PI; + qreal m_lineWidth = 10.0; +}; + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/circularbuffer.cpp b/plugin/src/Caelestia/Internal/circularbuffer.cpp new file mode 100644 index 000000000..9701e7fac --- /dev/null +++ b/plugin/src/Caelestia/Internal/circularbuffer.cpp @@ -0,0 +1,94 @@ +#include "circularbuffer.hpp" + +#include + +namespace caelestia::internal { + +CircularBuffer::CircularBuffer(QObject* parent) + : QObject(parent) {} + +int CircularBuffer::capacity() const { + return m_capacity; +} + +void CircularBuffer::setCapacity(int capacity) { + if (capacity < 0) + capacity = 0; + if (m_capacity == capacity) + return; + + const auto old = values(); + + m_capacity = capacity; + m_data.resize(capacity); + m_data.fill(0.0); + m_head = 0; + m_count = 0; + + // Re-push old values, keeping the most recent ones + const auto start = old.size() > capacity ? old.size() - capacity : 0; + for (auto i = start; i < old.size(); ++i) { + m_data[m_head] = old[i]; + m_head = (m_head + 1) % m_capacity; + m_count++; + } + + emit capacityChanged(); + emit countChanged(); + emit valuesChanged(); +} + +int CircularBuffer::count() const { + return m_count; +} + +QList CircularBuffer::values() const { + QList result; + result.reserve(m_count); + for (int i = 0; i < m_count; ++i) + result.append(at(i)); + return result; +} + +qreal CircularBuffer::maximum() const { + if (m_count == 0) + return 0.0; + + qreal maxVal = at(0); + for (int i = 1; i < m_count; ++i) + maxVal = std::max(maxVal, at(i)); + return maxVal; +} + +void CircularBuffer::push(qreal value) { + if (m_capacity <= 0) + return; + + m_data[m_head] = value; + m_head = (m_head + 1) % m_capacity; + if (m_count < m_capacity) { + m_count++; + emit countChanged(); + } + emit valuesChanged(); +} + +void CircularBuffer::clear() { + if (m_count == 0) + return; + + m_head = 0; + m_count = 0; + emit countChanged(); + emit valuesChanged(); +} + +qreal CircularBuffer::at(int index) const { + if (index < 0 || index >= m_count) + return 0.0; + + const int actualIndex = (m_head - m_count + index + m_capacity) % m_capacity; + return m_data[actualIndex]; +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/circularbuffer.hpp b/plugin/src/Caelestia/Internal/circularbuffer.hpp new file mode 100644 index 000000000..ab2dba56e --- /dev/null +++ b/plugin/src/Caelestia/Internal/circularbuffer.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::internal { + +class CircularBuffer : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(int capacity READ capacity WRITE setCapacity NOTIFY capacityChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(QList values READ values NOTIFY valuesChanged) + Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged) + +public: + explicit CircularBuffer(QObject* parent = nullptr); + + [[nodiscard]] int capacity() const; + void setCapacity(int capacity); + + [[nodiscard]] int count() const; + [[nodiscard]] QList values() const; + [[nodiscard]] qreal maximum() const; + + Q_INVOKABLE void push(qreal value); + Q_INVOKABLE void clear(); + Q_INVOKABLE [[nodiscard]] qreal at(int index) const; + +signals: + void capacityChanged(); + void countChanged(); + void valuesChanged(); + +private: + QVector m_data; + int m_head = 0; + int m_count = 0; + int m_capacity = 0; +}; + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp new file mode 100644 index 000000000..b4938d1d2 --- /dev/null +++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp @@ -0,0 +1,212 @@ +#include "sparklineitem.hpp" + +#include +#include +#include + +namespace caelestia::internal { + +SparklineItem::SparklineItem(QQuickItem* parent) + : QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void SparklineItem::paint(QPainter* painter) { + const bool has1 = m_line1 && m_line1->count() >= 2; + const bool has2 = m_line2 && m_line2->count() >= 2; + if (!has1 && !has2) + return; + + painter->setRenderHint(QPainter::Antialiasing, true); + + // Draw line1 first (behind), then line2 (in front) + if (has1) + drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha); + if (has2) + drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha); +} + +void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { + const qreal w = width(); + const qreal h = height(); + const int len = buffer->count(); + const qreal stepX = w / static_cast(m_historyLength - 1); + const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX; + + // Build line path + QPainterPath linePath; + linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h); + for (int i = 1; i < len; ++i) { + const qreal x = startX + i * stepX; + const qreal y = h - (buffer->at(i) / m_maxValue) * h; + linePath.lineTo(x, y); + } + + // Stroke the line + QPen pen(color, m_lineWidth); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(linePath); + + // Fill under the line + QPainterPath fillPath = linePath; + fillPath.lineTo(startX + (len - 1) * stepX, h); + fillPath.lineTo(startX, h); + fillPath.closeSubpath(); + + QColor fillColor = color; + fillColor.setAlphaF(static_cast(fillAlpha)); + painter->setPen(Qt::NoPen); + painter->setBrush(fillColor); + painter->drawPath(fillPath); +} + +void SparklineItem::connectBuffer(CircularBuffer* buffer) { + if (!buffer) + return; + + connect(buffer, &CircularBuffer::valuesChanged, this, [this]() { + update(); + }); + connect(buffer, &QObject::destroyed, this, [this, buffer]() { + if (m_line1 == buffer) { + m_line1 = nullptr; + emit line1Changed(); + } + if (m_line2 == buffer) { + m_line2 = nullptr; + emit line2Changed(); + } + update(); + }); +} + +CircularBuffer* SparklineItem::line1() const { + return m_line1; +} + +void SparklineItem::setLine1(CircularBuffer* buffer) { + if (m_line1 == buffer) + return; + if (m_line1) + disconnect(m_line1, nullptr, this, nullptr); + m_line1 = buffer; + connectBuffer(buffer); + emit line1Changed(); + update(); +} + +CircularBuffer* SparklineItem::line2() const { + return m_line2; +} + +void SparklineItem::setLine2(CircularBuffer* buffer) { + if (m_line2 == buffer) + return; + if (m_line2) + disconnect(m_line2, nullptr, this, nullptr); + m_line2 = buffer; + connectBuffer(buffer); + emit line2Changed(); + update(); +} + +QColor SparklineItem::line1Color() const { + return m_line1Color; +} + +void SparklineItem::setLine1Color(const QColor& color) { + if (m_line1Color == color) + return; + m_line1Color = color; + emit line1ColorChanged(); + update(); +} + +QColor SparklineItem::line2Color() const { + return m_line2Color; +} + +void SparklineItem::setLine2Color(const QColor& color) { + if (m_line2Color == color) + return; + m_line2Color = color; + emit line2ColorChanged(); + update(); +} + +qreal SparklineItem::line1FillAlpha() const { + return m_line1FillAlpha; +} + +void SparklineItem::setLine1FillAlpha(qreal alpha) { + if (qFuzzyCompare(m_line1FillAlpha, alpha)) + return; + m_line1FillAlpha = alpha; + emit line1FillAlphaChanged(); + update(); +} + +qreal SparklineItem::line2FillAlpha() const { + return m_line2FillAlpha; +} + +void SparklineItem::setLine2FillAlpha(qreal alpha) { + if (qFuzzyCompare(m_line2FillAlpha, alpha)) + return; + m_line2FillAlpha = alpha; + emit line2FillAlphaChanged(); + update(); +} + +qreal SparklineItem::maxValue() const { + return m_maxValue; +} + +void SparklineItem::setMaxValue(qreal value) { + if (qFuzzyCompare(m_maxValue, value)) + return; + m_maxValue = value; + emit maxValueChanged(); + update(); +} + +qreal SparklineItem::slideProgress() const { + return m_slideProgress; +} + +void SparklineItem::setSlideProgress(qreal progress) { + if (qFuzzyCompare(m_slideProgress, progress)) + return; + m_slideProgress = progress; + emit slideProgressChanged(); + update(); +} + +int SparklineItem::historyLength() const { + return m_historyLength; +} + +void SparklineItem::setHistoryLength(int length) { + if (m_historyLength == length) + return; + m_historyLength = length; + emit historyLengthChanged(); + update(); +} + +qreal SparklineItem::lineWidth() const { + return m_lineWidth; +} + +void SparklineItem::setLineWidth(qreal width) { + if (qFuzzyCompare(m_lineWidth, width)) + return; + m_lineWidth = width; + emit lineWidthChanged(); + update(); +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/sparklineitem.hpp b/plugin/src/Caelestia/Internal/sparklineitem.hpp new file mode 100644 index 000000000..945a1b34f --- /dev/null +++ b/plugin/src/Caelestia/Internal/sparklineitem.hpp @@ -0,0 +1,90 @@ +#pragma once + +#include +#include +#include +#include + +#include "circularbuffer.hpp" + +namespace caelestia::internal { + +class SparklineItem : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed) + Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed) + Q_PROPERTY(QColor line1Color READ line1Color WRITE setLine1Color NOTIFY line1ColorChanged) + Q_PROPERTY(QColor line2Color READ line2Color WRITE setLine2Color NOTIFY line2ColorChanged) + Q_PROPERTY(qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY line1FillAlphaChanged) + Q_PROPERTY(qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY line2FillAlphaChanged) + Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) + Q_PROPERTY(qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY slideProgressChanged) + Q_PROPERTY(int historyLength READ historyLength WRITE setHistoryLength NOTIFY historyLengthChanged) + Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) + +public: + explicit SparklineItem(QQuickItem* parent = nullptr); + + void paint(QPainter* painter) override; + + [[nodiscard]] CircularBuffer* line1() const; + void setLine1(CircularBuffer* buffer); + + [[nodiscard]] CircularBuffer* line2() const; + void setLine2(CircularBuffer* buffer); + + [[nodiscard]] QColor line1Color() const; + void setLine1Color(const QColor& color); + + [[nodiscard]] QColor line2Color() const; + void setLine2Color(const QColor& color); + + [[nodiscard]] qreal line1FillAlpha() const; + void setLine1FillAlpha(qreal alpha); + + [[nodiscard]] qreal line2FillAlpha() const; + void setLine2FillAlpha(qreal alpha); + + [[nodiscard]] qreal maxValue() const; + void setMaxValue(qreal value); + + [[nodiscard]] qreal slideProgress() const; + void setSlideProgress(qreal progress); + + [[nodiscard]] int historyLength() const; + void setHistoryLength(int length); + + [[nodiscard]] qreal lineWidth() const; + void setLineWidth(qreal width); + +signals: + void line1Changed(); + void line2Changed(); + void line1ColorChanged(); + void line2ColorChanged(); + void line1FillAlphaChanged(); + void line2FillAlphaChanged(); + void maxValueChanged(); + void slideProgressChanged(); + void historyLengthChanged(); + void lineWidthChanged(); + +private: + void drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha); + void connectBuffer(CircularBuffer* buffer); + + CircularBuffer* m_line1 = nullptr; + CircularBuffer* m_line2 = nullptr; + QColor m_line1Color; + QColor m_line2Color; + qreal m_line1FillAlpha = 0.15; + qreal m_line2FillAlpha = 0.2; + qreal m_maxValue = 1024.0; + qreal m_slideProgress = 0.0; + int m_historyLength = 30; + qreal m_lineWidth = 2.0; +}; + +} // namespace caelestia::internal diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index a9406480b..451864710 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -5,6 +5,8 @@ import qs.config import Quickshell import Quickshell.Io +import Caelestia.Internal + import QtQuick Singleton { @@ -20,9 +22,9 @@ Singleton { readonly property real downloadTotal: _downloadTotal readonly property real uploadTotal: _uploadTotal - // History of speeds for sparkline (most recent at end) - readonly property var downloadHistory: _downloadHistory - readonly property var uploadHistory: _uploadHistory + // History buffers for sparkline + readonly property CircularBuffer downloadBuffer: _downloadBuffer + readonly property CircularBuffer uploadBuffer: _uploadBuffer readonly property int historyLength: 30 // Private properties @@ -30,8 +32,6 @@ Singleton { property real _uploadSpeed: 0 property real _downloadTotal: 0 property real _uploadTotal: 0 - property var _downloadHistory: [] - property var _uploadHistory: [] // Previous readings for calculating speed property real _prevRxBytes: 0 @@ -139,6 +139,16 @@ Singleton { }; } + CircularBuffer { + id: _downloadBuffer + capacity: root.historyLength + 1 + } + + CircularBuffer { + id: _uploadBuffer + capacity: root.historyLength + 1 + } + FileView { id: netDevFile path: "/proc/net/dev" @@ -189,17 +199,11 @@ Singleton { root._downloadSpeed = rxDelta / timeDelta; root._uploadSpeed = txDelta / timeDelta; - const maxHistory = root.historyLength + 1; - - if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) { - const dh = root._downloadHistory; - root._downloadHistory = dh.length >= maxHistory ? [...dh.slice(1), root._downloadSpeed] : [...dh, root._downloadSpeed]; - } + if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) + _downloadBuffer.push(root._downloadSpeed); - if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) { - const uh = root._uploadHistory; - root._uploadHistory = uh.length >= maxHistory ? [...uh.slice(1), root._uploadSpeed] : [...uh, root._uploadSpeed]; - } + if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) + _uploadBuffer.push(root._uploadSpeed); } // Calculate totals with overflow handling From c1795d187a3561d805514809c9a434956a1a9118 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 13 Mar 2026 00:23:04 +1100 Subject: [PATCH 050/409] feat: add excluded screens config opt Completely disables everything (except lock) for screen --- config/Config.qml | 1 + config/GeneralConfig.qml | 1 + modules/areapicker/AreaPicker.qml | 3 ++- modules/background/Background.qml | 2 +- modules/drawers/Drawers.qml | 2 +- services/Brightness.qml | 2 +- services/Screens.qml | 20 ++++++++++++++++++++ 7 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 services/Screens.qml diff --git a/config/Config.qml b/config/Config.qml index fe0728657..d6f5d198f 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -139,6 +139,7 @@ Singleton { function serializeGeneral(): var { return { logo: general.logo, + excludedScreens: general.excludedScreens, apps: { terminal: general.apps.terminal, audio: general.apps.audio, diff --git a/config/GeneralConfig.qml b/config/GeneralConfig.qml index 52ef0de3e..0d1d1e191 100644 --- a/config/GeneralConfig.qml +++ b/config/GeneralConfig.qml @@ -2,6 +2,7 @@ import Quickshell.Io JsonObject { property string logo: "" + property list excludedScreens: [] property Apps apps: Apps {} property Idle idle: Idle {} property Battery battery: Battery {} diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml index 0d8b2fe13..308b7d232 100644 --- a/modules/areapicker/AreaPicker.qml +++ b/modules/areapicker/AreaPicker.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import qs.components.containers import qs.components.misc +import qs.services import Quickshell import Quickshell.Wayland import Quickshell.Io @@ -15,7 +16,7 @@ Scope { property bool clipboardOnly Variants { - model: Quickshell.screens + model: Screens.screens StyledWindow { id: win diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 682da624c..c1f149a00 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -12,7 +12,7 @@ Loader { active: Config.background.enabled sourceComponent: Variants { - model: Quickshell.screens + model: Screens.screens StyledWindow { id: win diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 93534ec21..86c9e99d2 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -13,7 +13,7 @@ import QtQuick import QtQuick.Effects Variants { - model: Quickshell.screens + model: Screens.screens Scope { id: scope diff --git a/services/Brightness.qml b/services/Brightness.qml index 7eb7d51f7..567824042 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -67,7 +67,7 @@ Singleton { Variants { id: variants - model: Quickshell.screens + model: Quickshell.screens // Don't respect excluded screens cause ipc Monitor {} } diff --git a/services/Screens.qml b/services/Screens.qml new file mode 100644 index 000000000..a64751785 --- /dev/null +++ b/services/Screens.qml @@ -0,0 +1,20 @@ +pragma Singleton + +import qs.config +import qs.utils +import Quickshell + +Singleton { + id: root + + readonly property list screens: { + const excluded = Config.general.excludedScreens; + if (excluded.length === 0) + return Quickshell.screens; + return Quickshell.screens.filter(s => !Strings.testRegexList(excluded, s.name)); + } + + function isExcluded(screen: ShellScreen): bool { + return Strings.testRegexList(Config.general.excludedScreens, screen.name); + } +} From d97ba8a9c08bc931261766b1070bce610e7cb22e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 13 Mar 2026 02:14:52 +1100 Subject: [PATCH 051/409] fix: performance network usage jumping around --- modules/dashboard/Performance.qml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 618d4e66e..339c731fc 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -681,7 +681,7 @@ Item { SparklineItem { id: sparkline - property real targetMax: Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024) + property real targetMax: 1024 property real smoothMax: targetMax anchors.fill: parent @@ -694,19 +694,23 @@ Item { maxValue: smoothMax historyLength: NetworkUsage.historyLength - Timer { - interval: Config.dashboard.resourceUpdateInterval - running: networkCard.visible - repeat: true - onTriggered: sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024) + Connections { + target: NetworkUsage.downloadBuffer + + function onValuesChanged(): void { + sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); + slideAnim.restart(); + } } - NumberAnimation on slideProgress { + NumberAnimation { + id: slideAnim + + target: sparkline + property: "slideProgress" from: 0 to: 1 duration: Config.dashboard.resourceUpdateInterval - loops: Animation.Infinite - running: networkCard.visible } Behavior on smoothMax { From 12b07b7cdfa9de0e7f78b7168fc2cee358002167 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Sun, 15 Mar 2026 07:19:44 +0100 Subject: [PATCH 052/409] dash: use currentIndex for width, restore binding (#1286) * [CI] chore: update flake * Dashboard perf settings save, visibility on none enabled * Dashboard heigh stutter fixed, persist current tab * restore binding * wrapper async=false * ScriptModel, centralized tabs/panes, individual toggle * fixes, missed mediaUpdateInterval, passing values * add binding back, ensure active tab uses bar.currentIndex --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> --- modules/dashboard/Tabs.qml | 13 +++++++++---- modules/dashboard/Wrapper.qml | 1 + 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index ed4613d9a..6e09e767a 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -52,15 +52,20 @@ Item { anchors.top: bar.bottom anchors.topMargin: 5 - implicitWidth: bar.currentItem?.implicitWidth ?? 0 + implicitWidth: { + const tab = bar.currentItem; + if (tab) + return tab.implicitWidth; + const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; + return width; + } implicitHeight: 3 x: { const tab = bar.currentItem; - if (!tab) - return 0; const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; - return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; + const tabWidth = tab?.implicitWidth ?? width; + return width * bar.currentIndex + (width - tabWidth) / 2; } clip: true diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 81bfcd328..0e37909e8 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -82,6 +82,7 @@ Item { running: true interval: Appearance.anim.durations.extraLarge onTriggered: { + content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); content.visible = true; } } From 1508abd3e13fe30bd90bc11d80b52aa8d8d4596e Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Sun, 15 Mar 2026 07:34:52 +0100 Subject: [PATCH 053/409] workspaces: window icons limit (#1267) * workspace window icons display limit * serialization --- config/BarConfig.qml | 1 + config/Config.qml | 1 + .../workspaces/SpecialWorkspaces.qml | 6 ++- .../bar/components/workspaces/Workspace.qml | 6 ++- modules/controlcenter/taskbar/TaskbarPane.qml | 37 +++++++++++++++++++ 5 files changed, 49 insertions(+), 2 deletions(-) diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 36a5f784b..62d6b178d 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -71,6 +71,7 @@ JsonObject { property bool occupiedBg: false property bool showWindows: true property bool showWindowsOnSpecialWorkspaces: showWindows + property int maxWindowIcons: 0 // 0 = unlimited property bool activeTrail: false property bool perMonitorWorkspaces: true property string label: " " // if empty, will show workspace name's first letter diff --git a/config/Config.qml b/config/Config.qml index d6f5d198f..eaeafb7be 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -209,6 +209,7 @@ Singleton { occupiedBg: bar.workspaces.occupiedBg, showWindows: bar.workspaces.showWindows, showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, + maxWindowIcons: bar.workspaces.maxWindowIcons, activeTrail: bar.workspaces.activeTrail, perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, label: bar.workspaces.label, diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index ad85af89e..cd3572be2 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -224,7 +224,11 @@ Item { Repeater { model: ScriptModel { - values: Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId) + values: { + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); + const maxIcons = Config.bar.workspaces.maxWindowIcons; + return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; + } } MaterialIcon { diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index 3c8238b57..f6e767ef9 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -87,7 +87,11 @@ ColumnLayout { Repeater { model: ScriptModel { - values: Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws) + values: { + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws); + const maxIcons = Config.bar.workspaces.maxWindowIcons; + return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; + } } MaterialIcon { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index d12d17449..9c999e5ba 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -38,6 +38,7 @@ Item { property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false + property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true property bool scrollVolume: Config.bar.scrollActions.volume ?? true @@ -81,6 +82,7 @@ Item { Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; Config.bar.workspaces.showWindows = root.workspacesShowWindows; + Config.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; Config.bar.scrollActions.workspaces = root.scrollWorkspaces; Config.bar.scrollActions.volume = root.scrollVolume; @@ -402,6 +404,41 @@ Item { } } + StyledRect { + Layout.fillWidth: true + implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Appearance.padding.large * 2 + radius: Appearance.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { + Anim {} + } + + RowLayout { + id: workspacesMaxWindowIconsRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + StyledText { + Layout.fillWidth: true + text: qsTr("Max window icons") + } + + CustomSpinBox { + min: 0 + max: 20 + value: root.workspacesMaxWindowIcons + onValueModified: value => { + root.workspacesMaxWindowIcons = value; + root.saveConfig(); + } + } + } + } + StyledRect { Layout.fillWidth: true implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 From bffe29e6b738dff3d7a669b065ff6295cbb2b4d8 Mon Sep 17 00:00:00 2001 From: Evertiro Date: Sun, 15 Mar 2026 01:35:25 -0500 Subject: [PATCH 054/409] fix: missing serialization for hiddenIcons (#1263) Signed-off-by: Dan Griffiths --- config/Config.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/Config.qml b/config/Config.qml index eaeafb7be..059ccce5e 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -222,7 +222,8 @@ Singleton { background: bar.tray.background, recolour: bar.tray.recolour, compact: bar.tray.compact, - iconSubs: bar.tray.iconSubs + iconSubs: bar.tray.iconSubs, + hiddenIcons: bar.tray.hiddenIcons }, status: { showAudio: bar.status.showAudio, From dd2d7dceabc25fc667db59dec8fe0dd253b15ad1 Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Sun, 15 Mar 2026 14:49:37 +0800 Subject: [PATCH 055/409] systemusage: optimized storage aggregation and improved device filtering (#1261) * refactor: Optimized storage aggregation + The storage aggregation logic doesn't account more complex storage setups and relied too much on risky string parsing to guess where partitions are. For example, in my case, I had a LUKS-encrypted drive which lives inside a "crypt," because it couldn't match the type (it only matched "disk" and "part"), it did not include my entire drive at all. Also, Linux devices names aren't always predictable (take mapper devices or complex NVMe paths), so if the RegEx doesn't match the name of those devices, the data just dissapears. I decided to go for a JSON approach making the code shorter and safer. Everything should work about the same. * systemusage: More intuitive filtering for storage devices + Removes "useless" drives from being show on the storage dashboard + Prioritizes the root disk to be shown first * refactor: formatted code properly --- services/SystemUsage.qml | 139 ++++++++++++++++++--------------------- 1 file changed, 63 insertions(+), 76 deletions(-) diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 1b07454a6..508564461 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -138,91 +138,78 @@ Singleton { Process { id: storage - // Get physical disks with aggregated usage from their partitions - // lsblk outputs: NAME SIZE TYPE FSUSED FSSIZE in bytes - command: ["lsblk", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE", "-P"] + // -J triggers JSON output. -b triggers bytes. + command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] + stdout: StdioCollector { onStreamFinished: { - const diskMap = {}; // Map disk name -> { name, totalSize, used, fsTotal } - const lines = text.trim().split("\n"); - - for (const line of lines) { - if (line.trim() === "") - continue; - - // Parse KEY="VALUE" format - const nameMatch = line.match(/NAME="([^"]+)"/); - const sizeMatch = line.match(/SIZE="([^"]+)"/); - const typeMatch = line.match(/TYPE="([^"]+)"/); - const fsusedMatch = line.match(/FSUSED="([^"]*)"/); - const fssizeMatch = line.match(/FSSIZE="([^"]*)"/); - - if (!nameMatch || !typeMatch) - continue; - - const name = nameMatch[1]; - const type = typeMatch[1]; - const size = parseInt(sizeMatch?.[1] || "0", 10); - const fsused = parseInt(fsusedMatch?.[1] || "0", 10); - const fssize = parseInt(fssizeMatch?.[1] || "0", 10); - - if (type === "disk") { - // Skip zram (swap) devices - if (name.startsWith("zram")) - continue; + const data = JSON.parse(text); + const diskList = []; + const seenDevices = new Set(); + + // Helper to recursively sum usage from children (partitions, crypt, lvm) + const aggregateUsage = dev => { + let used = 0; + let size = 0; + let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/")); + + if (!seenDevices.has(dev.name)) { + // lsblk returns null for empty/unformatted partitions, which parses to 0 here + used = parseInt(dev.fsused) || 0; + size = parseInt(dev.fssize) || 0; + seenDevices.add(dev.name); + } - // Initialize disk entry - if (!diskMap[name]) { - diskMap[name] = { - name: name, - totalSize: size, - used: 0, - fsTotal: 0 - }; - } - } else if (type === "part") { - // Find parent disk (remove trailing numbers/p+numbers) - let parentDisk = name.replace(/p?\d+$/, ""); - // For nvme devices like nvme0n1p1, parent is nvme0n1 - if (name.match(/nvme\d+n\d+p\d+/)) - parentDisk = name.replace(/p\d+$/, ""); - - // Aggregate partition usage to parent disk - if (diskMap[parentDisk]) { - diskMap[parentDisk].used += fsused; - diskMap[parentDisk].fsTotal += fssize; + if (dev.children) { + for (const child of dev.children) { + const stats = aggregateUsage(child); + used += stats.used; + size += stats.size; + if (stats.isRoot) + isRoot = true; } } - } + return { + used, + size, + isRoot + }; + }; + + for (const dev of data.blockdevices) { + // Only process physical disks at the top level + if (dev.type === "disk" && !dev.name.startsWith("zram")) { + const stats = aggregateUsage(dev); + + if (stats.size === 0) { + continue; + } - // Convert map to sorted array - const diskList = []; - let totalUsed = 0; - let totalSize = 0; - - for (const diskName of Object.keys(diskMap).sort()) { - const disk = diskMap[diskName]; - // Use filesystem total if available, otherwise use disk size - const total = disk.fsTotal > 0 ? disk.fsTotal : disk.totalSize; - const used = disk.used; - const perc = total > 0 ? used / total : 0; - - // Convert bytes to KiB for consistency with formatKib - diskList.push({ - mount: disk.name // Using 'mount' property for compatibility - , - used: used / 1024, - total: total / 1024, - free: (total - used) / 1024, - perc: perc - }); - - totalUsed += used; - totalSize += total; + const total = stats.size; + const used = stats.used; + + diskList.push({ + mount: dev.name, + used: used / 1024 // KiB + , + total: total / 1024 // KiB + , + free: (total - used) / 1024, + perc: total > 0 ? used / total : 0, + hasRoot: stats.isRoot + }); + } } - root.disks = diskList; + // Sort by putting the disk with root first, then sort the rest alphabetically + root.disks = diskList.sort((a, b) => { + if (a.hasRoot && !b.hasRoot) + return -1; + if (!a.hasRoot && b.hasRoot) + return 1; + return a.mount.localeCompare(b.mount); + }); } } } From 533b6590cddcea3e022a0547a3be248a691d7fee Mon Sep 17 00:00:00 2001 From: cordlessblues <75154048+cordlessblues@users.noreply.github.com> Date: Sun, 15 Mar 2026 02:27:46 -0500 Subject: [PATCH 056/409] notifs: support int:value hint (#1254) * add .vscode/settings.json to gitignore * added support for the Int:value hint * fix * more fix * f * comment --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- modules/notifications/Notification.qml | 37 ++++++++++++++++++++++++++ services/Notifs.qml | 6 +++++ 2 files changed, 43 insertions(+) diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 728cd9f7e..c8efa8d78 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -10,6 +10,7 @@ import Quickshell.Widgets import Quickshell.Services.Notifications import QtQuick import QtQuick.Layouts +import QtQuick.Shapes StyledRect { id: root @@ -183,6 +184,42 @@ StyledRect { } } + Shape { + id: progressIndicator + + anchors.centerIn: appIcon + width: appIcon.implicitWidth + progressShape.strokeWidth * 2 + height: appIcon.implicitHeight + progressShape.strokeWidth * 2 + preferredRendererType: Shape.CurveRenderer + + ShapePath { + id: progressShape + + capStyle: ShapePath.RoundCap + fillColor: "transparent" + strokeWidth: 2 + strokeColor: Colours.palette.m3primary + + PathAngleArc { + id: progressArc + + radiusX: progressIndicator.width / 2 - Appearance.padding.small / 2 + centerX: progressIndicator.width / 2 + radiusY: progressIndicator.height / 2 - Appearance.padding.small / 2 + centerY: progressIndicator.height / 2 + + startAngle: -90 + sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360 + + Behavior on sweepAngle { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + } + } + } + } + } + StyledText { id: appName diff --git a/services/Notifs.qml b/services/Notifs.qml index aa440feb1..aff2dfc6a 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -186,6 +186,7 @@ Singleton { property string appIcon property string appName property string image + property var hints // Hints are not persisted across restarts property real expireTimeout: Config.notifs.defaultExpireTimeout property int urgency: NotificationUrgency.Normal property bool resident @@ -301,6 +302,10 @@ Singleton { invoke: () => a.invoke() })); } + + function onHintsChanged(): void { + notif.hints = notif.notification.hints; + } } function lock(item: Item): void { @@ -335,6 +340,7 @@ Singleton { if (notification?.image) dummyImageLoader.active = true; expireTimeout = notification.expireTimeout; + hints = notification.hints; urgency = notification.urgency; resident = notification.resident; hasActionIcons = notification.hasActionIcons; From eed6adb0e49451872a3f8542956d6cb731c61ccb Mon Sep 17 00:00:00 2001 From: Xavier Lhinares <60365026+XLhinares@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:28:56 +0800 Subject: [PATCH 057/409] media: hide person icon if user pfp is ready (#1213) --- modules/dashboard/dash/User.qml | 1 + modules/lock/Center.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index b66b1f9a4..5ede24bb5 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -32,6 +32,7 @@ Row { fill: 1 grade: 200 font.pointSize: Math.floor(info.implicitHeight / 2) || 1 + visible: pfp.status !== Image.Ready } CachingImage { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 19cf9d290..aa926acd1 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -97,6 +97,7 @@ ColumnLayout { text: "person" color: Colours.palette.m3onSurfaceVariant font.pointSize: Math.floor(root.centerWidth / 4) + visible: pfp.status !== Image.Ready } CachingImage { From e3048464218d065aac3d5a2f3a8c307968555118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=A5=88=E8=A6=8B=20=E3=83=AC=E3=82=A4?= Date: Sun, 15 Mar 2026 13:02:32 +0530 Subject: [PATCH 058/409] config: add option to hide notifications on lockscreen (#1211) --- README.md | 3 ++- config/Config.qml | 1 + config/LockConfig.qml | 1 + modules/lock/NotifDock.qml | 6 +++--- 4 files changed, 7 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 886f0611c..651881559 100644 --- a/README.md +++ b/README.md @@ -567,7 +567,8 @@ default, you must create it manually. "hiddenApps": [] }, "lock": { - "recolourLogo": false + "recolourLogo": false, + "hideNotifs": false }, "notifs": { "actionOnClick": false, diff --git a/config/Config.qml b/config/Config.qml index 059ccce5e..c5cc1096a 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -395,6 +395,7 @@ Singleton { recolourLogo: lock.recolourLogo, enableFprint: lock.enableFprint, maxFprintTries: lock.maxFprintTries, + hideNotifs: lock.hideNotifs, sizes: { heightMult: lock.sizes.heightMult, ratio: lock.sizes.ratio, diff --git a/config/LockConfig.qml b/config/LockConfig.qml index 2af4e2cd5..d0a9fb357 100644 --- a/config/LockConfig.qml +++ b/config/LockConfig.qml @@ -4,6 +4,7 @@ JsonObject { property bool recolourLogo: false property bool enableFprint: true property int maxFprintTries: 3 + property bool hideNotifs: false property Sizes sizes: Sizes {} component Sizes: JsonObject { diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 01f7e4b47..cce86cdf3 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -41,7 +41,7 @@ ColumnLayout { Loader { anchors.centerIn: parent active: opacity > 0 - opacity: Notifs.list.length > 0 ? 0 : 1 + opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 sourceComponent: ColumnLayout { spacing: Appearance.spacing.large @@ -61,7 +61,7 @@ ColumnLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("No Notifications") + text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") color: Colours.palette.m3outlineVariant font.pointSize: Appearance.font.size.large font.family: Appearance.font.family.mono @@ -78,7 +78,7 @@ ColumnLayout { StyledListView { anchors.fill: parent - + visible: !Config.lock.hideNotifs spacing: Appearance.spacing.small clip: true From d2e35a071b36a797a2cb3aebc8a643edde31fa41 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 15 Mar 2026 18:40:12 +1100 Subject: [PATCH 059/409] config: don't serialise sizes We do not want to serialise sizes cause people aren't meant to change them --- config/Config.qml | 164 ++++++++++++++-------------------------------- 1 file changed, 51 insertions(+), 113 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index c5cc1096a..07530fc8e 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -27,6 +27,8 @@ Singleton { property alias services: adapter.services property alias paths: adapter.paths + property bool recentlySaved: false + // Public save function - call this to persist config changes function save(): void { saveTimer.restart(); @@ -34,48 +36,6 @@ Singleton { recentSaveCooldown.restart(); } - property bool recentlySaved: false - - ElapsedTimer { - id: timer - } - - Timer { - id: saveTimer - - interval: 500 - onTriggered: { - timer.restart(); - try { - // Parse current config to preserve structure and comments if possible - let config = {}; - try { - config = JSON.parse(fileView.text()); - } catch (e) { - // If parsing fails, start with empty object - config = {}; - } - - // Update config with current values - config = serializeConfig(); - - // Save to file with pretty printing - fileView.setText(JSON.stringify(config, null, 2)); - } catch (e) { - Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); - } - } - } - - Timer { - id: recentSaveCooldown - - interval: 2000 - onTriggered: { - recentlySaved = false; - } - } - // Helper function to serialize the config object function serializeConfig(): var { return { @@ -238,13 +198,6 @@ Singleton { clock: { showIcon: bar.clock.showIcon }, - sizes: { - innerWidth: bar.sizes.innerWidth, - windowPreviewSize: bar.sizes.windowPreviewSize, - trayMenuWidth: bar.sizes.trayMenuWidth, - batteryWidth: bar.sizes.batteryWidth, - networkWidth: bar.sizes.networkWidth - }, entries: bar.entries, excludedScreens: bar.excludedScreens }; @@ -271,32 +224,12 @@ Singleton { showMemory: dashboard.performance.showMemory, showStorage: dashboard.performance.showStorage, showNetwork: dashboard.performance.showNetwork - }, - sizes: { - tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, - tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, - infoWidth: dashboard.sizes.infoWidth, - infoIconSize: dashboard.sizes.infoIconSize, - dateTimeWidth: dashboard.sizes.dateTimeWidth, - mediaWidth: dashboard.sizes.mediaWidth, - mediaProgressSweep: dashboard.sizes.mediaProgressSweep, - mediaProgressThickness: dashboard.sizes.mediaProgressThickness, - resourceProgessThickness: dashboard.sizes.resourceProgessThickness, - weatherWidth: dashboard.sizes.weatherWidth, - mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize, - mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize, - resourceSize: dashboard.sizes.resourceSize } }; } function serializeControlCenter(): var { - return { - sizes: { - heightMult: controlCenter.sizes.heightMult, - ratio: controlCenter.sizes.ratio - } - }; + return {}; } function serializeLauncher(): var { @@ -319,12 +252,6 @@ Singleton { variants: launcher.useFuzzy.variants, wallpapers: launcher.useFuzzy.wallpapers }, - sizes: { - itemWidth: launcher.sizes.itemWidth, - itemHeight: launcher.sizes.itemHeight, - wallpaperWidth: launcher.sizes.wallpaperWidth, - wallpaperHeight: launcher.sizes.wallpaperHeight - }, actions: launcher.actions }; } @@ -336,12 +263,7 @@ Singleton { clearThreshold: notifs.clearThreshold, expandThreshold: notifs.expandThreshold, actionOnClick: notifs.actionOnClick, - groupPreviewNum: notifs.groupPreviewNum, - sizes: { - width: notifs.sizes.width, - image: notifs.sizes.image, - badge: notifs.sizes.badge - } + groupPreviewNum: notifs.groupPreviewNum }; } @@ -350,11 +272,7 @@ Singleton { enabled: osd.enabled, hideDelay: osd.hideDelay, enableBrightness: osd.enableBrightness, - enableMicrophone: osd.enableMicrophone, - sizes: { - sliderWidth: osd.sizes.sliderWidth, - sliderHeight: osd.sizes.sliderHeight - } + enableMicrophone: osd.enableMicrophone }; } @@ -374,20 +292,12 @@ Singleton { shutdown: session.commands.shutdown, hibernate: session.commands.hibernate, reboot: session.commands.reboot - }, - sizes: { - button: session.sizes.button } }; } function serializeWinfo(): var { - return { - sizes: { - heightMult: winfo.sizes.heightMult, - detailsWidth: winfo.sizes.detailsWidth - } - }; + return {}; } function serializeLock(): var { @@ -395,12 +305,7 @@ Singleton { recolourLogo: lock.recolourLogo, enableFprint: lock.enableFprint, maxFprintTries: lock.maxFprintTries, - hideNotifs: lock.hideNotifs, - sizes: { - heightMult: lock.sizes.heightMult, - ratio: lock.sizes.ratio, - centerWidth: lock.sizes.centerWidth - } + hideNotifs: lock.hideNotifs }; } @@ -408,10 +313,6 @@ Singleton { return { enabled: utilities.enabled, maxToasts: utilities.maxToasts, - sizes: { - width: utilities.sizes.width, - toastWidth: utilities.sizes.toastWidth - }, toasts: { configLoaded: utilities.toasts.configLoaded, chargingChanged: utilities.toasts.chargingChanged, @@ -435,10 +336,7 @@ Singleton { function serializeSidebar(): var { return { enabled: sidebar.enabled, - dragThreshold: sidebar.dragThreshold, - sizes: { - width: sidebar.sizes.width - } + dragThreshold: sidebar.dragThreshold }; } @@ -467,6 +365,46 @@ Singleton { }; } + ElapsedTimer { + id: timer + } + + Timer { + id: saveTimer + + interval: 500 + onTriggered: { + timer.restart(); + try { + // Parse current config to preserve structure and comments if possible + let config = {}; + try { + config = JSON.parse(fileView.text()); + } catch (e) { + // If parsing fails, start with empty object + config = {}; + } + + // Update config with current values + config = root.serializeConfig(); + + // Save to file with pretty printing + fileView.setText(JSON.stringify(config, null, 2)); + } catch (e) { + Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); + } + } + } + + Timer { + id: recentSaveCooldown + + interval: 2000 + onTriggered: { + root.recentlySaved = false; + } + } + FileView { id: fileView @@ -474,7 +412,7 @@ Singleton { watchChanges: true onFileChanged: { // Prevent reload loop - don't reload if we just saved - if (!recentlySaved) { + if (!root.recentlySaved) { timer.restart(); reload(); } else { @@ -487,9 +425,9 @@ Singleton { JSON.parse(text()); const elapsed = timer.elapsedMs(); // Only show toast for external changes (not our own saves) and when elapsed time is meaningful - if (adapter.utilities.toasts.configLoaded && !recentlySaved && elapsed > 0) { + if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) { Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); - } else if (adapter.utilities.toasts.configLoaded && recentlySaved && elapsed > 0) { + } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) { Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); } } catch (e) { From aea2ac96ef9a6817fe9e81867da202eed4daba90 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bora=20G=C3=BClerman?= <49169566+eratoriele@users.noreply.github.com> Date: Sun, 15 Mar 2026 10:42:34 +0300 Subject: [PATCH 060/409] controlcenter/taskbar: add excludedScreens (#1215) also changed controlcenter/components/ConnectedButtonGroup - Changed row layout to grid layout - Added optional prop: row, which defaults to 1 so it looks same as row layout if not given - added new field to options, which bypasses rootItem bind. This is needed because we can not predict the number of monitors the user has, and can not create a seperate variable for each one --- .../components/ConnectedButtonGroup.qml | 17 +++++--- modules/controlcenter/taskbar/TaskbarPane.qml | 40 +++++++++++++++++++ services/Hypr.qml | 4 ++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 01cd612c9..ab707fb73 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -10,9 +10,10 @@ import QtQuick.Layouts StyledRect { id: root - property var options: [] // Array of {label: string, propertyName: string, onToggled: function} + property var options: [] // Array of {label: string, propertyName: string, onToggled: function, state: bool?} property var rootItem: null // The root item that contains the properties we want to bind to property string title: "" // Optional title text + property int rows: 1 // Number of rows Layout.fillWidth: true implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 @@ -37,10 +38,13 @@ StyledRect { font.pointSize: Appearance.font.size.normal } - RowLayout { - id: buttonRow + GridLayout { + id: buttonGrid Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small + rowSpacing: Appearance.spacing.small + columnSpacing: Appearance.spacing.small + rows: root.rows + columns: Math.ceil(root.options.length / root.rows) Repeater { id: repeater @@ -62,7 +66,10 @@ StyledRect { // Create binding in Component.onCompleted Component.onCompleted: { - if (root.rootItem && modelData.propertyName) { + if (modelData.state !== undefined && modelData.state) { + _checked = modelData.state; + } + else if (root.rootItem && modelData.propertyName) { const propName = modelData.propertyName; const rootItem = root.rootItem; _checked = Qt.binding(function () { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 9c999e5ba..6c6b5e519 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -46,6 +46,8 @@ Item { property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true property bool popoutTray: Config.bar.popouts.tray ?? true property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true + property list monitorNames: Hypr.monitorNames() + property list excludedScreens: Config.bar.excludedScreens ?? [] anchors.fill: parent @@ -90,6 +92,7 @@ Item { Config.bar.popouts.activeWindow = root.popoutActiveWindow; Config.bar.popouts.tray = root.popoutTray; Config.bar.popouts.statusIcons = root.popoutStatusIcons; + Config.bar.excludedScreens = root.excludedScreens; const entries = []; for (let i = 0; i < entriesModel.count; i++) { @@ -677,6 +680,43 @@ Item { ] } } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Monitors") + font.pointSize: Appearance.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root + // max 3 options per line + rows: Math.ceil(root.monitorNames.length / 3) + + options: root.monitorNames.map(e => ({ + label: qsTr(e), + propertyName: `monitor${e}`, + onToggled: function (_) { + // if the given monitor is in the excluded list, it should be added back + let addedBack = excludedScreens.includes(e) + if (addedBack) { + const index = excludedScreens.indexOf(e); + if (index !== -1) { + excludedScreens.splice(index, 1); + } + } else { + if (!excludedScreens.includes(e)) { + excludedScreens.push(e); + } + } + root.saveConfig(); + }, + state: !Strings.testRegexList(root.excludedScreens, e) + })) + } + } } } } diff --git a/services/Hypr.qml b/services/Hypr.qml index a26c24df4..86c82f6b0 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -75,6 +75,10 @@ Singleton { dispatch(`workspace ${openSpecials[nextIndex].name}`); } + function monitorNames(): list { + return monitors.values.map(e => e.name); + } + function monitorFor(screen: ShellScreen): HyprlandMonitor { return Hyprland.monitorFor(screen); } From 0903a6a84b34e806ae6f4458f127010982e41bcb Mon Sep 17 00:00:00 2001 From: Kalagmitan <121934419+Kalagmitan@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:51:07 +0800 Subject: [PATCH 061/409] bar/osicon: fix blurriness (#1271) * osicon: Unblurred sidebar icon + The multiplier on the implicitWidth and implicitHeight caused the distro icon on the sidebar to look blurry. I believe this is because the screen can't render a fraction of a pixel, and so, the qtengine compensates by using anti-aliasing which causes the icon look smudged. + I changed the font size of the icon to a standard integer size (extraLarge) and removed the multipliers. A side-effect of this commit is that the icon looks a bit bigger now, I chose extraLarge instead of just large because the icon looked a bit too small, but I do not know of other people's preference on this. * feat: Adjusted root implictWidth and implicitHeight * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- components/Logo.qml | 65 ++++++++++++++++--------------- modules/bar/components/OsIcon.qml | 14 +++---- modules/lock/Fetch.qml | 1 - 3 files changed, 40 insertions(+), 40 deletions(-) diff --git a/components/Logo.qml b/components/Logo.qml index 3ab4f2b25..7cd41e17f 100644 --- a/components/Logo.qml +++ b/components/Logo.qml @@ -4,8 +4,6 @@ import qs.services Item { id: root - implicitWidth: designWidth - implicitHeight: designHeight readonly property real designWidth: 128 readonly property real designHeight: 90.38 @@ -13,6 +11,9 @@ Item { property color topColour: Colours.palette.m3primary property color bottomColour: Colours.palette.m3onSurface + implicitWidth: designWidth + implicitHeight: designHeight + Shape { anchors.centerIn: parent width: root.designWidth @@ -21,49 +22,49 @@ Item { transformOrigin: Item.Center preferredRendererType: Shape.CurveRenderer - ShapePath { - fillColor: root.topColour - strokeColor: "transparent" + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" - PathSvg { - path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" - } + PathSvg { + path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" } + } - ShapePath { - fillColor: root.bottomColour - strokeColor: "transparent" + ShapePath { + fillColor: root.bottomColour + strokeColor: "transparent" - PathSvg { - path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" - } + PathSvg { + path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" } + } - ShapePath { - fillColor: root.topColour - strokeColor: "transparent" + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" - PathSvg { - path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" - } + PathSvg { + path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" } + } - ShapePath { - fillColor: root.topColour - strokeColor: "transparent" + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" - PathSvg { - path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" - } + PathSvg { + path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" } + } - ShapePath { - fillColor: root.topColour - strokeColor: "transparent" + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" - PathSvg { - path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" - } + PathSvg { + path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" } + } } } diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index a61500abb..6710294a0 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -1,15 +1,15 @@ +import qs.components import qs.components.effects import qs.services import qs.config import qs.utils import QtQuick -import qs.components Item { id: root - - implicitWidth: Appearance.font.size.large * 1.2 - implicitHeight: Appearance.font.size.large * 1.2 + + implicitWidth: Math.round(Appearance.font.size.large * 1.2) + implicitHeight: Math.round(Appearance.font.size.large * 1.2) MouseArea { anchors.fill: parent @@ -29,8 +29,8 @@ Item { id: caelestiaLogo Logo { - implicitWidth: Appearance.font.size.large * 1.8 - implicitHeight: Appearance.font.size.large * 1.8 + implicitWidth: Math.round(Appearance.font.size.large * 1.6) + implicitHeight: Math.round(Appearance.font.size.large * 1.6) } } @@ -39,7 +39,7 @@ Item { ColouredIcon { source: SysInfo.osLogo - implicitSize: Appearance.font.size.large * 1.2 + implicitSize: Math.round(Appearance.font.size.large * 1.2) colour: Colours.palette.m3tertiary } } diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index 55d6aa764..e96b14315 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -147,7 +147,6 @@ ColumnLayout { Logo { width: height - height: height } } From b68f0bae14227a4d65303654fef4801c76f57ac0 Mon Sep 17 00:00:00 2001 From: Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> Date: Sun, 15 Mar 2026 15:55:55 +0800 Subject: [PATCH 062/409] bar/activewindow: add compact option (#1201) Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- README.md | 4 ++ config/BarConfig.qml | 1 + config/Config.qml | 4 ++ modules/bar/components/ActiveWindow.qml | 14 +++- modules/controlcenter/taskbar/TaskbarPane.qml | 70 ++++++++++++++----- 5 files changed, 73 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 651881559..0b0ec2068 100644 --- a/README.md +++ b/README.md @@ -331,6 +331,10 @@ default, you must create it manually. } }, "bar": { + "activeWindow": { + "compact": false, + "inverted": false + }, "clock": { "showIcon": true }, diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 62d6b178d..6254d9409 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -82,6 +82,7 @@ JsonObject { } component ActiveWindow: JsonObject { + property bool compact: false property bool inverted: false } diff --git a/config/Config.qml b/config/Config.qml index 07530fc8e..2a261e71d 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -178,6 +178,10 @@ Singleton { capitalisation: bar.workspaces.capitalisation, specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons }, + activeWindow: { + compact: bar.activeWindow.compact, + inverted: bar.activeWindow.inverted + }, tray: { background: bar.tray.background, recolour: bar.tray.recolour, diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 0c9b21e6f..b21c52503 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -13,6 +13,18 @@ Item { required property Brightness.Monitor monitor property color colour: Colours.palette.m3primary + readonly property string windowTitle: Hypr.activeToplevel?.title ?? qsTr("Desktop") + + function getCompactName() { + if (!root.windowTitle || root.windowTitle === qsTr("Desktop")) + return qsTr("Desktop"); + // " - " (standard hyphen), " — " (em dash), " – " (en dash) + const parts = root.windowTitle.split(/\s+[\-\u2013\u2014]\s+/); + if (parts.length > 1) + return parts[parts.length - 1].trim(); + return root.windowTitle; + } + readonly property int maxHeight: { const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer"); const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0); @@ -46,7 +58,7 @@ Item { TextMetrics { id: metrics - text: Hypr.activeToplevel?.title ?? qsTr("Desktop") + text: Config.bar.activeWindow.compact ? root.getCompactName() : root.windowTitle font.pointSize: Appearance.font.size.smaller font.family: Appearance.font.family.mono elide: Qt.ElideRight diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 6c6b5e519..ba65c1e74 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -19,6 +19,8 @@ Item { required property Session session + property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false + property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false property bool clockShowIcon: Config.bar.clock.showIcon ?? true property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true @@ -65,6 +67,8 @@ Item { } function saveConfig(entryIndex, entryEnabled) { + Config.bar.activeWindow.compact = root.activeWindowCompact; + Config.bar.activeWindow.inverted = root.activeWindowInverted; Config.bar.clock.showIcon = root.clockShowIcon; Config.bar.persistent = root.persistent; Config.bar.showOnHover = root.showOnHover; @@ -595,6 +599,34 @@ Item { } } } + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Active window") + font.pointSize: Appearance.font.size.normal + } + + SwitchRow { + label: qsTr("Compact") + checked: root.activeWindowCompact + onToggled: checked => { + root.activeWindowCompact = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Inverted") + checked: root.activeWindowInverted + onToggled: checked => { + root.activeWindowInverted = checked; + root.saveConfig(); + } + } + } } ColumnLayout { @@ -696,25 +728,25 @@ Item { rows: Math.ceil(root.monitorNames.length / 3) options: root.monitorNames.map(e => ({ - label: qsTr(e), - propertyName: `monitor${e}`, - onToggled: function (_) { - // if the given monitor is in the excluded list, it should be added back - let addedBack = excludedScreens.includes(e) - if (addedBack) { - const index = excludedScreens.indexOf(e); - if (index !== -1) { - excludedScreens.splice(index, 1); - } - } else { - if (!excludedScreens.includes(e)) { - excludedScreens.push(e); - } - } - root.saveConfig(); - }, - state: !Strings.testRegexList(root.excludedScreens, e) - })) + label: qsTr(e), + propertyName: `monitor${e}`, + onToggled: function (_) { + // if the given monitor is in the excluded list, it should be added back + let addedBack = excludedScreens.includes(e); + if (addedBack) { + const index = excludedScreens.indexOf(e); + if (index !== -1) { + excludedScreens.splice(index, 1); + } + } else { + if (!excludedScreens.includes(e)) { + excludedScreens.push(e); + } + } + root.saveConfig(); + }, + state: !Strings.testRegexList(root.excludedScreens, e) + })) } } } From 197eafdadc54c1513dc4e392b965b582f0a9b847 Mon Sep 17 00:00:00 2001 From: nik-bsta Date: Sun, 15 Mar 2026 08:59:02 +0100 Subject: [PATCH 063/409] nix: remove outdated app2unit version pin (#1266) * nix: Remove outadeted app2unit version pin. * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- flake.nix | 1 - nix/app2unit.nix | 14 -------------- 2 files changed, 15 deletions(-) delete mode 100644 nix/app2unit.nix diff --git a/flake.nix b/flake.nix index 5c884115d..0d7ebb8c2 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,6 @@ withX11 = false; withI3 = false; }; - app2unit = pkgs.callPackage ./nix/app2unit.nix {inherit pkgs;}; caelestia-cli = inputs.caelestia-cli.packages.${pkgs.stdenv.hostPlatform.system}.default; }; with-cli = caelestia-shell.override {withCli = true;}; diff --git a/nix/app2unit.nix b/nix/app2unit.nix deleted file mode 100644 index 51b4241ce..000000000 --- a/nix/app2unit.nix +++ /dev/null @@ -1,14 +0,0 @@ -{ - pkgs, # To ensure the nixpkgs version of app2unit - fetchFromGitHub, - ... -}: -pkgs.app2unit.overrideAttrs (final: prev: rec { - version = "1.0.3"; # Fix old issue related to missing env var - src = fetchFromGitHub { - owner = "Vladimir-csp"; - repo = "app2unit"; - tag = "v${version}"; - hash = "sha256-7eEVjgs+8k+/NLteSBKgn4gPaPLHC+3Uzlmz6XB0930="; - }; -}) From 63c86e950b59ba1b29df367065ee2623b0335906 Mon Sep 17 00:00:00 2001 From: Xavier Lhinares <60365026+XLhinares@users.noreply.github.com> Date: Sun, 15 Mar 2026 16:17:49 +0800 Subject: [PATCH 064/409] bar: allow setting custom workspace app icons in shell.json (#1214) * bar: allow setting custom workspace app icons in shell.json * rename to windowIcons and use regex field for regex Also allow specifying regex flags and exact name * add default config (fix steam icons) --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- README.md | 6 ++++++ config/BarConfig.qml | 6 ++++++ config/Config.qml | 3 ++- utils/Icons.qml | 30 ++++++++++++++++++++++++++---- 4 files changed, 40 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0b0ec2068..bd2ef13e6 100644 --- a/README.md +++ b/README.md @@ -420,6 +420,12 @@ default, you must create it manually. "name": "steam", "icon": "sports_esports" } + ], + "windowIcons": [ + { + "regex": "steam(_app_(default|[0-9]+))?", + "icon": "sports_esports" + } ] }, "excludedScreens": [""], diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 6254d9409..2e041082e 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -79,6 +79,12 @@ JsonObject { property string activeLabel: "󰮯" property string capitalisation: "preserve" // upper, lower, or preserve - relevant only if label is empty property list specialWorkspaceIcons: [] + property list windowIcons: [ + { + regex: "steam(_app_(default|[0-9]+))?", + icon: "sports_esports" + } + ] } component ActiveWindow: JsonObject { diff --git a/config/Config.qml b/config/Config.qml index 2a261e71d..584aebab7 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -176,7 +176,8 @@ Singleton { occupiedLabel: bar.workspaces.occupiedLabel, activeLabel: bar.workspaces.activeLabel, capitalisation: bar.workspaces.capitalisation, - specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons + specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons, + windowIcons: bar.workspaces.windowIcons }, activeWindow: { compact: bar.activeWindow.compact, diff --git a/utils/Icons.qml b/utils/Icons.qml index c06cbf809..34f8049bf 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -79,6 +79,26 @@ Singleton { Office: "content_paste" }) + // Checks if a name matches an icon config. Icon configs can have the following keys: + // - name: The exact name of the icon + // - regex: A regex to match against the name (takes priority over name) + // - flags: The regex flags (only used if regex is set) + // - icon: The icon to use + function matchIconConfig(name: string, iconConfig: var): bool { + if (!iconConfig.icon) + return false; + + if (iconConfig.regex) { + const re = new RegExp(iconConfig.regex, iconConfig.flags ?? ""); + if (re.test(name)) + return true; + } else if (iconConfig.name === name) { + return true; + } + + return false; + } + function getAppIcon(name: string, fallback: string): string { const icon = DesktopEntries.heuristicLookup(name)?.icon; if (fallback !== "undefined") @@ -87,6 +107,10 @@ Singleton { } function getAppCategoryIcon(name: string, fallback: string): string { + for (const iconConfig of Config.bar.workspaces.windowIcons) + if (matchIconConfig(name, iconConfig)) + return iconConfig.icon; + const categories = DesktopEntries.heuristicLookup(name)?.categories; if (categories) @@ -188,11 +212,9 @@ Singleton { function getSpecialWsIcon(name: string): string { name = name.toLowerCase().slice("special:".length); - for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) { - if (iconConfig.name === name) { + for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) + if (matchIconConfig(name, iconConfig)) return iconConfig.icon; - } - } if (name === "special") return "star"; From 4c93de1513841d27bae442c7160832624cd1429a Mon Sep 17 00:00:00 2001 From: Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> Date: Sun, 15 Mar 2026 16:38:50 +0800 Subject: [PATCH 065/409] utilities: allow disabling and moving toggles + support for extra row (#1181) * quickToggles: Allow disabling and moving toggles and support for extra row * edit: README * splitIndex logic edit * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- README.md | 32 +++++- config/Config.qml | 3 +- config/UtilitiesConfig.qml | 31 ++++++ modules/utilities/cards/Toggles.qml | 161 +++++++++++++++++++--------- 4 files changed, 173 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index bd2ef13e6..e3fcba321 100644 --- a/README.md +++ b/README.md @@ -661,7 +661,37 @@ default, you must create it manually. "enabled": false } ] - } + }, + "quickToggles": [ + { + "id": "wifi", + "enabled": true + }, + { + "id": "bluetooth", + "enabled": true + }, + { + "id": "mic", + "enabled": true + }, + { + "enabled": true, + "id": "settings" + }, + { + "id": "gameMode", + "enabled": true + }, + { + "id": "dnd", + "enabled": true + }, + { + "id": "vpn", + "enabled": true + } + ] } } ``` diff --git a/config/Config.qml b/config/Config.qml index 584aebab7..6efe96003 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -334,7 +334,8 @@ Singleton { vpn: { enabled: utilities.vpn.enabled, provider: utilities.vpn.provider - } + }, + quickToggles: utilities.quickToggles }; } diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index cf4644602..4d1dd6e8d 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -32,4 +32,35 @@ JsonObject { property bool enabled: false property list provider: ["netbird"] } + + property list quickToggles: [ + { + id: "wifi", + enabled: true + }, + { + id: "bluetooth", + enabled: true + }, + { + id: "mic", + enabled: true + }, + { + id: "settings", + enabled: true + }, + { + id: "gameMode", + enabled: true + }, + { + id: "dnd", + enabled: true + }, + { + id: "vpn", + enabled: false + } + ] } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index dd4a687ba..d610586bf 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -14,6 +14,30 @@ StyledRect { required property var visibilities required property Item popouts + readonly property var quickToggles: { + const seenIds = new Set(); + + return Config.utilities.quickToggles.filter(item => { + if (!item.enabled) + return false; + + if (seenIds.has(item.id)) { + return false; + } + + if (item.id === "vpn") { + return Config.utilities.vpn.provider.some(p => + typeof p === "object" ? (p.enabled === true) : false + ); + } + + seenIds.add(item.id); + return true; + }); + } + readonly property int splitIndex: Math.ceil(quickToggles.length / 2) + readonly property bool needExtraRow: quickToggles.length > 6 + Layout.fillWidth: true implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 @@ -32,64 +56,97 @@ StyledRect { font.pointSize: Appearance.font.size.normal } - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small + ToggleRow { + rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles + } - Toggle { - icon: "wifi" - checked: Nmcli.wifiEnabled - onClicked: Nmcli.toggleWifi() - } + ToggleRow { + visible: root.needExtraRow + rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] + } + } - Toggle { - icon: "bluetooth" - checked: Bluetooth.defaultAdapter?.enabled ?? false - onClicked: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.enabled = !adapter.enabled; - } - } + component ToggleRow: RowLayout { + property var rowModel: [] - Toggle { - icon: "mic" - checked: !Audio.sourceMuted - onClicked: { - const audio = Audio.source?.audio; - if (audio) - audio.muted = !audio.muted; - } - } + Layout.fillWidth: true + spacing: Appearance.spacing.small - Toggle { - icon: "settings" - inactiveOnColour: Colours.palette.m3onSurfaceVariant - toggle: false - onClicked: { - root.visibilities.utilities = false; - root.popouts.detach("network"); - } - } + Repeater { + model: parent.rowModel - Toggle { - icon: "gamepad" - checked: GameMode.enabled - onClicked: GameMode.enabled = !GameMode.enabled - } + delegate: DelegateChooser { + role: "id" - Toggle { - icon: "notifications_off" - checked: Notifs.dnd - onClicked: Notifs.dnd = !Notifs.dnd - } - - Toggle { - icon: "vpn_key" - checked: VPN.connected - enabled: !VPN.connecting - visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) - onClicked: VPN.toggle() + DelegateChoice { + roleValue: "wifi" + delegate: Toggle { + icon: "wifi" + checked: Nmcli.wifiEnabled + onClicked: Nmcli.toggleWifi() + } + } + DelegateChoice { + roleValue: "bluetooth" + delegate: Toggle { + icon: "bluetooth" + checked: Bluetooth.defaultAdapter?.enabled ?? false + onClicked: { + const adapter = Bluetooth.defaultAdapter; + if (adapter) + adapter.enabled = !adapter.enabled; + } + } + } + DelegateChoice { + roleValue: "mic" + delegate: Toggle { + icon: "mic" + checked: !Audio.sourceMuted + onClicked: { + const audio = Audio.source?.audio; + if (audio) + audio.muted = !audio.muted; + } + } + } + DelegateChoice { + roleValue: "settings" + delegate: Toggle { + icon: "settings" + inactiveOnColour: Colours.palette.m3onSurfaceVariant + toggle: false + onClicked: { + root.visibilities.utilities = false; + root.popouts.detach("network"); + } + } + } + DelegateChoice { + roleValue: "gameMode" + delegate: Toggle { + icon: "gamepad" + checked: GameMode.enabled + onClicked: GameMode.enabled = !GameMode.enabled + } + } + DelegateChoice { + roleValue: "dnd" + delegate: Toggle { + icon: "notifications_off" + checked: Notifs.dnd + onClicked: Notifs.dnd = !Notifs.dnd + } + } + DelegateChoice { + roleValue: "vpn" + delegate: Toggle { + icon: "vpn_key" + checked: VPN.connected + enabled: !VPN.connecting + onClicked: VPN.toggle() + } + } } } } From f5f4c51a73e34383a98c984311d63c7f56a56f6e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:16:06 +1100 Subject: [PATCH 066/409] bar/activewindow: fix anim --- services/Hypr.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/Hypr.qml b/services/Hypr.qml index 86c82f6b0..c703f7047 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -16,7 +16,10 @@ Singleton { readonly property var workspaces: Hyprland.workspaces readonly property var monitors: Hyprland.monitors - readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null + readonly property HyprlandToplevel activeToplevel: { + const t = Hyprland.activeToplevel; + return t?.workspace?.name.startsWith("special:") || Hyprland.focusedWorkspace?.toplevels.values.length > 0 ? t : null; + } readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor readonly property int activeWsId: focusedWorkspace?.id ?? 1 From dc8767e28b4172a256f23140932ac4af228f0f38 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:16:34 +1100 Subject: [PATCH 067/409] bar/activewindow: format --- modules/bar/components/ActiveWindow.qml | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index b21c52503..3eb690412 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -13,16 +13,17 @@ Item { required property Brightness.Monitor monitor property color colour: Colours.palette.m3primary - readonly property string windowTitle: Hypr.activeToplevel?.title ?? qsTr("Desktop") - - function getCompactName() { - if (!root.windowTitle || root.windowTitle === qsTr("Desktop")) + readonly property string windowTitle: { + const title = Hypr.activeToplevel?.title; + if (!title) return qsTr("Desktop"); - // " - " (standard hyphen), " — " (em dash), " – " (en dash) - const parts = root.windowTitle.split(/\s+[\-\u2013\u2014]\s+/); - if (parts.length > 1) - return parts[parts.length - 1].trim(); - return root.windowTitle; + if (Config.bar.activeWindow.compact) { + // " - " (standard hyphen), " — " (em dash), " – " (en dash) + const parts = root.windowTitle.split(/\s+[\-\u2013\u2014]\s+/); + if (parts.length > 1) + return parts[parts.length - 1].trim(); + } + return title; } readonly property int maxHeight: { @@ -58,7 +59,7 @@ Item { TextMetrics { id: metrics - text: Config.bar.activeWindow.compact ? root.getCompactName() : root.windowTitle + text: root.windowTitle font.pointSize: Appearance.font.size.smaller font.family: Appearance.font.family.mono elide: Qt.ElideRight @@ -93,7 +94,7 @@ Item { transform: [ Translate { - x: Config.bar.activeWindow.inverted ? -implicitWidth + text.implicitHeight : 0 + x: Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0 }, Rotation { angle: Config.bar.activeWindow.inverted ? 270 : 90 From 521cd4079ee665dda1881fb6082497d58d654969 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:29:28 +1100 Subject: [PATCH 068/409] bar/activewindow: allow disable show on hover Closes #1209 Closes #1019 --- README.md | 3 ++- config/BarConfig.qml | 1 + config/Config.qml | 3 ++- modules/bar/Bar.qml | 3 ++- modules/bar/components/ActiveWindow.qml | 15 +++++++++++++++ 5 files changed, 22 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index e3fcba321..e3bebcd7e 100644 --- a/README.md +++ b/README.md @@ -333,7 +333,8 @@ default, you must create it manually. "bar": { "activeWindow": { "compact": false, - "inverted": false + "inverted": false, + "showOnHover": true }, "clock": { "showIcon": true diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 2e041082e..310344b11 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -90,6 +90,7 @@ JsonObject { component ActiveWindow: JsonObject { property bool compact: false property bool inverted: false + property bool showOnHover: true } component Tray: JsonObject { diff --git a/config/Config.qml b/config/Config.qml index 6efe96003..2fd8c436f 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -181,7 +181,8 @@ Singleton { }, activeWindow: { compact: bar.activeWindow.compact, - inverted: bar.activeWindow.inverted + inverted: bar.activeWindow.inverted, + showOnHover: bar.activeWindow.showOnHover }, tray: { background: bar.tray.background, diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index cb384e393..95c166e6f 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -68,7 +68,7 @@ ColumnLayout { popouts.hasCurrent = false; item.expanded = true; } - } else if (id === "activeWindow" && Config.bar.popouts.activeWindow) { + } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { popouts.currentName = id.toLowerCase(); popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y; popouts.hasCurrent = true; @@ -134,6 +134,7 @@ ColumnLayout { DelegateChoice { roleValue: "activeWindow" delegate: WrappedLoader { + Layout.fillWidth: true sourceComponent: ActiveWindow { bar: root monitor: Brightness.getMonitorForScreen(root.screen) diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 3eb690412..4881f4208 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -38,6 +38,21 @@ Item { implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight) implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin + Loader { + anchors.fill: parent + active: !Config.bar.activeWindow.showOnHover + + sourceComponent: MouseArea { + cursorShape: Qt.PointingHandCursor + onClicked: { + const popouts = root.bar.popouts; + popouts.currentName = "activewindow"; + popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y; + popouts.hasCurrent = true; + } + } + } + MaterialIcon { id: icon From 5d492539d1b99824af7b38ac6c007191a1c8a34d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:33:15 +1100 Subject: [PATCH 069/409] bar/activewindow: toggle when not show on hover --- modules/bar/components/ActiveWindow.qml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 4881f4208..62bab0f6a 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -46,9 +46,13 @@ Item { cursorShape: Qt.PointingHandCursor onClicked: { const popouts = root.bar.popouts; - popouts.currentName = "activewindow"; - popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y; - popouts.hasCurrent = true; + if (popouts.hasCurrent) { + popouts.hasCurrent = false; + } else { + popouts.currentName = "activewindow"; + popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y; + popouts.hasCurrent = true; + } } } } From 377778596acf90451d1bd19f0c03b5f1c0467958 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 15 Mar 2026 22:36:40 +1100 Subject: [PATCH 070/409] fix: close other popouts when hover activewindow --- modules/bar/components/ActiveWindow.qml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 62bab0f6a..414c9c579 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -44,6 +44,12 @@ Item { sourceComponent: MouseArea { cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onPositionChanged: { + const popouts = root.bar.popouts; + if (popouts.hasCurrent && popouts.currentName !== "activewindow") + popouts.hasCurrent = false; + } onClicked: { const popouts = root.bar.popouts; if (popouts.hasCurrent) { From 27ae9f70421ecad2f782060eb0edf687f929e97d Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 03:00:43 +0300 Subject: [PATCH 071/409] Added my screen recorder config --- modules/utilities/cards/Record.qml | 228 ++++++++++++++++++++++---- services/Recorder.qml | 253 +++++++++++++++++++++++++---- 2 files changed, 417 insertions(+), 64 deletions(-) diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 273c64002..c4de7f1b7 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -19,6 +19,11 @@ StyledRect { radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer + property bool actuallyRecording: Recorder.running + property string lastError: "" + property string currentVideoMode: "fullscreen" + property string currentAudioMode: "none" + ColumnLayout { id: layout @@ -38,7 +43,7 @@ StyledRect { } radius: Appearance.rounding.full - color: Recorder.running ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + color: root.actuallyRecording ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { id: icon @@ -47,7 +52,7 @@ StyledRect { anchors.horizontalCenterOffset: -0.5 anchors.verticalCenterOffset: 1.5 text: "screen_record" - color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + color: root.actuallyRecording ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } @@ -65,51 +70,117 @@ StyledRect { StyledText { Layout.fillWidth: true - text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off") - color: Colours.palette.m3onSurfaceVariant + text: { + if (root.lastError !== "") return qsTr("Error: %1").arg(root.lastError); + if (Recorder.paused) return qsTr("Recording paused"); + if (root.actuallyRecording) { + const videoText = root.currentVideoMode; + const audioText = root.currentAudioMode === "none" ? "no audio" : root.currentAudioMode; + return qsTr("Recording %1 - %2").arg(videoText).arg(audioText); + } + return qsTr("Recording off"); + } + color: root.lastError !== "" ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small elide: Text.ElideRight } } SplitButton { - disabled: Recorder.running - active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0] - menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text + disabled: root.actuallyRecording + active: menuItems.find(m => root.props.recordingMode === m.mode) ?? menuItems[0] + menu.onItemSelected: item => { + root.props.recordingMode = item.mode; + root.currentVideoMode = item.videoMode; + root.currentAudioMode = item.audioMode; + } menuItems: [ MenuItem { + property string mode: "fullscreen" + property string videoMode: "fullscreen" + property string audioMode: "none" icon: "fullscreen" text: qsTr("Record fullscreen") activeText: qsTr("Fullscreen") - onClicked: Recorder.start() + onClicked: startRecording() }, MenuItem { + property string mode: "region" + property string videoMode: "region" + property string audioMode: "none" icon: "screenshot_region" text: qsTr("Record region") activeText: qsTr("Region") - onClicked: Recorder.start(["-r"]) + onClicked: startRecording() }, MenuItem { - icon: "select_to_speak" - text: qsTr("Record fullscreen with sound") - activeText: qsTr("Fullscreen") - onClicked: Recorder.start(["-s"]) + property string mode: "window" + property string videoMode: "window" + property string audioMode: "none" + icon: "web_asset" + text: qsTr("Record window") + activeText: qsTr("Window") + onClicked: startRecording() + }, + MenuItem { + property string mode: "mic" + property string videoMode: "fullscreen" + property string audioMode: "mic" + icon: "mic" + text: qsTr("Record fullscreen with mic") + activeText: qsTr("Mic") + onClicked: startRecording() }, MenuItem { + property string mode: "system" + property string videoMode: "fullscreen" + property string audioMode: "system" icon: "volume_up" - text: qsTr("Record region with sound") - activeText: qsTr("Region") - onClicked: Recorder.start(["-sr"]) + text: qsTr("Record fullscreen with system audio") + activeText: qsTr("System") + onClicked: startRecording() + }, + MenuItem { + property string mode: "combined" + property string videoMode: "fullscreen" + property string audioMode: "combined" + icon: "settings_voice" + text: qsTr("Record fullscreen with mic + system") + activeText: qsTr("Combined") + onClicked: startRecording() } ] } } + StyledRect { + id: errorBanner + Layout.fillWidth: true + visible: root.lastError !== "" + implicitHeight: visible ? errorText.implicitHeight + Appearance.padding.normal * 2 : 0 + radius: Appearance.rounding.small + color: Colours.palette.m3errorContainer + + StyledText { + id: errorText + anchors.fill: parent + anchors.margins: Appearance.padding.normal + text: root.lastError + color: Colours.palette.m3onErrorContainer + wrapMode: Text.Wrap + font.pointSize: Appearance.font.size.small + } + + Behavior on implicitHeight { + Anim { duration: Appearance.anim.durations.small } + } + } + Loader { id: listOrControls - property bool running: Recorder.running + property bool running: root.actuallyRecording Layout.fillWidth: true Layout.preferredHeight: implicitHeight @@ -117,9 +188,7 @@ StyledRect { Behavior on Layout.preferredHeight { id: locHeightAnim - enabled: false - Anim {} } @@ -175,7 +244,6 @@ StyledRect { Component { id: recordingList - RecordingList { props: root.props visibilities: root.visibilities @@ -184,7 +252,6 @@ StyledRect { Component { id: recordingControls - RowLayout { spacing: Appearance.spacing.normal @@ -197,7 +264,6 @@ StyledRect { StyledText { id: recText - anchors.centerIn: parent animate: true text: Recorder.paused ? "PAUSED" : "REC" @@ -210,10 +276,9 @@ StyledRect { } SequentialAnimation on opacity { - running: !Recorder.paused + running: !Recorder.paused && root.actuallyRecording alwaysRunToEnd: true loops: Animation.Infinite - Anim { from: 1 to: 0 @@ -232,17 +297,14 @@ StyledRect { StyledText { text: { const elapsed = Recorder.elapsed; - const hours = Math.floor(elapsed / 3600); const mins = Math.floor((elapsed % 3600) / 60); const secs = Math.floor(elapsed % 60).toString().padStart(2, "0"); - let time; if (hours > 0) time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`; else time = `${mins}:${secs}`; - return qsTr("Recording for %1").arg(time); } font.pointSize: Appearance.font.size.normal @@ -261,7 +323,6 @@ StyledRect { font.pointSize: Appearance.font.size.large onClicked: { Recorder.togglePause(); - internalChecked = Recorder.paused; } } @@ -270,7 +331,116 @@ StyledRect { inactiveColour: Colours.palette.m3error inactiveOnColour: Colours.palette.m3onError font.pointSize: Appearance.font.size.large - onClicked: Recorder.stop() + onClicked: stopRecording() + } + } + } + + function startRecording() { + // Clear any previous errors + root.lastError = ""; + + const mode = root.props.recordingMode || "fullscreen"; + + // Map the combined mode to separate video and audio modes + let videoMode = "fullscreen"; + let audioMode = "none"; + + switch(mode) { + case "fullscreen": + videoMode = "fullscreen"; + audioMode = "none"; + break; + case "region": + videoMode = "region"; + audioMode = "none"; + break; + case "window": + videoMode = "window"; + audioMode = "none"; + break; + case "mic": + videoMode = "fullscreen"; + audioMode = "mic"; + break; + case "system": + videoMode = "fullscreen"; + audioMode = "system"; + break; + case "combined": + videoMode = "fullscreen"; + audioMode = "combined"; + break; + } + + root.currentVideoMode = videoMode; + root.currentAudioMode = audioMode; + + console.log("Starting recording - Video:", videoMode, "Audio:", audioMode); + + // Call Recorder service + const success = Recorder.start(videoMode, audioMode); + + if (!success) { + root.lastError = "Failed to start recording"; + } + } + + function stopRecording() { + root.lastError = ""; + Recorder.stop(); + } + + // Clear error after timeout + Timer { + id: errorTimeout + interval: 10000 + repeat: false + running: root.lastError !== "" + onTriggered: { + root.lastError = ""; + } + } + + Connections { + target: Recorder + + function onRunningChanged() { + // Sync actuallyRecording with Recorder.running + root.actuallyRecording = Recorder.running; + + if (!Recorder.running) { + console.log("Recording stopped"); + } + } + + function onErrorOccurred(errorMsg) { + console.error("Recorder error:", errorMsg); + root.lastError = errorMsg; + errorTimeout.restart(); + } + + function onRecordingStarted() { + console.log("Recording started successfully"); + root.lastError = ""; + } + + function onRecordingStopped() { + console.log("Recording stopped successfully"); + } + } + + Component.onCompleted: { + // Sync initial state + root.actuallyRecording = Recorder.running; + + // Set initial mode if saved + if (root.props.recordingMode) { + const mode = root.props.recordingMode; + const item = menuItems.find(m => m.mode === mode); + if (item) { + root.currentVideoMode = item.videoMode; + root.currentAudioMode = item.audioMode; } } } diff --git a/services/Recorder.qml b/services/Recorder.qml index 6eddce949..7abbbc8ae 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -10,25 +10,86 @@ Singleton { readonly property alias running: props.running readonly property alias paused: props.paused readonly property alias elapsed: props.elapsed - property bool needsStart - property list startArgs - property bool needsStop - property bool needsPause - - function start(extraArgs = []): void { - needsStart = true; - startArgs = extraArgs; - checkProc.running = true; + + signal errorOccurred(string errorMsg) + signal recordingStarted() + signal recordingStopped() + + function start(videoMode: string, audioMode: string): bool { + if (props.running) { + console.warn("Recording already running"); + errorOccurred("Recording already in progress"); + return false; + } + + // Build command array + const args = ["caelestia", "record", "--mode", videoMode]; + + if (audioMode && audioMode !== "none") { + args.push("--audio", audioMode); + } + + console.log("Executing:", args.join(" ")); + + try { + Quickshell.execDetached(args); + props.running = true; + props.paused = false; + props.elapsed = 0; + verifyTimer.restart(); + recordingStarted(); + return true; + } catch (error) { + console.error("Failed to start recording:", error); + errorOccurred("Failed to execute recording command: " + error); + props.running = false; + return false; + } } function stop(): void { - needsStop = true; - checkProc.running = true; + if (!props.running) { + console.warn("No recording to stop"); + return; + } + + console.log("Stopping recording"); + + try { + Quickshell.execDetached(["caelestia", "record", "--stop"]); + // Don't immediately set running to false - wait for process to confirm + stopVerifyTimer.restart(); + } catch (error) { + console.error("Failed to stop recording:", error); + errorOccurred("Failed to stop recording: " + error); + // Force state reset on error + props.running = false; + props.paused = false; + props.elapsed = 0; + recordingStopped(); + } } function togglePause(): void { - needsPause = true; - checkProc.running = true; + if (!props.running) { + console.warn("No recording to pause"); + return; + } + + console.log("Toggling pause"); + + try { + Quickshell.execDetached(["caelestia", "record", "--pause"]); + props.paused = !props.paused; + } catch (error) { + console.error("Failed to toggle pause:", error); + errorOccurred("Failed to pause/resume recording: " + error); + } + } + + function verifyRunning(): bool { + statusProc.running = true; + return props.running; } PersistentProperties { @@ -36,47 +97,169 @@ Singleton { property bool running: false property bool paused: false - property real elapsed: 0 // Might get too large for int + property real elapsed: 0 reloadableId: "recorder" } + // Main process checker - runs periodically when recording Process { id: checkProc - running: true + running: false command: ["pidof", "gpu-screen-recorder"] + onExited: code => { - props.running = code === 0; + const wasRunning = props.running; + const isRunning = code === 0; - if (code === 0) { - if (root.needsStop) { - Quickshell.execDetached(["caelestia", "record"]); - props.running = false; - props.paused = false; - } else if (root.needsPause) { - Quickshell.execDetached(["caelestia", "record", "-p"]); - props.paused = !props.paused; - } - } else if (root.needsStart) { - Quickshell.execDetached(["caelestia", "record", ...root.startArgs]); - props.running = true; + // Detect unexpected stop + if (wasRunning && !isRunning) { + console.warn("Recording process stopped unexpectedly"); + props.running = false; props.paused = false; props.elapsed = 0; + recordingStopped(); } - root.needsStart = false; - root.needsStop = false; - root.needsPause = false; + // Schedule next check if still recording + if (props.running) { + statusCheckTimer.restart(); + } + } + } + + // Verification timer after start + Timer { + id: verifyTimer + interval: 1500 + repeat: false + onTriggered: { + console.log("Verifying recording started"); + statusProc.running = true; } } - Connections { - target: Time - // enabled: props.running && !props.paused + // Verification timer after stop + Timer { + id: stopVerifyTimer + interval: 500 + repeat: false + onTriggered: { + console.log("Verifying recording stopped"); + stopStatusProc.running = true; + } + } + + // Status check process for start verification + Process { + id: statusProc - function onSecondsChanged(): void { + running: false + command: ["pidof", "gpu-screen-recorder"] + + onExited: code => { + const isRunning = code === 0; + + if (!isRunning && props.running) { + console.error("Recording process failed to start"); + errorOccurred("Recording process failed to start"); + props.running = false; + props.paused = false; + props.elapsed = 0; + } else if (isRunning && props.running) { + console.log("Recording verified running"); + statusCheckTimer.restart(); + } + } + } + + // Status check process for stop verification + Process { + id: stopStatusProc + + running: false + command: ["pidof", "gpu-screen-recorder"] + + onExited: code => { + const isRunning = code === 0; + + if (!isRunning) { + console.log("Recording stopped successfully"); + props.running = false; + props.paused = false; + props.elapsed = 0; + recordingStopped(); + } else { + // Process still running, try again + console.warn("Process still running, checking again"); + stopVerifyTimer.restart(); + } + } + } + + // Elapsed time tracker + Timer { + id: elapsedTimer + interval: 1000 + repeat: true + running: props.running && !props.paused + + onTriggered: { props.elapsed++; } } + + // Periodic status check while recording + Timer { + id: statusCheckTimer + interval: 3000 + repeat: false + + onTriggered: { + if (props.running) { + checkProc.running = true; + } + } + } + + // Initialize on component completion + Component.onCompleted: { + console.log("Recorder service initialized"); + // Check initial state + initialStatusProc.running = true; + } + + // Initial status check + Process { + id: initialStatusProc + + running: false + command: ["pidof", "gpu-screen-recorder"] + + onExited: code => { + if (code === 0) { + console.log("Found existing recording process"); + props.running = true; + statusCheckTimer.restart(); + } else { + console.log("No existing recording process"); + props.running = false; + props.paused = false; + props.elapsed = 0; + } + } + } + + // Cleanup on destruction + Component.onDestruction: { + if (props.running) { + console.log("Service destroyed while recording - stopping recording"); + try { + Quickshell.execDetached(["caelestia", "record", "--stop"]); + } catch (error) { + console.error("Failed to stop recording on cleanup:", error); + } + } + } } From 2293630d8e39ed1276efb733b0bc40e0e523e1f0 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 14 Dec 2025 03:21:38 +0300 Subject: [PATCH 072/409] Updated audio options --- services/Recorder.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/Recorder.qml b/services/Recorder.qml index 7abbbc8ae..be83629ca 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -25,7 +25,7 @@ Singleton { // Build command array const args = ["caelestia", "record", "--mode", videoMode]; - if (audioMode && audioMode !== "none") { + if (audioMode) { args.push("--audio", audioMode); } From e5bf3e787a25d062cd15e945065024c82600c90f Mon Sep 17 00:00:00 2001 From: MateusAquino Date: Tue, 6 Jan 2026 15:17:37 -0300 Subject: [PATCH 073/409] feat: add persisted audio sources dropdown --- config/Config.qml | 5 + config/UtilitiesConfig.qml | 7 + modules/utilities/Wrapper.qml | 1 + modules/utilities/cards/Record.qml | 238 +++++++++++++++++++---------- 4 files changed, 169 insertions(+), 82 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index b32ebda76..191c85562 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -404,6 +404,11 @@ Singleton { vpn: { enabled: utilities.vpn.enabled, provider: utilities.vpn.provider + }, + recording: { + videoMode: utilities.recording.videoMode, + recordSystem: utilities.recording.recordSystem, + recordMicrophone: utilities.recording.recordMicrophone } }; } diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index cf4644602..6b845215a 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -7,6 +7,13 @@ JsonObject { property Sizes sizes: Sizes {} property Toasts toasts: Toasts {} property Vpn vpn: Vpn {} + property Recording recording: Recording {} + + component Recording: JsonObject { + property string videoMode: "fullscreen" + property bool recordSystem: false + property bool recordMicrophone: false + } component Sizes: JsonObject { property int width: 430 diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 77178e36e..842a550f9 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -14,6 +14,7 @@ Item { readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false + property bool recordingAudioExpanded: false property string recordingConfirmDelete property string recordingMode diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index c4de7f1b7..7caccc83f 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -21,8 +21,17 @@ StyledRect { property bool actuallyRecording: Recorder.running property string lastError: "" - property string currentVideoMode: "fullscreen" - property string currentAudioMode: "none" + property string currentVideoMode: Config.utilities.recording.videoMode + + // Computed audio mode based on settings + readonly property string currentAudioMode: { + const recordSystem = Config.utilities.recording.recordSystem; + const recordMic = Config.utilities.recording.recordMicrophone; + if (recordSystem && recordMic) return "combined"; + if (recordSystem) return "system"; + if (recordMic) return "mic"; + return "none"; + } ColumnLayout { id: layout @@ -88,18 +97,16 @@ StyledRect { SplitButton { disabled: root.actuallyRecording - active: menuItems.find(m => root.props.recordingMode === m.mode) ?? menuItems[0] + active: menuItems.find(m => m.mode === Config.utilities.recording.videoMode) ?? menuItems[0] menu.onItemSelected: item => { - root.props.recordingMode = item.mode; - root.currentVideoMode = item.videoMode; - root.currentAudioMode = item.audioMode; + Config.utilities.recording.videoMode = item.mode; + root.currentVideoMode = item.mode; + Config.save(); } menuItems: [ MenuItem { property string mode: "fullscreen" - property string videoMode: "fullscreen" - property string audioMode: "none" icon: "fullscreen" text: qsTr("Record fullscreen") activeText: qsTr("Fullscreen") @@ -107,8 +114,6 @@ StyledRect { }, MenuItem { property string mode: "region" - property string videoMode: "region" - property string audioMode: "none" icon: "screenshot_region" text: qsTr("Record region") activeText: qsTr("Region") @@ -116,39 +121,10 @@ StyledRect { }, MenuItem { property string mode: "window" - property string videoMode: "window" - property string audioMode: "none" icon: "web_asset" text: qsTr("Record window") activeText: qsTr("Window") onClicked: startRecording() - }, - MenuItem { - property string mode: "mic" - property string videoMode: "fullscreen" - property string audioMode: "mic" - icon: "mic" - text: qsTr("Record fullscreen with mic") - activeText: qsTr("Mic") - onClicked: startRecording() - }, - MenuItem { - property string mode: "system" - property string videoMode: "fullscreen" - property string audioMode: "system" - icon: "volume_up" - text: qsTr("Record fullscreen with system audio") - activeText: qsTr("System") - onClicked: startRecording() - }, - MenuItem { - property string mode: "combined" - property string videoMode: "fullscreen" - property string audioMode: "combined" - icon: "settings_voice" - text: qsTr("Record fullscreen with mic + system") - activeText: qsTr("Combined") - onClicked: startRecording() } ] } @@ -177,6 +153,144 @@ StyledRect { } } + // Audio Sources Section + ColumnLayout { + Layout.fillWidth: true + visible: !root.actuallyRecording + spacing: Appearance.spacing.small + + RowLayout { + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Audio Sources") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { Layout.fillWidth: true } + + IconButton { + icon: root.props.recordingAudioExpanded ? "expand_less" : "expand_more" + type: IconButton.Tonal + font.pointSize: Appearance.font.size.small + onClicked: { + root.props.recordingAudioExpanded = !root.props.recordingAudioExpanded; + } + } + } + + ColumnLayout { + Layout.fillWidth: true + visible: root.props.recordingAudioExpanded + spacing: Appearance.spacing.smaller + + // System Audio (Default Sink) + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledSwitch { + checked: Config.utilities.recording.recordSystem + onToggled: { + Config.utilities.recording.recordSystem = checked; + Config.save(); + } + } + + StyledText { + Layout.preferredWidth: 85 + text: qsTr("System") + font.pointSize: Appearance.font.size.small + elide: Text.ElideRight + } + + StyledSlider { + id: systemVolumeSlider + Layout.fillWidth: true + implicitHeight: 24 + opacity: Config.utilities.recording.recordSystem ? 1.0 : 0.5 + from: 0 + to: 1 + value: Audio.volume + onMoved: { + Audio.setVolume(value); + } + } + + StyledText { + text: Math.round(Audio.volume * 100) + "%" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + Layout.preferredWidth: 40 + } + + IconButton { + icon: Audio.muted ? "volume_off" : "volume_up" + type: Audio.muted ? IconButton.Filled : IconButton.Tonal + font.pointSize: Appearance.font.size.small + onClicked: { + if (Audio.sink?.audio) { + Audio.sink.audio.muted = !Audio.sink.audio.muted; + } + } + } + } + + // Microphone (Default Source) + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + StyledSwitch { + checked: Config.utilities.recording.recordMicrophone + onToggled: { + Config.utilities.recording.recordMicrophone = checked; + Config.save(); + } + } + + StyledText { + Layout.preferredWidth: 85 + text: qsTr("Microphone") + font.pointSize: Appearance.font.size.small + elide: Text.ElideRight + } + + StyledSlider { + id: micVolumeSlider + Layout.fillWidth: true + implicitHeight: 24 + opacity: Config.utilities.recording.recordMicrophone ? 1.0 : 0.5 + from: 0 + to: 1 + value: Audio.sourceVolume + onMoved: { + Audio.setSourceVolume(value); + } + } + + StyledText { + text: Math.round(Audio.sourceVolume * 100) + "%" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + Layout.preferredWidth: 40 + } + + IconButton { + icon: Audio.sourceMuted ? "mic_off" : "mic" + type: Audio.sourceMuted ? IconButton.Filled : IconButton.Tonal + font.pointSize: Appearance.font.size.small + onClicked: { + if (Audio.source?.audio) { + Audio.source.audio.muted = !Audio.source.audio.muted; + } + } + } + } + } + } + Loader { id: listOrControls @@ -340,41 +454,10 @@ StyledRect { // Clear any previous errors root.lastError = ""; - const mode = root.props.recordingMode || "fullscreen"; - - // Map the combined mode to separate video and audio modes - let videoMode = "fullscreen"; - let audioMode = "none"; - - switch(mode) { - case "fullscreen": - videoMode = "fullscreen"; - audioMode = "none"; - break; - case "region": - videoMode = "region"; - audioMode = "none"; - break; - case "window": - videoMode = "window"; - audioMode = "none"; - break; - case "mic": - videoMode = "fullscreen"; - audioMode = "mic"; - break; - case "system": - videoMode = "fullscreen"; - audioMode = "system"; - break; - case "combined": - videoMode = "fullscreen"; - audioMode = "combined"; - break; - } + const videoMode = Config.utilities.recording.videoMode || "fullscreen"; + const audioMode = root.currentAudioMode; root.currentVideoMode = videoMode; - root.currentAudioMode = audioMode; console.log("Starting recording - Video:", videoMode, "Audio:", audioMode); @@ -433,15 +516,6 @@ StyledRect { Component.onCompleted: { // Sync initial state root.actuallyRecording = Recorder.running; - - // Set initial mode if saved - if (root.props.recordingMode) { - const mode = root.props.recordingMode; - const item = menuItems.find(m => m.mode === mode); - if (item) { - root.currentVideoMode = item.videoMode; - root.currentAudioMode = item.audioMode; - } - } + root.currentVideoMode = Config.utilities.recording.videoMode || "fullscreen"; } } From 60de5a129a6ec7ba1d43d97600ee0ca993204616 Mon Sep 17 00:00:00 2001 From: Mateus Date: Tue, 17 Mar 2026 04:26:53 -0300 Subject: [PATCH 074/409] fix: react weather location + nominatim fallback (#1290) --- services/Weather.qml | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/services/Weather.qml b/services/Weather.qml index a3095423b..98e29bbba 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -54,18 +54,35 @@ Singleton { return; } - const [lat, lon] = coords.split(","); - const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; - Requests.get(url, text => { + const [lat, lon] = coords.split(",").map(s => s.trim()); + + const fallbackToBigDataCloud = () => { + const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`; + Requests.get(fallbackUrl, text => { + const geo = JSON.parse(text); + const geoCity = geo.city || geo.locality; + if (geoCity) { + city = geoCity; + cachedCities.set(coords, geoCity); + } else { + city = "Unknown City"; + } + }); + }; + + const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; + Requests.get(nominatimUrl, text => { const geo = JSON.parse(text).features?.[0]?.properties.geocoding; if (geo) { const geoCity = geo.type === "city" ? geo.name : geo.city; - city = geoCity; - cachedCities.set(coords, geoCity); - } else { - city = "Unknown City"; + if (geoCity) { + city = geoCity; + cachedCities.set(coords, geoCity); + return; + } } - }); + fallbackToBigDataCloud(); + }, fallbackToBigDataCloud); } function fetchCoordsFromCity(cityName: string): void { @@ -149,7 +166,7 @@ Singleton { if (!loc || loc.indexOf(",") === -1) return ""; - const [lat, lon] = loc.split(","); + const [lat, lon] = loc.split(",").map(s => s.trim()); const baseUrl = "https://api.open-meteo.com/v1/forecast"; const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; @@ -192,6 +209,13 @@ Singleton { onLocChanged: fetchWeatherData() + Connections { + target: Config.services + function onWeatherLocationChanged(): void { + root.reload(); + } + } + // Refresh current location hourly Timer { interval: 3600000 // 1 hour From 6b61164808a5e5db6ce4c6003134c2054d863b87 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 20:46:55 +1100 Subject: [PATCH 075/409] fix: dash wrong current item Closes #1291 --- modules/dashboard/Content.qml | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index bbb42724c..d0386b70a 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -81,7 +81,10 @@ Item { id: view readonly property int currentIndex: root.state.currentTab - readonly property Item currentItem: row.children[currentIndex] + readonly property Item currentItem: { + repeater.count; // Trigger update on count change + return repeater.itemAt(currentIndex); + } anchors.fill: parent @@ -119,6 +122,8 @@ Item { id: row Repeater { + id: repeater + model: ScriptModel { values: root.dashboardTabs } From 0548365a8c24f122f1e9486e152e4d1f8453b0a4 Mon Sep 17 00:00:00 2001 From: Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:47:52 +0800 Subject: [PATCH 076/409] activewindow: fix compact title binding loop (#1302) --- modules/bar/components/ActiveWindow.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 414c9c579..990a65a6b 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -19,7 +19,7 @@ Item { return qsTr("Desktop"); if (Config.bar.activeWindow.compact) { // " - " (standard hyphen), " — " (em dash), " – " (en dash) - const parts = root.windowTitle.split(/\s+[\-\u2013\u2014]\s+/); + const parts = title.split(/\s+[\-\u2013\u2014]\s+/); if (parts.length > 1) return parts[parts.length - 1].trim(); } From 70d6225186dab0a21b363b402df2f19b632dad3a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:36:48 +1100 Subject: [PATCH 077/409] feat: allow hover drawers when border < 2 Closes #1297 --- config/BorderConfig.qml | 3 +++ modules/bar/BarWrapper.qml | 1 + .../appearance/sections/BorderSection.qml | 2 +- modules/drawers/Drawers.qml | 8 ++++---- modules/drawers/Interactions.qml | 16 ++++++++-------- 5 files changed, 17 insertions(+), 13 deletions(-) diff --git a/config/BorderConfig.qml b/config/BorderConfig.qml index b15811fdd..b203925d1 100644 --- a/config/BorderConfig.qml +++ b/config/BorderConfig.qml @@ -3,4 +3,7 @@ import Quickshell.Io JsonObject { property int thickness: Appearance.padding.normal property int rounding: Appearance.rounding.large + + readonly property int minThickness: 2 + readonly property int clampedThickness: Math.max(minThickness, thickness) } diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 29961b62c..5fddf5cd3 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -14,6 +14,7 @@ Item { required property BarPopouts.Wrapper popouts required property bool disabled + readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index 9532d70d6..a259f934e 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -50,7 +50,7 @@ CollapsibleSection { label: qsTr("Border thickness") value: rootPane.borderThickness - from: 0.1 + from: 0 to: 100 decimals: 1 suffix: "px" diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 86c9e99d2..302353bef 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -57,10 +57,10 @@ Variants { WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: Region { - x: bar.implicitWidth + win.dragMaskPadding - y: Config.border.thickness + win.dragMaskPadding - width: win.width - bar.implicitWidth - Config.border.thickness - win.dragMaskPadding * 2 - height: win.height - Config.border.thickness * 2 - win.dragMaskPadding * 2 + x: bar.clampedWidth + win.dragMaskPadding + y: Config.border.clampedThickness + win.dragMaskPadding + width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 + height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 intersection: Intersection.Xor regions: regions.instances diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 9579b15ae..10807fcef 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -33,15 +33,15 @@ CustomMouseArea { } function inRightPanel(panel: Item, x: real, y: real): bool { - return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y); + return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); } function inTopPanel(panel: Item, x: real, y: real): bool { - return y < Config.border.thickness + panel.y + panel.height && withinPanelWidth(panel, x, y); + return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y); } - function inBottomPanel(panel: Item, x: real, y: real): bool { - return y > root.height - Config.border.thickness - panel.height - Config.border.rounding && withinPanelWidth(panel, x, y); + function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { + return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); } function onWheel(event: WheelEvent): void { @@ -88,11 +88,11 @@ CustomMouseArea { const dragY = y - dragStart.y; // Show bar in non-exclusive mode on hover - if (!visibilities.bar && Config.bar.showOnHover && x < bar.implicitWidth) + if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) bar.isHovered = true; // Show/hide bar on drag - if (pressed && dragStart.x < bar.implicitWidth) { + if (pressed && dragStart.x < bar.clampedWidth) { if (dragX > Config.bar.dragThreshold) visibilities.bar = true; else if (dragX < -Config.bar.dragThreshold) @@ -113,7 +113,7 @@ CustomMouseArea { root.panels.osd.hovered = true; } - const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; + const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); // Show/hide session on drag if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { @@ -188,7 +188,7 @@ CustomMouseArea { } // Show utilities on hover - const showUtilities = inBottomPanel(panels.utilities, x, y); + const showUtilities = inBottomPanel(panels.utilities, x, y, true); // Always update visibility based on hover if not in shortcut mode if (!utilitiesShortcutActive) { From ffe90bd07736718b968b303badcfa685511f19e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=A5=88=E8=A6=8B=20=E3=83=AC=E3=82=A4?= Date: Thu, 19 Mar 2026 17:08:36 +0530 Subject: [PATCH 078/409] dashboard: add synced lyrics to media tab (#1197) * lyrics: rewrite model * lyrics: cleanup code * lyrics: some minor layouting * lyrics: add fade shaders * lyrics: use appearance values from config * lyrics: formatting * lyrics: update readme * lyrics: fix jumping, redo netease backend (resused from another one of projects) * lyrics: fix netease searches, remove credit lines * lyrics: add RequestIds * lyrics: formatting * lyrics: lyricsDir path cleanup * lyrics: make empty lines not take up much space * lyrics: make empty lines not take up any space * Lyrics: Use QNetworkAccessManager instead of XMLHttpRequest also added ability to set request headers and ability to reset cookies for requests.cpp * lyrics: cleanup old code * lyrics: toggle lyrics by clicking the album art * lyrics: sanitize non-breaking spaces * lyrics: add a menu to select lyrics and manually search for them * lyrics: remove LrcLib backend * lyrics: change focus to when dashboard is opened instead of the lyricMenu * lyrics: improve UI * lyrics: improve UI more ig * lyrics: extract LyricsView and LyricMenu into separate components * lyrics: cleanup old files * lyrics: refactor LyricsService * lyrics: restore ComponentBehavior: Bound in lyrics components * lyrics: change SongID to real to fix integer overflow * lyrics: add animations to the lyricMenu * lyrics: fix manual search fields not auto updating * lyrics: fix jerks when switching tracks * lyrics: create lyrics directory if it doesn't exist * lyrics: silence logs, fix binding loops, fix index out of range errors * lyrics: only request keyboard focus when lyric menu is open * config: add option to enable/disable the lyrics * format + silence fileview errors * fix merge * anim and lsp fixes + format --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- README.md | 3 +- assets/shaders/fade.frag | 26 +++ assets/shaders/fade.frag.qsb | Bin 0 -> 1659 bytes config/Config.qml | 4 +- config/ServiceConfig.qml | 1 + config/UserPaths.qml | 1 + modules/dashboard/Content.qml | 11 +- modules/dashboard/LyricMenu.qml | 323 +++++++++++++++++++++++++++ modules/dashboard/LyricsView.qml | 114 ++++++++++ modules/dashboard/Media.qml | 220 ++++++++++++++----- modules/dashboard/MediaWrapper.qml | 13 ++ modules/dashboard/Wrapper.qml | 1 + modules/drawers/Drawers.qml | 2 +- plugin/src/Caelestia/requests.cpp | 22 +- plugin/src/Caelestia/requests.hpp | 3 +- services/LyricsService.qml | 340 +++++++++++++++++++++++++++++ utils/scripts/lrcparser.js | 62 ++++++ 17 files changed, 1082 insertions(+), 64 deletions(-) create mode 100644 assets/shaders/fade.frag create mode 100644 assets/shaders/fade.frag.qsb create mode 100644 modules/dashboard/LyricMenu.qml create mode 100644 modules/dashboard/LyricsView.qml create mode 100644 modules/dashboard/MediaWrapper.qml create mode 100644 services/LyricsService.qml create mode 100644 utils/scripts/lrcparser.js diff --git a/README.md b/README.md index e3bebcd7e..385ded162 100644 --- a/README.md +++ b/README.md @@ -598,7 +598,8 @@ default, you must create it manually. "paths": { "mediaGif": "root:/assets/bongocat.gif", "sessionGif": "root:/assets/kurukuru.gif", - "wallpaperDir": "~/Pictures/Wallpapers" + "wallpaperDir": "~/Pictures/Wallpapers", + "lyricsDir": "~/Music/lyrics" }, "services": { "audioIncrement": 0.1, diff --git a/assets/shaders/fade.frag b/assets/shaders/fade.frag new file mode 100644 index 000000000..a6cdf70d0 --- /dev/null +++ b/assets/shaders/fade.frag @@ -0,0 +1,26 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float fadeMargin; +}; + +layout(binding = 1) uniform sampler2D source; + +void main() { + vec4 tex = texture(source, qt_TexCoord0); + float factor = 1.0; + float margin = 0.1; + + if (qt_TexCoord0.y < margin) { + factor = qt_TexCoord0.y / margin; + } else if (qt_TexCoord0.y > (1.0 - margin)) { + factor = (1.0 - qt_TexCoord0.y) / margin; + } + + fragColor = tex * factor * qt_Opacity; +} diff --git a/assets/shaders/fade.frag.qsb b/assets/shaders/fade.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..888e4c10654840ca4da53af2441f2396360300b2 GIT binary patch literal 1659 zcmV->288(l02j`9ob6caZ_`#3|1}Q^hVg#0u?roL(h{7K7HFCRYqx;}>O?CxEvh2d zv4gk74z|;^LY*e<%RX)2_P0#?8`yuc?|VqorgQIcdgF#i2YYDZN{M}r&-?z)@l62i z0+0hR1mHLxAx0fbaIl6s1PBnK0TVz50RG^G0gDVAgo_Xsno_wX)s^|@s_wuFx^U%P z9aGZhZA~UC&8ztC!9t7>9@YTtW{I1c^#0`LFsm$}0}D1h#8_vSHo8ncXuy%4 zLNwvQXQlb4De1}VL4*KZgs|ZNC~)TV?4{QZzBV>l`Jb|xkzumG8#^(_+ylr+kGru$ zUO%|Ic()kEjbeF{s*Am{s4A*`7?x?#r)_zZEe-&fl*P8 zhUNG7_D9JReSmrN{4mEtI%CMm>rrX#%Nq%O9KerHo>Y$@hZCF|tudxgYW7a`+L78I zgSVMq<`x|0IEbEP-f`q`ou5&iPV-(|XFIB%IaD~WX|_T14D%?qD#td&u@OD1#dd>b z$=($5@;+%risKh#B#tV)PrI~usZK{y^f9KZ$jSZK%X>jI)%zgV`Xl6}+#!}B{w%W6 z{vp0U!gZ(nV_frF$jf@Z$#zb%ZHoUb<|j1u2kCu}Jl9;w5dUXndzk$VIr)q*eSvd6 zqxl*c;OiacC-gb?^)>R6KF>1ev|NbyD>BkA`M99@C=Bp1%KU`B$UeSeA7fk};{U)o zj85Tg*@N-euie9M>fCe#a25smYS#89l`B0UkG* zFGg0KF!BOlluXm;gpTb=>Y>+s*b#0ku;R)Vw&%wc!-(RrYsX@x>l%*@A%tjKaalZy z>-VfU^wuhpCoK?jp<~%zysmRytKrt`DWC$B#uIlb`O7S?3~tD8rxiIV^#Pt{MWIwQ#ihid^4PD|=b6R(&5WiZ z*o;DbH(T}hL#sJ&#a7K+CM#6wi)?jNT%PXlyj1)5IbjB-lcN3Wt=0A$I*EsvX zt=J|t0S~ce^ITy5o@CC=)>*4l_K-Vk$_VJeC0|T$zC-zF$ zb)8TGVp@bw(~Dv!EUcKxK8C^P^|tkC5Gq8K*UIFg-ov1n79Gp}EO6b(i3@iB+lnPQ zn3QOFi|8g=)1t-oQ3c?ocPhp#G=CUVr<2Zs4Jm5gIpJmNZ5#CEB0&^ zN*eI`vwqZbdoF%>=l+BGTo^=A{f^)1#$37q*2J_}dhg@8g@ya`rb=jljfHNUGNQQ^ z(_-;KeaQ-&PRu4`^N~QkuLD_A^`I(1QB|dos>chSH{9(XINbltiNoFItH6EbZCOvX z<<>1%6LbrlL=;<1r|!{xvblYrs_GKOe)J}hF*X!qfNNzTkc1M_IYm7=tp((SaN=(0 zrwnZ9Q-^-$CVw5!uibA)Jl}408;+Q1JF(TOM{%R&t;`ym{K#uJF4I@>;jCeFBRW_6 zR@;d>mhA|sHR#men^73(qh9?Z0kPK3-(pY*pFD#M!F+ zSC5}w+S2o;PLOKO=N<^OmJL*=Qmf|Ww(rat` F26KRpUEcrz literal 0 HcmV?d00001 diff --git a/config/Config.qml b/config/Config.qml index 2fd8c436f..1fdfa4c0a 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -360,13 +360,15 @@ Singleton { maxVolume: services.maxVolume, smartScheme: services.smartScheme, defaultPlayer: services.defaultPlayer, - playerAliases: services.playerAliases + playerAliases: services.playerAliases, + showLyrics: services.showLyrics }; } function serializePaths(): var { return { wallpaperDir: paths.wallpaperDir, + lyricsDir: paths.lyricsDir, sessionGif: paths.sessionGif, mediaGif: paths.mediaGif }; diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml index 29600cc54..5294fb691 100644 --- a/config/ServiceConfig.qml +++ b/config/ServiceConfig.qml @@ -19,4 +19,5 @@ JsonObject { "to": "YT Music" } ] + property bool showLyrics: true } diff --git a/config/UserPaths.qml b/config/UserPaths.qml index f8de26782..ea4bf4597 100644 --- a/config/UserPaths.qml +++ b/config/UserPaths.qml @@ -3,6 +3,7 @@ import Quickshell.Io JsonObject { property string wallpaperDir: `${Paths.pictures}/Wallpapers` + property string lyricsDir: `${Paths.home}/Music/lyrics/` property string sessionGif: "root:/assets/kurukuru.gif" property string mediaGif: "root:/assets/bongocat.gif" } diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index d0386b70a..34e4f77a2 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -12,6 +12,15 @@ Item { id: root required property PersistentProperties visibilities + readonly property bool needsKeyboard: { + const count = repeater.count; + for (let i = 0; i < count; i++) { + const item = repeater.itemAt(i) as Loader; + if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) + return true; + } + return false; + } required property PersistentProperties state required property FileDialog facePicker @@ -160,7 +169,7 @@ Item { Component { id: mediaComponent - Media { + MediaWrapper { visibilities: root.visibilities } } diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml new file mode 100644 index 000000000..0add06bb2 --- /dev/null +++ b/modules/dashboard/LyricMenu.qml @@ -0,0 +1,323 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.controls +import qs.services +import qs.config +import QtQuick +import QtQuick.Layouts + +StyledRect { + id: root + + required property real contentHeight + + implicitHeight: contentHeight + + radius: Appearance.rounding.large + color: Colours.tPalette.m3surfaceContainer + + function searchCandidates(title, artist) { + LyricsService.currentRequestId++; + LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); + } + + Loader { + anchors.fill: parent + active: root.height > 0 + + sourceComponent: ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.normal + + // Header: icon, backend name, refresh, toggle + RowLayout { + Layout.fillWidth: true + spacing: Appearance.padding.small + + MaterialIcon { + text: "lyrics" + fill: 1 + color: Colours.palette.m3primary + font.pointSize: Appearance.spacing.large + } + + StyledText { + Layout.fillWidth: true + text: LyricsService.backend + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3secondary + elide: Text.ElideRight + } + + IconButton { + icon: "refresh" + type: IconButton.Text + onClicked: LyricsService.loadLyrics() + } + + StyledSwitch { + checked: LyricsService.lyricsVisible + onToggled: LyricsService.toggleVisibility() + } + } + + StyledText { + Layout.fillWidth: true + text: "Fetched Candidates:" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + elide: Text.ElideRight + } + + // Candidates list + ListView { + id: candidatesView + + Layout.fillWidth: true + Layout.fillHeight: true + + visible: LyricsService.candidatesModel.count > 0 + model: LyricsService.candidatesModel + clip: true + spacing: Appearance.spacing.small + + opacity: visible ? 1 : 0 + // Behavior on opacity { + // NumberAnimation { duration: Appearance.anim.durations.normal } + // } + + delegate: Item { + id: delegateRoot + width: ListView.view.width * 0.98 + height: 70 + anchors.horizontalCenter: parent?.horizontalCenter + + required property real id + required property string title + required property string artist + + property bool hovered: false + property bool pressed: false + + scale: hovered ? 1.02 : 1.0 + Behavior on scale { + NumberAnimation { + duration: Appearance.anim.durations.small + easing.type: Easing.OutCubic + } + } + + Rectangle { + id: background + anchors.fill: parent + radius: Appearance.rounding.small + + color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) + + border.width: delegateRoot.hovered ? 1 : 0 + border.color: Colours.palette.m3primary + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.small + } + } + Behavior on border.width { + NumberAnimation { + duration: Appearance.anim.durations.small + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: delegateRoot.hovered = true + onExited: delegateRoot.hovered = false + onPressed: delegateRoot.pressed = true + onReleased: delegateRoot.pressed = false + onClicked: LyricsService.selectCandidate(delegateRoot.id) + } + + Row { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + // Active indicator bar + Rectangle { + width: 4 + height: parent.height * 0.6 + radius: 2 + anchors.verticalCenter: parent.verticalCenter + color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.small + } + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 30 + spacing: 4 + + Text { + text: delegateRoot.title + font.pointSize: Appearance.font.size.normal + font.bold: true + color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface + width: parent.width + elide: Text.ElideRight + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.small + } + } + } + + Text { + text: delegateRoot.artist + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + elide: Text.ElideRight + } + } + } + } + } + + Item { + Layout.fillHeight: true + visible: LyricsService.candidatesModel.count == 0 + } + + // Manual search + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.padding.small + + StyledText { + Layout.fillWidth: true + text: "Manual Search" + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + elide: Text.ElideRight + } + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.padding.small + + StyledInputField { + id: searchTitle + Layout.fillWidth: true + horizontalAlignment: TextInput.AlignLeft + + Binding { + target: searchTitle + property: "text" + value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") + } + } + + StyledInputField { + id: searchArtist + Layout.fillWidth: true + horizontalAlignment: TextInput.AlignLeft + + Binding { + target: searchArtist + property: "text" + value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") + } + } + + IconButton { + icon: "search" + onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) + } + } + } + + // Offset controls + RowLayout { + Layout.fillWidth: true + spacing: Appearance.padding.small + + MaterialIcon { + text: "contrast_square" + font.pointSize: Appearance.font.size.large + color: Colours.palette.m3secondary + } + + StyledText { + text: "Offset" + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + IconButton { + icon: "remove" + type: IconButton.Text + onClicked: { + LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); + LyricsService.savePrefs(); + } + } + + TextInput { + id: offsetInput + horizontalAlignment: TextInput.AlignHCenter + color: Colours.palette.m3secondary + font.pointSize: Appearance.font.size.normal + selectByMouse: true + text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" + + Binding { + target: offsetInput + property: "text" + value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" + when: !offsetInput.activeFocus + } + + Connections { + target: LyricsService + function onCurrentRequestIdChanged() { + offsetInput.focus = false; + } + } + + onEditingFinished: { + let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); + let val = parseFloat(cleaned); + if (!isNaN(val)) { + LyricsService.offset = parseFloat(val.toFixed(1)); + LyricsService.savePrefs(); + } else { + offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; + } + } + } + + IconButton { + icon: "add" + type: IconButton.Text + onClicked: { + LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); + LyricsService.savePrefs(); + } + } + } + } + } +} diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml new file mode 100644 index 000000000..bce1b23a0 --- /dev/null +++ b/modules/dashboard/LyricsView.qml @@ -0,0 +1,114 @@ +import qs.components +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Effects + +StyledListView { + id: root + + readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 + + clip: true + model: LyricsService.model + currentIndex: LyricsService.currentIndex + + visible: lyricsActuallyVisible || hideTimer.running + + onLyricsActuallyVisibleChanged: { + if (!lyricsActuallyVisible) + hideTimer.restart(); + } + + Timer { + id: hideTimer + interval: 300 // long enough to bridge the track switch gap + running: false + repeat: false + } + + preferredHighlightBegin: height / 2 - 30 + preferredHighlightEnd: height / 2 + 30 + highlightRangeMode: ListView.ApplyRange + highlightFollowsCurrentItem: true + highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Appearance.anim.durations.normal + + layer.enabled: true + layer.effect: ShaderEffect { + required property Item source + property real fadeMargin: 0.5 + fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") + } + + onModelChanged: { + if (model && model.count > 0) { + Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); + } + } + + delegate: Item { + id: delegateRoot + width: ListView.view.width + + required property string lyricLine + required property real time + required property int index + + readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 + height: hasContent ? (lyricText.contentHeight + Appearance.spacing.large) : 0 + + property bool isCurrent: ListView.isCurrentItem + + MultiEffect { + id: effect + anchors.fill: lyricText + source: lyricText + scale: lyricText.scale + enabled: delegateRoot.isCurrent + visible: delegateRoot.isCurrent + + blurEnabled: true + blur: 0.4 + + shadowEnabled: true + shadowColor: Colours.palette.m3primary + shadowOpacity: 0.5 + shadowBlur: 0.6 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 0 + + autoPaddingEnabled: true + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) + } + + Text { + id: lyricText + text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" + width: parent.width * 0.85 + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + font.pointSize: Appearance.font.size.normal + color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font.bold: delegateRoot.isCurrent + scale: delegateRoot.isCurrent ? 1.15 : 1.0 + Behavior on color { + CAnim { + duration: Appearance.anim.durations.small + } + } + Behavior on scale { + Anim { + duration: Appearance.anim.durations.small + } + } + } + } +} diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 722bc9332..ec92743ca 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -16,6 +16,14 @@ Item { id: root required property PersistentProperties visibilities + readonly property bool needsKeyboard: lyricMenuOpen + + readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 + readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight + + property bool lyricMenuOpen: false + property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 + property bool lyricsShowingDebounced: false property real playerProgress: { const active = Players.active; @@ -35,8 +43,21 @@ Item { return `${mins}:${secs}`; } + onLyricsShowingChanged: { + if (lyricsShowing) { + lyricsHideDelay.stop(); + lyricsShowingDebounced = true; + } else { + lyricsHideDelay.restart(); + } + } + implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 - implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 + implicitHeight: nonAnimHeight + + Behavior on implicitHeight { + Anim {} + } Behavior on playerProgress { Anim { @@ -49,7 +70,25 @@ Item { interval: Config.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true - onTriggered: Players.active?.positionChanged() + onTriggered: { + if (!Players.active) + return; + LyricsService.updatePosition(); + Players.active?.positionChanged(); + } + } + + Timer { + id: lyricsHideDelay + interval: 300 + repeat: false + } + + Connections { + target: lyricsHideDelay + function onTriggered() { + root.lyricsShowingDebounced = false; + } } ServiceRef { @@ -145,6 +184,13 @@ Item { fillMode: Image.PreserveAspectCrop sourceSize.width: width sourceSize.height: height + + MouseArea { + anchors.fill: parent + onClicked: { + LyricsService.toggleVisibility(); + } + } } } @@ -200,6 +246,12 @@ Item { wrapMode: Players.active ? Text.NoWrap : Text.WordWrap } + LyricsView { + id: lyricsViewInDetails + Layout.fillWidth: true + Layout.preferredHeight: 200 + } + RowLayout { id: controls @@ -209,6 +261,14 @@ Item { spacing: Appearance.spacing.small + PlayerControl { + type: IconButton.Text + icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" + font.pointSize: Math.round(Appearance.font.size.large) + disabled: !Players.active?.shuffleSupported + onClicked: Players.active.shuffle = !Players.active?.shuffle + } + PlayerControl { type: IconButton.Text icon: "skip_previous" @@ -235,6 +295,13 @@ Item { disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } + + PlayerControl { + type: IconButton.Text + icon: "lyrics" + font.pointSize: Math.round(Appearance.font.size.large) + onClicked: root.lyricMenuOpen = !root.lyricMenuOpen + } } StyledSlider { @@ -299,83 +366,120 @@ Item { font.pointSize: Appearance.font.size.small } } + } - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small + ColumnLayout { + id: leftSection - PlayerControl { - type: IconButton.Text - icon: "move_up" - inactiveOnColour: Colours.palette.m3secondary - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canRaise - onClicked: { - Players.active?.raise(); - root.visibilities.dashboard = false; - } + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 + anchors.left: details.right + anchors.leftMargin: Appearance.spacing.normal + + visible: lyricMenu.height === 0 || opacity > 0 + opacity: lyricMenu.height === 0 ? 1 : 0 + Behavior on opacity { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.OutCubic } + } - SplitButton { - id: playerSelector + Item { + id: bongocat - disabled: !Players.list.length - active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null - menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData + implicitWidth: visualiser.width + implicitHeight: visualiser.height - menuItems: playerList.instances - fallbackIcon: "music_off" - fallbackText: qsTr("No players") + AnimatedImage { + anchors.centerIn: parent - label.Layout.maximumWidth: slider.implicitWidth * 0.28 - label.elide: Text.ElideRight + width: visualiser.width * 0.75 + height: visualiser.height * 0.75 - stateLayer.disabled: true - menuOnTop: true + playing: Players.active?.isPlaying ?? false + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + source: Paths.absolutePath(Config.paths.mediaGif) + asynchronous: true + fillMode: AnimatedImage.PreserveAspectFit + } + } + } - Variants { - id: playerList + LyricMenu { + id: lyricMenu - model: Players.list + anchors.top: parent.top + anchors.left: details.right + anchors.right: parent.right + anchors.leftMargin: Appearance.spacing.normal - PlayerItem {} - } - } + contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Appearance.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight - PlayerControl { - type: IconButton.Text - icon: "delete" - inactiveOnColour: Colours.palette.m3error - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canQuit - onClicked: Players.active?.quit() + visible: root.lyricMenuOpen || height > 0 + height: root.lyricMenuOpen ? implicitHeight : 0 + clip: true + Behavior on height { + NumberAnimation { + duration: Appearance.anim.durations.normal + easing.type: Easing.OutCubic } } } - Item { - id: bongocat + RowLayout { + id: playerChanger + parent: !root.lyricsShowingDebounced ? details : leftSection + Layout.alignment: Qt.AlignHCenter + spacing: Appearance.spacing.small - anchors.verticalCenter: parent.verticalCenter - anchors.left: details.right - anchors.leftMargin: Appearance.spacing.normal + PlayerControl { + type: IconButton.Text + icon: "move_up" + inactiveOnColour: Colours.palette.m3secondary + padding: Appearance.padding.small + font.pointSize: Appearance.font.size.large + disabled: !Players.active?.canRaise + onClicked: { + Players.active?.raise(); + root.visibilities.dashboard = false; + } + } - implicitWidth: visualiser.width - implicitHeight: visualiser.height + SplitButton { + id: playerSelector - AnimatedImage { - anchors.centerIn: parent + disabled: !Players.list.length + active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null + menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData - width: visualiser.width * 0.75 - height: visualiser.height * 0.75 + menuItems: playerList.instances + fallbackIcon: "music_off" + fallbackText: qsTr("No players") - playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type - source: Paths.absolutePath(Config.paths.mediaGif) - asynchronous: true - fillMode: AnimatedImage.PreserveAspectFit + label.Layout.maximumWidth: slider.implicitWidth * 0.28 + label.elide: Text.ElideRight + + stateLayer.disabled: true + menuOnTop: true + + Variants { + id: playerList + + model: Players.list + + PlayerItem {} + } + } + + PlayerControl { + type: IconButton.Text + icon: "delete" + inactiveOnColour: Colours.palette.m3error + padding: Appearance.padding.small + font.pointSize: Appearance.font.size.large + disabled: !Players.active?.canQuit + onClicked: Players.active?.quit() } } diff --git a/modules/dashboard/MediaWrapper.qml b/modules/dashboard/MediaWrapper.qml new file mode 100644 index 000000000..b03a11ee8 --- /dev/null +++ b/modules/dashboard/MediaWrapper.qml @@ -0,0 +1,13 @@ +import QtQuick + +Item { + property alias visibilities: media.visibilities + readonly property alias needsKeyboard: media.needsKeyboard + + implicitWidth: media.implicitWidth + implicitHeight: media.nonAnimHeight + + Media { + id: media + } +} diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 0e37909e8..01eddcc3f 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -12,6 +12,7 @@ Item { id: root required property PersistentProperties visibilities + readonly property bool needsKeyboard: content.item?.needsKeyboard ?? false readonly property PersistentProperties dashState: PersistentProperties { property int currentTab property date currentDate: new Date() diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 302353bef..1423cd21b 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -54,7 +54,7 @@ Variants { screen: scope.modelData name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: Region { x: bar.clampedWidth + win.dragMaskPadding diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp index 2ceddb351..4818507ea 100644 --- a/plugin/src/Caelestia/requests.cpp +++ b/plugin/src/Caelestia/requests.cpp @@ -3,6 +3,8 @@ #include #include #include +#include +#include namespace caelestia { @@ -10,13 +12,27 @@ Requests::Requests(QObject* parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) {} -void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const { +void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { if (!onSuccess.isCallable()) { qWarning() << "Requests::get: onSuccess is not callable"; return; } QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); + request.setRawHeader("Cache-Control", "no-cache, no-store"); + request.setRawHeader("Pragma", "no-cache"); + request.setRawHeader("Connection", "close"); + + if (headers.isObject()) { + QJSValueIterator it(headers); + while (it.hasNext()) { + it.next(); + request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8()); + } + } + auto reply = m_manager->get(request); QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() { @@ -32,4 +48,8 @@ void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const }); } +void Requests::resetCookies() const { + m_manager->setCookieJar(new QNetworkCookieJar(m_manager)); +} + } // namespace caelestia diff --git a/plugin/src/Caelestia/requests.hpp b/plugin/src/Caelestia/requests.hpp index 1db2f4cf6..03c8d723f 100644 --- a/plugin/src/Caelestia/requests.hpp +++ b/plugin/src/Caelestia/requests.hpp @@ -14,7 +14,8 @@ class Requests : public QObject { public: explicit Requests(QObject* parent = nullptr); - Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const; + Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; + Q_INVOKABLE void resetCookies() const; private: QNetworkAccessManager* m_manager; diff --git a/services/LyricsService.qml b/services/LyricsService.qml new file mode 100644 index 000000000..35dba4681 --- /dev/null +++ b/services/LyricsService.qml @@ -0,0 +1,340 @@ +pragma Singleton + +import qs.config +import qs.utils +import Caelestia +import QtQuick +import Quickshell +import Quickshell.Io +import "../utils/scripts/lrcparser.js" as Lrc + +Singleton { + id: root + + property var player: Players.active + property int currentIndex: -1 + property bool loading: false + property bool isManualSeeking: false + property bool lyricsVisible: Config.services.showLyrics + property string backend: "Local" + property real currentSongId: 0 + + property real offset + + readonly property string lyricsDir: Paths.absolutePath(Config.paths.lyricsDir) + readonly property string lyricsMapFile: Paths.absolutePath(Config.paths.lyricsDir) + "/lyrics_map.json" + + property int currentRequestId: 0 + + // The data source for the UI + readonly property alias model: lyricsModel + readonly property alias candidatesModel: fetchedCandidatesModel + + property var lyricsMap: ({}) + + ListModel { + id: lyricsModel + } + ListModel { + id: fetchedCandidatesModel + } + + Timer { + id: seekTimer + interval: 500 + onTriggered: root.isManualSeeking = false + } + + // If no local lyrics were loaded within the interval, fall back to NetEase + Timer { + id: fallbackTimer + interval: 200 + onTriggered: { + if (lyricsModel.count === 0) { + root.backend = "NetEase"; + fallbackToOnline(); + } + } + } + + Timer { + id: loadDebounce + interval: 50 + onTriggered: root._doLoadLyrics() + } + + FileView { + id: lyricsMapFileView + path: root.lyricsMapFile + printErrors: false + onLoaded: { + try { + root.lyricsMap = JSON.parse(text()); + } catch (e) { + root.lyricsMap = {}; + } + } + } + + FileView { + id: lrcFile + printErrors: false + onLoaded: { + fallbackTimer.stop(); + let parsed = Lrc.parseLrc(text()); + if (parsed.length > 0) { + root.backend = "Local"; + updateModel(parsed); + loading = false; + } else { + root.backend = "NetEase"; + fallbackToOnline(); + } + } + } + + Connections { + target: Players + function onActiveChanged() { + root.player = Players.active; + loadLyrics(); + } + } + + Connections { + target: root.player + ignoreUnknownSignals: true + function onMetadataChanged() { + loadLyrics(); + } + } + + Process { + id: saveLyricsMap + command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] + } + + function getMetadata() { + if (!player || !player.metadata) + return null; + let artist = player.metadata["xesam:artist"]; + const title = player.metadata["xesam:title"]; + if (Array.isArray(artist)) + artist = artist.join(", "); + return { + artist: artist || "Unknown", + title: title || "Unknown" + }; + } + + function _metaKey(meta) { + return `${meta.artist} - ${meta.title}`; + } + + function savePrefs() { + let meta = getMetadata(); + if (!meta) + return; + let key = _metaKey(meta); + let existing = root.lyricsMap[key] ?? {}; + root.lyricsMap[key] = { + offset: root.offset, + backend: root.backend, + neteaseId: existing.neteaseId ?? null + }; + // reassign to notify QML bindings of the map change + root.lyricsMap = root.lyricsMap; + saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; + saveLyricsMap.running = true; + } + + function toggleVisibility() { + Config.services.showLyrics = !Config.services.showLyrics; + Config.save(); + } + + function loadLyrics() { + loadDebounce.restart(); + } + + function _doLoadLyrics() { + const meta = getMetadata(); + if (!meta) + return; + + loading = true; + lyricsModel.clear(); + currentIndex = -1; + root.currentSongId = 0; + root.backend = "Local"; + + root.currentRequestId++; + let requestId = root.currentRequestId; + + let key = _metaKey(meta); + let saved = root.lyricsMap[key]; + root.offset = saved?.offset ?? 0.0; + + if (saved?.neteaseId && saved?.backend === "NetEase") { + root.backend = "NetEase"; + root.currentSongId = saved.neteaseId; + fetchNetEaseLyrics(saved.neteaseId, requestId); + fetchNetEaseCandidates(meta.title, meta.artist, requestId); + return; + } + + if (saved?.backend === "NetEase") { + fallbackTimer.restart(); + return; + } + + let cleanDir = lyricsDir.replace(/\/$/, ""); + let fullPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; + + lrcFile.path = ""; + lrcFile.path = fullPath; + fetchNetEaseCandidates(meta.title, meta.artist, requestId); //to populate the list regardless + + // if the file is missing, FileView will not fire onLoaded, so we arm the fallback timer here as a safety net. It is cancelled in onLoaded if the file loads successfully. + if (saved?.backend !== "Local") + fallbackTimer.restart(); + } + + function updateModel(parsedArray) { + root.currentIndex = -1; + lyricsModel.clear(); + for (let line of parsedArray) { + lyricsModel.append({ + time: line.time, + lyricLine: line.text + }); + } + } + + function fallbackToOnline() { + let meta = getMetadata(); + if (!meta) + return; + fetchNetEase(meta.title, meta.artist, root.currentRequestId); + } + + // NetEase + + // shared headers for all NetEase requests + readonly property var _netEaseHeaders: ({ + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", + "Referer": "https://music.163.com/" + }) + + // searches NetEase and populates the candidates model. returns the result array via the onResults callback + function _searchNetEase(title, artist, reqId, onResults) { + Requests.resetCookies(); + const query = encodeURIComponent(`${title} ${artist}`); + const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; + + Requests.get(url, text => { + if (reqId !== root.currentRequestId) + return; + const res = JSON.parse(text); + const songs = res.result?.songs || []; + + fetchedCandidatesModel.clear(); + for (let s of songs) { + fetchedCandidatesModel.append({ + id: s.id, + title: s.name || "Unknown Title", + artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" + }); + } + + onResults(songs); + }, err => {}, root._netEaseHeaders); + } + + // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. + function fetchNetEaseCandidates(title, artist, reqId) { + _searchNetEase(title, artist, reqId, _songs => {}); + } + + // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. + function fetchNetEase(title, artist, reqId) { + _searchNetEase(title, artist, reqId, songs => { + const bestMatch = songs.find(s => { + const inputArtist = String(artist || "").toLowerCase(); + const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); + return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); + }); + + if (!bestMatch) { + return; // No reliable lyrics found + } + + let key = `${artist} - ${title}`; + root.lyricsMap[key] = { + offset: root.lyricsMap[key]?.offset ?? 0.0, + backend: "NetEase", + neteaseId: bestMatch.id + }; + root.currentSongId = bestMatch.id; + savePrefs(); + fetchNetEaseLyrics(bestMatch.id, reqId); + }); + } + + function fetchNetEaseLyrics(id, reqId) { + const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; + Requests.get(url, text => { + if (reqId !== root.currentRequestId) + return; + const res = JSON.parse(text); + if (res.lrc?.lyric) { + updateModel(Lrc.parseLrc(res.lrc.lyric)); + loading = false; + } + }); + } + + function selectCandidate(songId) { + let meta = getMetadata(); + if (!meta) + return; + root.backend = "NetEase"; + root.currentSongId = songId; + let key = _metaKey(meta); + root.lyricsMap[key] = { + offset: root.lyricsMap[key]?.offset ?? 0.0, + neteaseId: songId + }; + savePrefs(); + fetchNetEaseLyrics(songId, currentRequestId); + } + + function updatePosition() { + if (isManualSeeking || loading || !player || lyricsModel.count === 0) + return; + + let pos = player.position - root.offset; + let newIdx = -1; + for (let i = lyricsModel.count - 1; i >= 0; i--) { + if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor + newIdx = i; + break; + } + } + + if (newIdx !== currentIndex) { + root.currentIndex = newIdx; + } + } + + function jumpTo(index, time) { + root.isManualSeeking = true; + root.currentIndex = index; + + if (player) { + player.position = time + root.offset + 0.01; // compensate for rounding + } + + seekTimer.restart(); + } +} diff --git a/utils/scripts/lrcparser.js b/utils/scripts/lrcparser.js new file mode 100644 index 000000000..847779eda --- /dev/null +++ b/utils/scripts/lrcparser.js @@ -0,0 +1,62 @@ +function parseLrc(text) { + if (!text) return []; + let lines = text.split("\n"); + let result = []; + + let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; + + // Blacklist for credits/metadata often found in NetEase lyrics + const creditKeywords = [ + "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" + ]; + + for (let line of lines) { + + timeRegex.lastIndex = 0; + let matches = []; + let match; + + while ((match = timeRegex.exec(line)) !== null) { + matches.push(match); + } + + if (matches.length === 0) continue; + + let lyric = line.replace(timeRegex, "").trim(); + + let min = parseInt(matches[0][1]); + let sec = parseFloat(matches[0][2]); + let totalTime = min * 60 + sec; + + // Only filter credits if they appear in the first 20 seconds + if (totalTime < 20) { + let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); + if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { + continue; + } + } + + for (let match of matches) { + let min = parseInt(match[1]); + let sec = parseFloat(match[2]); + + result.push({ + time: min * 60 + sec, + text: lyric + }); + } + } + + result.sort((a, b) => a.time - b.time); + return result; +} + +function getCurrentLine(lyrics, position) { + const epsilon = 0.1; // 100ms tolerance + for (let i = lyrics.length - 1; i >= 0; i--) { + if ((position + epsilon) >= lyrics[i].time) { + return i; + } + } + return -1; +} From 7fbcf178997a9cd7b49f36f5054e8617cacaf75e Mon Sep 17 00:00:00 2001 From: Ezekiel Gonzales <141341590+notsoeazy@users.noreply.github.com> Date: Thu, 19 Mar 2026 19:39:35 +0800 Subject: [PATCH 079/409] bar: add date and background option for clock (#1198) * bar: add date and background option for clock * fix height binding loop qs WARN --- README.md | 2 + config/BarConfig.qml | 2 + config/Config.qml | 2 + modules/bar/components/Clock.qml | 67 ++++++++++++++----- modules/controlcenter/taskbar/TaskbarPane.qml | 22 ++++++ 5 files changed, 77 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 385ded162..0c96b29fa 100644 --- a/README.md +++ b/README.md @@ -337,6 +337,8 @@ default, you must create it manually. "showOnHover": true }, "clock": { + "background": false, + "showDate": false, "showIcon": true }, "dragThreshold": 20, diff --git a/config/BarConfig.qml b/config/BarConfig.qml index 310344b11..d3e892aa4 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -113,6 +113,8 @@ JsonObject { } component Clock: JsonObject { + property bool background: false + property bool showDate: true property bool showIcon: true } diff --git a/config/Config.qml b/config/Config.qml index 1fdfa4c0a..a8eb0a4b7 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -202,6 +202,8 @@ Singleton { showLockStatus: bar.status.showLockStatus }, clock: { + background: bar.clock.background, + showDate: bar.clock.showDate, showIcon: bar.clock.showIcon }, entries: bar.entries, diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 801e93d77..089bc5a82 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -5,34 +5,65 @@ import qs.services import qs.config import QtQuick -Column { +StyledRect { id: root - property color colour: Colours.palette.m3tertiary + readonly property color colour: Colours.palette.m3tertiary + readonly property int padding: Config.bar.clock.background ? Appearance.padding.normal : Appearance.padding.small - spacing: Appearance.spacing.small + implicitWidth: Config.bar.sizes.innerWidth + implicitHeight: layout.implicitHeight + root.padding * 2 - Loader { - anchors.horizontalCenter: parent.horizontalCenter + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Appearance.rounding.full - active: Config.bar.clock.showIcon - visible: active + Column { + id: layout + anchors.centerIn: parent + spacing: Appearance.spacing.small - sourceComponent: MaterialIcon { - text: "calendar_month" + Loader { + anchors.horizontalCenter: parent.horizontalCenter + + active: Config.bar.clock.showIcon + visible: active + + sourceComponent: MaterialIcon { + text: "calendar_month" + color: root.colour + } + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + visible: Config.bar.clock.showDate + + horizontalAlignment: StyledText.AlignHCenter + text: Time.format("ddd\nd") + font.pointSize: Appearance.font.size.smaller + font.family: Appearance.font.family.sans color: root.colour } - } - StyledText { - id: text + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + visible: Config.bar.clock.showDate + height: visible ? 1 : 0 + + width: parent.width * 0.8 + color: root.colour + opacity: 0.2 + } - anchors.horizontalCenter: parent.horizontalCenter + StyledText { + anchors.horizontalCenter: parent.horizontalCenter - horizontalAlignment: StyledText.AlignHCenter - text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.mono - color: root.colour + horizontalAlignment: StyledText.AlignHCenter + text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") + font.pointSize: Appearance.font.size.smaller + font.family: Appearance.font.family.mono + color: root.colour + } } } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index ba65c1e74..c23904b80 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -22,6 +22,8 @@ Item { property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false property bool clockShowIcon: Config.bar.clock.showIcon ?? true + property bool clockBackground: Config.bar.clock.background ?? false + property bool clockShowDate: Config.bar.clock.showDate ?? false property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true property int dragThreshold: Config.bar.dragThreshold ?? 20 @@ -69,6 +71,8 @@ Item { function saveConfig(entryIndex, entryEnabled) { Config.bar.activeWindow.compact = root.activeWindowCompact; Config.bar.activeWindow.inverted = root.activeWindowInverted; + Config.bar.clock.background = root.clockBackground; + Config.bar.clock.showDate = root.clockShowDate; Config.bar.clock.showIcon = root.clockShowIcon; Config.bar.persistent = root.persistent; Config.bar.showOnHover = root.showOnHover; @@ -537,6 +541,24 @@ Item { font.pointSize: Appearance.font.size.normal } + SwitchRow { + label: qsTr("Background") + checked: root.clockBackground + onToggled: checked => { + root.clockBackground = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Show date") + checked: root.clockShowDate + onToggled: checked => { + root.clockShowDate = checked; + root.saveConfig(); + } + } + SwitchRow { label: qsTr("Show clock icon") checked: root.clockShowIcon From 3e057c829dcfadf10374bd648b8894ffacf1765c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:40:25 +1100 Subject: [PATCH 080/409] bar: dont show date by default --- config/BarConfig.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/BarConfig.qml b/config/BarConfig.qml index d3e892aa4..463302ab5 100644 --- a/config/BarConfig.qml +++ b/config/BarConfig.qml @@ -114,7 +114,7 @@ JsonObject { component Clock: JsonObject { property bool background: false - property bool showDate: true + property bool showDate: false property bool showIcon: true } From ab91536059b9bae31c829f67be0d5f9a72ea7c57 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:46:41 +1100 Subject: [PATCH 081/409] fix: prevent changing material font to non material Also swap places with sans font Closes #1224 --- .../appearance/sections/FontsSection.qml | 44 +++++++++---------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 3988863af..1466746db 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -19,28 +19,27 @@ CollapsibleSection { showBackground: true CollapsibleSection { - id: materialFontSection - title: qsTr("Material font family") + id: sansFontSection + title: qsTr("Sans-serif font family") expanded: true showBackground: true nested: true Loader { - id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - active: materialFontSection.expanded + active: sansFontSection.expanded sourceComponent: StyledListView { - id: materialFontList - property alias contentHeight: materialFontList.contentHeight + id: sansFontList + property alias contentHeight: sansFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList + flickable: sansFontList } delegate: StyledRect { @@ -49,7 +48,7 @@ CollapsibleSection { width: ListView.view.width - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + readonly property bool isCurrent: modelData === rootPane.fontFamilySans color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 @@ -57,13 +56,13 @@ CollapsibleSection { StateLayer { function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; + rootPane.fontFamilySans = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilyMaterialRow + id: fontFamilySansRow anchors.left: parent.left anchors.right: parent.right @@ -92,7 +91,7 @@ CollapsibleSection { } } - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -178,27 +177,28 @@ CollapsibleSection { } CollapsibleSection { - id: sansFontSection - title: qsTr("Sans-serif font family") + id: materialFontSection + title: qsTr("Material font family") expanded: false showBackground: true nested: true Loader { + id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - active: sansFontSection.expanded + active: materialFontSection.expanded sourceComponent: StyledListView { - id: sansFontList - property alias contentHeight: sansFontList.contentHeight + id: materialFontList + property alias contentHeight: materialFontList.contentHeight clip: true spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList + flickable: materialFontList } delegate: StyledRect { @@ -207,7 +207,7 @@ CollapsibleSection { width: ListView.view.width - readonly property bool isCurrent: modelData === rootPane.fontFamilySans + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 @@ -215,13 +215,13 @@ CollapsibleSection { StateLayer { function onClicked(): void { - rootPane.fontFamilySans = modelData; + rootPane.fontFamilyMaterial = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilySansRow + id: fontFamilyMaterialRow anchors.left: parent.left anchors.right: parent.right @@ -250,7 +250,7 @@ CollapsibleSection { } } - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } } From e2552f604dee9d80f4fcc4b1b522193035b07aa3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:54:16 +1100 Subject: [PATCH 082/409] fix: gate wallpaper placeholder on update Closes #1289 --- modules/background/Wallpaper.qml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 39a48fc8f..b83ff8bec 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -13,6 +13,7 @@ Item { property string source: Wallpapers.current property Image current: one + property bool completed onSourceChanged: { if (!source) @@ -25,13 +26,16 @@ Item { Component.onCompleted: { if (source) - Qt.callLater(() => one.update()); + Qt.callLater(() => { + one.update(); + completed = true; + }); } Loader { anchors.fill: parent - active: !root.source + active: root.completed && !root.source sourceComponent: StyledRect { color: Colours.palette.m3surfaceContainer From c96c9c4038ad3940ff7844fdbe07dc1bd19e0b4c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:57:13 +1100 Subject: [PATCH 083/409] fix: dash currentItem null ref --- modules/dashboard/Content.qml | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 34e4f77a2..fba608fe7 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -99,15 +99,15 @@ Item { flickableDirection: Flickable.HorizontalFlick - implicitWidth: currentItem.implicitWidth - implicitHeight: currentItem.implicitHeight + implicitWidth: currentItem?.implicitWidth ?? 0 + implicitHeight: currentItem?.implicitHeight ?? 0 - contentX: currentItem.x + contentX: currentItem?.x ?? 0 contentWidth: row.implicitWidth contentHeight: row.implicitHeight onContentXChanged: { - if (!moving) + if (!moving || !currentItem) return; const x = contentX - currentItem.x; @@ -118,13 +118,16 @@ Item { } onDragEnded: { + if (!currentItem) + return; + const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 10) root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 10) root.state.currentTab = Math.max(root.state.currentTab - 1, 0); else - contentX = Qt.binding(() => currentItem.x); + contentX = Qt.binding(() => currentItem?.x ?? 0); } RowLayout { From 5e4df661a06eaa2c24b1bde3f525e414fc2d78f5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:59:20 +1100 Subject: [PATCH 084/409] fix: tray menu currentItem maybe null --- modules/bar/popouts/TrayMenu.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 9b743db19..027552d89 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -14,8 +14,8 @@ StackView { required property Item popouts required property QsMenuHandle trayItem - implicitWidth: currentItem.implicitWidth - implicitHeight: currentItem.implicitHeight + implicitWidth: currentItem?.implicitWidth ?? 0 + implicitHeight: currentItem?.implicitHeight ?? 0 initialItem: SubMenu { handle: root.trayItem From bf90d38a237600e236c914897e8096da43006b41 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:59:38 +1100 Subject: [PATCH 085/409] fix: plugin maybe nullptr deletions --- plugin/src/Caelestia/Services/audioprovider.hpp | 2 +- plugin/src/Caelestia/Services/beattracker.cpp | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Services/audioprovider.hpp b/plugin/src/Caelestia/Services/audioprovider.hpp index 5bf9bb00d..4b85929f4 100644 --- a/plugin/src/Caelestia/Services/audioprovider.hpp +++ b/plugin/src/Caelestia/Services/audioprovider.hpp @@ -23,7 +23,7 @@ public slots: virtual void process() = 0; private: - QTimer* m_timer; + QTimer* m_timer = nullptr; }; class AudioProvider : public Service { diff --git a/plugin/src/Caelestia/Services/beattracker.cpp b/plugin/src/Caelestia/Services/beattracker.cpp index 93addc679..649705799 100644 --- a/plugin/src/Caelestia/Services/beattracker.cpp +++ b/plugin/src/Caelestia/Services/beattracker.cpp @@ -19,7 +19,9 @@ BeatProcessor::~BeatProcessor() { if (m_in) { del_fvec(m_in); } - del_fvec(m_out); + if (m_out) { + del_fvec(m_out); + } } void BeatProcessor::process() { From f637bad0d9008dcac736b20cad18e0f6759f7838 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:00:23 +1100 Subject: [PATCH 086/409] fix: sparkline div by 0 --- plugin/src/Caelestia/Internal/sparklineitem.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp index b4938d1d2..5ffcc2f84 100644 --- a/plugin/src/Caelestia/Internal/sparklineitem.cpp +++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp @@ -27,6 +27,9 @@ void SparklineItem::paint(QPainter* painter) { } void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { + if (m_historyLength < 2) + return; + const qreal w = width(); const qreal h = height(); const int len = buffer->count(); From 3c819cac1bfe09c873412d12a05625b923f063fb Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 19 Mar 2026 23:09:30 +1100 Subject: [PATCH 087/409] fix: swapped r and b channels in image analyser --- plugin/src/Caelestia/imageanalyser.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp index 880b0785c..3b3cdf841 100644 --- a/plugin/src/Caelestia/imageanalyser.cpp +++ b/plugin/src/Caelestia/imageanalyser.cpp @@ -191,14 +191,14 @@ void ImageAnalyser::analyse(QPromise& promise, const QImage& imag continue; } - const quint32 mr = static_cast(pixel[0] & 0xF8); + const quint32 mr = static_cast(pixel[2] & 0xF8); const quint32 mg = static_cast(pixel[1] & 0xF8); - const quint32 mb = static_cast(pixel[2] & 0xF8); + const quint32 mb = static_cast(pixel[0] & 0xF8); ++colours[(mr << 16) | (mg << 8) | mb]; - const qreal r = pixel[0] / 255.0; + const qreal r = pixel[2] / 255.0; const qreal g = pixel[1] / 255.0; - const qreal b = pixel[2] / 255.0; + const qreal b = pixel[0] / 255.0; totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); ++count; } From 9d36f5c314862dc11f437822dbc18fee4f857fb9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 00:06:35 +1100 Subject: [PATCH 088/409] feat: async calculator --- modules/launcher/items/CalcItem.qml | 9 ++- plugin/src/Caelestia/qalculator.cpp | 102 ++++++++++++++++++++++++++++ plugin/src/Caelestia/qalculator.hpp | 23 +++++++ 3 files changed, 132 insertions(+), 2 deletions(-) diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 65489d9bc..28a05327a 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -12,8 +12,13 @@ Item { required property var list readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) + onMathChanged: { + if (math.length > 0) + Qalculator.evalAsync(math); + } + function onClicked(): void { - Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); + Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); root.list.visibilities.launcher = false; } @@ -55,7 +60,7 @@ Item { return Colours.palette.m3onSurface; } - text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") + text: root.math.length > 0 ? (Qalculator.result || qsTr("Calculating...")) : qsTr("Type an expression to calculate") elide: Text.ElideLeft Layout.fillWidth: true diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp index 44e8d21e2..bfc977e8f 100644 --- a/plugin/src/Caelestia/qalculator.cpp +++ b/plugin/src/Caelestia/qalculator.cpp @@ -1,9 +1,13 @@ #include "qalculator.hpp" #include +#include +#include namespace caelestia { +QMutex Qalculator::s_calculatorMutex; + Qalculator::Qalculator(QObject* parent) : QObject(parent) { if (!CALCULATOR) { @@ -19,6 +23,8 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString(); } + QMutexLocker locker(&s_calculatorMutex); + EvaluationOptions eo; PrintOptions po; @@ -49,4 +55,100 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString::fromStdString(result); } +void Qalculator::evalAsync(const QString& expr) { + const quint64 gen = ++m_generation; + + if (expr.isEmpty()) { + if (!m_result.isEmpty()) { + m_result.clear(); + emit resultChanged(); + } + if (!m_rawResult.isEmpty()) { + m_rawResult.clear(); + emit rawResultChanged(); + } + if (m_busy) { + m_busy = false; + emit busyChanged(); + } + return; + } + + if (!m_busy) { + m_busy = true; + emit busyChanged(); + } + + const auto future = QtConcurrent::run([expr]() -> QPair { + QMutexLocker locker(&s_calculatorMutex); + + EvaluationOptions eo; + PrintOptions po; + + std::string parsed; + std::string result = CALCULATOR->calculateAndPrint( + CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); + + std::string error; + while (CALCULATOR->message()) { + if (!CALCULATOR->message()->message().empty()) { + if (CALCULATOR->message()->type() == MESSAGE_ERROR) { + error += "error: "; + } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { + error += "warning: "; + } + error += CALCULATOR->message()->message(); + } + CALCULATOR->nextMessage(); + } + + if (!error.empty()) { + const QString errorStr = QString::fromStdString(error); + return { errorStr, errorStr }; + } + + const QString rawStr = QString::fromStdString(result); + return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; + }); + + auto* watcher = new QFutureWatcher>(this); + + connect(watcher, &QFutureWatcher>::finished, this, [this, watcher, gen]() { + watcher->deleteLater(); + + if (gen != m_generation) { + return; + } + + const auto [formatted, raw] = watcher->result(); + + if (m_result != formatted) { + m_result = formatted; + emit resultChanged(); + } + if (m_rawResult != raw) { + m_rawResult = raw; + emit rawResultChanged(); + } + if (m_busy) { + m_busy = false; + emit busyChanged(); + } + }); + + watcher->setFuture(future); +} + +QString Qalculator::result() const { + return m_result; +} + +QString Qalculator::rawResult() const { + return m_rawResult; +} + +bool Qalculator::busy() const { + return m_busy; +} + } // namespace caelestia diff --git a/plugin/src/Caelestia/qalculator.hpp b/plugin/src/Caelestia/qalculator.hpp index a07a8a2fc..b2f5517f7 100644 --- a/plugin/src/Caelestia/qalculator.hpp +++ b/plugin/src/Caelestia/qalculator.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -10,10 +11,32 @@ class Qalculator : public QObject { QML_ELEMENT QML_SINGLETON + Q_PROPERTY(QString result READ result NOTIFY resultChanged) + Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + public: explicit Qalculator(QObject* parent = nullptr); Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; + Q_INVOKABLE void evalAsync(const QString& expr); + + [[nodiscard]] QString result() const; + [[nodiscard]] QString rawResult() const; + [[nodiscard]] bool busy() const; + +signals: + void resultChanged(); + void rawResultChanged(); + void busyChanged(); + +private: + static QMutex s_calculatorMutex; + + QString m_result; + QString m_rawResult; + bool m_busy = false; + quint64 m_generation = 0; }; } // namespace caelestia From 52e7e9732bf46fd5cdcd5596e762a7ecde3323ea Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:01:56 +1100 Subject: [PATCH 089/409] feat: async components --- components/controls/Menu.qml | 1 + components/controls/ToggleButton.qml | 3 +++ components/filedialog/FolderContents.qml | 1 + components/filedialog/HeaderBar.qml | 4 ++++ components/images/CachingIconImage.qml | 1 + 5 files changed, 10 insertions(+) diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index c763b54a8..3e834aecb 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -83,6 +83,7 @@ Elevation { } Loader { + asynchronous: true Layout.alignment: Qt.AlignVCenter active: item.modelData.trailingIcon.length > 0 visible: active diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 98c7564f2..dd42af25f 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -74,6 +74,7 @@ StyledRect { } Loader { + asynchronous: true active: !!root.label visible: active @@ -101,6 +102,8 @@ StyledRect { // Tooltip - positioned absolutely, doesn't affect layout Loader { id: tooltipLoader + + asynchronous: true active: root.tooltip !== "" z: 10000 width: 0 diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index e16c7a15c..00d7a3d92 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -47,6 +47,7 @@ Item { } Loader { + asynchronous: true anchors.centerIn: parent opacity: view.count === 0 ? 1 : 0 diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index c9a3feb53..43184bfd1 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -75,6 +75,7 @@ StyledRect { spacing: 0 Loader { + asynchronous: true Layout.rightMargin: Appearance.spacing.small active: folder.index > 0 sourceComponent: StyledText { @@ -89,6 +90,7 @@ StyledRect { implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 Loader { + asynchronous: true anchors.fill: parent active: folder.index < root.dialog.cwd.length - 1 sourceComponent: StateLayer { @@ -103,6 +105,8 @@ StyledRect { Loader { id: homeIcon + asynchronous: true + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter anchors.leftMargin: Appearance.padding.normal diff --git a/components/images/CachingIconImage.qml b/components/images/CachingIconImage.qml index 1acc6a181..52c0d14f7 100644 --- a/components/images/CachingIconImage.qml +++ b/components/images/CachingIconImage.qml @@ -18,6 +18,7 @@ Item { Loader { id: loader + asynchronous: true anchors.fill: parent sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null } From 94387e7770d12cf238ca38639592e34c02211bd8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:02:04 +1100 Subject: [PATCH 090/409] feat: async picker and background --- modules/areapicker/Picker.qml | 1 + modules/background/Background.qml | 5 +++++ modules/background/DesktopClock.qml | 2 ++ modules/background/Visualiser.qml | 2 ++ modules/background/Wallpaper.qml | 1 + 5 files changed, 11 insertions(+) diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index f4f4a3679..3af11fcaf 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -206,6 +206,7 @@ MouseArea { Loader { id: screencopy + asynchronous: true anchors.fill: parent active: root.loader.freeze diff --git a/modules/background/Background.qml b/modules/background/Background.qml index c1f149a00..06a095292 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -9,6 +9,7 @@ import Quickshell.Wayland import QtQuick Loader { + asynchronous: true active: Config.background.enabled sourceComponent: Variants { @@ -39,6 +40,8 @@ Loader { Loader { id: wallpaper + asynchronous: true + anchors.fill: parent active: Config.background.wallpaperEnabled @@ -54,6 +57,8 @@ Loader { Loader { id: clockLoader + + asynchronous: true active: Config.background.desktopClock.enabled anchors.margins: Appearance.padding.large * 2 diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index f9a06a2aa..e81fd3af8 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -40,6 +40,7 @@ Item { } Loader { + asynchronous: true anchors.fill: parent active: root.blurEnabled @@ -101,6 +102,7 @@ Item { } Loader { + asynchronous: true Layout.alignment: Qt.AlignTop Layout.topMargin: Appearance.padding.large * 1.4 * root.scale diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 35a086b92..813c0a077 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -21,6 +21,7 @@ Item { opacity: shouldBeActive ? 1 : 0 Loader { + asynchronous: true anchors.fill: parent active: root.opacity > 0 && Config.background.visualiser.blur @@ -42,6 +43,7 @@ Item { layer.enabled: true Loader { + asynchronous: true anchors.fill: parent anchors.topMargin: root.offset anchors.bottomMargin: -root.offset diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index b83ff8bec..67511d860 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -33,6 +33,7 @@ Item { } Loader { + asynchronous: true anchors.fill: parent active: root.completed && !root.source From 96ed5895d8775a42b331f1b47613e32adbc5adf8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:02:44 +1100 Subject: [PATCH 091/409] feat: async control center --- modules/controlcenter/ControlCenter.qml | 1 + modules/controlcenter/NavRail.qml | 1 + modules/controlcenter/Panes.qml | 1 + modules/controlcenter/appearance/AppearancePane.qml | 1 + .../appearance/sections/ColorSchemeSection.qml | 1 + modules/controlcenter/appearance/sections/FontsSection.qml | 6 ++++++ modules/controlcenter/components/DeviceDetails.qml | 4 ++++ modules/controlcenter/components/DeviceList.qml | 2 ++ modules/controlcenter/components/SplitPaneLayout.qml | 2 ++ modules/controlcenter/components/SplitPaneWithDetails.qml | 2 ++ modules/controlcenter/dashboard/DashboardPane.qml | 1 + modules/controlcenter/launcher/LauncherPane.qml | 5 +++++ modules/controlcenter/network/NetworkingPane.qml | 3 +++ modules/controlcenter/taskbar/TaskbarPane.qml | 1 + 14 files changed, 31 insertions(+) diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 4aacfad99..6478774f0 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -42,6 +42,7 @@ Item { Layout.fillWidth: true Layout.columnSpan: 2 + asynchronous: true active: root.floating visible: active diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index e61a741a3..ef338b207 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -43,6 +43,7 @@ Item { Loader { Layout.topMargin: Appearance.spacing.large + asynchronous: true active: !root.session.floating visible: active diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index ab2f808e9..f18bf74f0 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -129,6 +129,7 @@ ClippingRectangle { id: loader anchors.fill: parent + asynchronous: true clip: false active: false diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index f29f7ab3d..b412ca572 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -124,6 +124,7 @@ Item { Layout.fillHeight: true Layout.bottomMargin: -Appearance.padding.large * 2 + asynchronous: true active: { const isActive = root.session.activeIndex === 3; const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 95cb4b725..3c58673cb 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -128,6 +128,7 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 1466746db..4b5f06992 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -28,6 +28,7 @@ CollapsibleSection { Loader { Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true active: sansFontSection.expanded sourceComponent: StyledListView { @@ -81,6 +82,7 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -107,6 +109,7 @@ CollapsibleSection { Loader { Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true active: monoFontSection.expanded sourceComponent: StyledListView { @@ -160,6 +163,7 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { @@ -187,6 +191,7 @@ CollapsibleSection { id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true active: materialFontSection.expanded sourceComponent: StyledListView { @@ -240,6 +245,7 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index a5d06471c..8e5cdb2ce 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -36,6 +36,7 @@ Item { id: headerLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null } @@ -44,6 +45,7 @@ Item { id: topContentLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.topContent visible: root.topContent !== null } @@ -55,6 +57,7 @@ Item { required property Component modelData Layout.fillWidth: true + asynchronous: true sourceComponent: modelData } } @@ -63,6 +66,7 @@ Item { id: bottomContentLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.bottomContent visible: root.bottomContent !== null } diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 722f9a16e..7c5292da1 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -32,6 +32,7 @@ ColumnLayout { id: headerLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null && root.showHeader } @@ -50,6 +51,7 @@ ColumnLayout { } Loader { + asynchronous: true sourceComponent: root.titleSuffix visible: root.titleSuffix !== null } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 89504a0b8..bf513e56c 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -49,6 +49,7 @@ RowLayout { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + asynchronous: true sourceComponent: root.leftContent Component.onCompleted: { @@ -90,6 +91,7 @@ RowLayout { anchors.fill: parent anchors.margins: Appearance.padding.large * 2 + asynchronous: true sourceComponent: root.rightContent Component.onCompleted: { diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index ce8c9d07d..79b23abc0 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -53,6 +53,7 @@ Item { anchors.fill: parent + asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -86,6 +87,7 @@ Item { id: overlayLoader anchors.fill: parent + asynchronous: true z: 1000 sourceComponent: root.overlayComponent active: root.overlayComponent !== null diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index df29f0964..728814e21 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -80,6 +80,7 @@ Item { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + asynchronous: true sourceComponent: dashboardContentComponent } } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index b236cf9e3..884123cf3 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -342,6 +342,7 @@ Item { spacing: Appearance.spacing.normal IconImage { + asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: 32 source: { @@ -360,6 +361,7 @@ Item { Layout.alignment: Qt.AlignVCenter readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false + asynchronous: true active: isHidden || isFav sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) @@ -414,6 +416,7 @@ Item { anchors.fill: parent + asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -536,6 +539,8 @@ Item { IconImage { id: appIconImage + + asynchronous: true Layout.alignment: Qt.AlignHCenter implicitSize: Appearance.font.size.extraLarge * 3 * 2 source: { diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 26cdbfacd..0b0a645ce 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -120,6 +120,7 @@ Item { Loader { Layout.fillWidth: true + asynchronous: true sourceComponent: Component { VpnList { session: root.session @@ -138,6 +139,7 @@ Item { Loader { Layout.fillWidth: true + asynchronous: true sourceComponent: Component { EthernetList { session: root.session @@ -156,6 +158,7 @@ Item { Loader { Layout.fillWidth: true + asynchronous: true sourceComponent: Component { WirelessList { session: root.session diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index c23904b80..99d56c844 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -140,6 +140,7 @@ Item { anchors.leftMargin: Appearance.padding.large anchors.rightMargin: Appearance.padding.large + asynchronous: true sourceComponent: taskbarContentComponent } } From c20ea3fabfc404fb75505e498f8c47a7640b0079 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:03:23 +1100 Subject: [PATCH 092/409] feat: async launcher --- modules/launcher/ContentList.qml | 1 + modules/launcher/items/AppItem.qml | 2 ++ modules/launcher/items/SchemeItem.qml | 1 + modules/launcher/items/VariantItem.qml | 1 + 4 files changed, 5 insertions(+) diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index b2a9c7708..730fcdb51 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -90,6 +90,7 @@ Item { Loader { id: wallpaperList + asynchronous: true active: false anchors.top: parent.top diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 2bd818d60..75c03a199 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -36,6 +36,7 @@ Item { IconImage { id: icon + asynchronous: true source: Quickshell.iconPath(root.modelData?.icon, "image-missing") implicitSize: parent.height * 0.8 @@ -74,6 +75,7 @@ Item { Loader { id: favouriteIcon + asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right active: modelData && Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 3ff184681..aade35f21 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -89,6 +89,7 @@ Item { Loader { id: current + asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index 5c34fa89f..34cc87faf 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -65,6 +65,7 @@ Item { Loader { id: current + asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter From a39a2fcd1809f4a195126386be3b3710e7a74755 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:03:33 +1100 Subject: [PATCH 093/409] feat: async dashboard --- modules/dashboard/LyricMenu.qml | 1 + modules/dashboard/dash/DateTime.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index 0add06bb2..f033b0dd7 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -23,6 +23,7 @@ StyledRect { } Loader { + asynchronous: true anchors.fill: parent active: root.height > 0 diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index e74044883..90d2f0b6d 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -48,6 +48,7 @@ Item { } Loader { + asynchronous: true Layout.alignment: Qt.AlignHCenter active: Config.services.useTwelveHourClock From 80b34c8c7c7a20f81034d7e9106ea544d10a3ca3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:04:03 +1100 Subject: [PATCH 094/409] feat: async lock --- modules/lock/Center.qml | 1 + modules/lock/Fetch.qml | 1 + modules/lock/NotifDock.qml | 1 + modules/lock/NotifGroup.qml | 3 +++ modules/lock/WeatherInfo.qml | 3 +++ 5 files changed, 9 insertions(+) diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index aa926acd1..07b80d3b2 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -54,6 +54,7 @@ ColumnLayout { } Loader { + asynchronous: true Layout.leftMargin: Appearance.spacing.small Layout.alignment: Qt.AlignVCenter diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index e96b14315..e3feb6934 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -162,6 +162,7 @@ ColumnLayout { } component WrappedLoader: Loader { + asynchronous: true visible: active } diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index cce86cdf3..9a4dde64d 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -39,6 +39,7 @@ ColumnLayout { color: "transparent" Loader { + asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 7fcb108ec..2e8ca5424 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -109,12 +109,14 @@ StyledRect { radius: Appearance.rounding.full Loader { + asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { + asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image @@ -270,6 +272,7 @@ StyledRect { } Loader { + asynchronous: true Layout.fillWidth: true opacity: root.expanded ? 1 : 0 diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index d6c25af29..e69a09dfd 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -19,6 +19,7 @@ ColumnLayout { spacing: Appearance.spacing.small Loader { + asynchronous: true Layout.topMargin: Appearance.padding.large * 2 Layout.bottomMargin: -Appearance.padding.large Layout.alignment: Qt.AlignHCenter @@ -71,6 +72,7 @@ ColumnLayout { } Loader { + asynchronous: true Layout.rightMargin: Appearance.padding.smaller active: root.width > 400 visible: active @@ -107,6 +109,7 @@ ColumnLayout { Loader { id: forecastLoader + asynchronous: true Layout.topMargin: Appearance.spacing.smaller Layout.bottomMargin: Appearance.padding.large * 2 Layout.fillWidth: true From 473e4456b0c73148cdb66c8a477c16bafc96a05a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:18:45 +1100 Subject: [PATCH 095/409] feat: async bar --- modules/bar/Bar.qml | 1 + modules/bar/components/ActiveWindow.qml | 1 + modules/bar/components/Clock.qml | 1 + modules/bar/components/OsIcon.qml | 1 + modules/bar/components/StatusIcons.qml | 1 + modules/bar/components/Tray.qml | 2 ++ modules/bar/components/workspaces/SpecialWorkspaces.qml | 5 +++++ modules/bar/components/workspaces/Workspace.qml | 2 ++ modules/bar/components/workspaces/Workspaces.qml | 4 ++++ modules/bar/popouts/ActiveWindow.qml | 1 + modules/bar/popouts/Battery.qml | 1 + modules/bar/popouts/Bluetooth.qml | 1 + modules/bar/popouts/TrayMenu.qml | 5 +++++ 13 files changed, 26 insertions(+) diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 95c166e6f..5a7c3ce6a 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -194,6 +194,7 @@ ColumnLayout { return null; } + asynchronous: true Layout.alignment: Qt.AlignHCenter // Cursed ahh thing to add padding to first and last enabled components diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 990a65a6b..0ae727b30 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -39,6 +39,7 @@ Item { implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin Loader { + asynchronous: true anchors.fill: parent active: !Config.bar.activeWindow.showOnHover diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 089bc5a82..96885764a 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -23,6 +23,7 @@ StyledRect { spacing: Appearance.spacing.small Loader { + asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: Config.bar.clock.showIcon diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index 6710294a0..0ad3bf30c 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -21,6 +21,7 @@ Item { } Loader { + asynchronous: true anchors.centerIn: parent sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index ca7dc2e3a..b1cc133fb 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -264,6 +264,7 @@ StyledRect { component WrappedLoader: Loader { required property string name + asynchronous: true Layout.alignment: Qt.AlignHCenter visible: active } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 7bafda16f..b5f0b020b 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -82,6 +82,8 @@ StyledRect { Loader { id: expandIcon + asynchronous: true + anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index cd3572be2..555bb3b46 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -164,6 +164,8 @@ Item { Loader { id: label + asynchronous: true + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 @@ -192,6 +194,8 @@ Item { Loader { id: windows + asynchronous: true + Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true Layout.preferredHeight: implicitHeight @@ -293,6 +297,7 @@ Item { } Loader { + asynchronous: true active: Config.bar.workspaces.activeIndicator anchors.fill: parent diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index f6e767ef9..32efc1253 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -55,6 +55,8 @@ ColumnLayout { Loader { id: windows + asynchronous: true + Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true Layout.topMargin: -Config.bar.sizes.innerWidth / 10 diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index b9fe87faf..a2231b74c 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -45,6 +45,7 @@ StyledClippingRect { } Loader { + asynchronous: true active: Config.bar.workspaces.occupiedBg anchors.fill: parent @@ -77,6 +78,7 @@ StyledClippingRect { } Loader { + asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: Config.bar.workspaces.activeIndicator @@ -110,6 +112,8 @@ StyledClippingRect { Loader { id: specialWs + asynchronous: true + anchors.fill: parent anchors.margins: Appearance.padding.small diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index adf7b7740..fe89c9e86 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -31,6 +31,7 @@ Item { IconImage { id: icon + asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: details.implicitHeight source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing") diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index ac975e1b7..78bd324e3 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -37,6 +37,7 @@ Column { } Loader { + asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 676da82f5..91ac56049 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -142,6 +142,7 @@ ColumnLayout { } Loader { + asynchronous: true active: device.modelData.bonded sourceComponent: Item { implicitWidth: connectBtn.implicitWidth diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 027552d89..a6680f44e 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -81,6 +81,7 @@ StackView { Loader { id: children + asynchronous: true anchors.left: parent.left anchors.right: parent.right @@ -114,11 +115,13 @@ StackView { Loader { id: icon + asynchronous: true anchors.left: parent.left active: item.modelData.icon !== "" sourceComponent: IconImage { + asynchronous: true implicitSize: label.implicitHeight source: item.modelData.icon @@ -149,6 +152,7 @@ StackView { Loader { id: expand + asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -165,6 +169,7 @@ StackView { } Loader { + asynchronous: true active: menu.isSubMenu sourceComponent: Item { From 23834525ed757e56b2ae4b97eef1427978503168 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:19:28 +1100 Subject: [PATCH 096/409] feat: async utilities --- modules/utilities/RecordingDeleteModal.qml | 1 + modules/utilities/Wrapper.qml | 1 + modules/utilities/cards/IdleInhibit.qml | 1 + modules/utilities/cards/Record.qml | 1 + modules/utilities/cards/RecordingList.qml | 1 + 5 files changed, 5 insertions(+) diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 127afe93b..5bb49b118 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -15,6 +15,7 @@ Loader { required property var props + asynchronous: true anchors.fill: parent opacity: root.props.recordingConfirmDelete ? 1 : 0 diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 77178e36e..d3371dde9 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -79,6 +79,7 @@ Item { Loader { id: content + asynchronous: true anchors.top: parent.top anchors.left: parent.left anchors.margins: Appearance.padding.large diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 0344e3ad2..cacf4c7c3 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -70,6 +70,7 @@ StyledRect { Loader { id: activeChip + asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left anchors.topMargin: Appearance.spacing.larger diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 273c64002..407b88ae3 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -111,6 +111,7 @@ StyledRect { property bool running: Recorder.running + asynchronous: true Layout.fillWidth: true Layout.preferredHeight: implicitHeight sourceComponent: running ? recordingControls : recordingList diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index b9d757a4d..375812b68 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -163,6 +163,7 @@ ColumnLayout { } Loader { + asynchronous: true anchors.centerIn: parent opacity: list.count === 0 ? 1 : 0 From 1d75e9c6fc7aded3384fde2daf43bd510cc5d55b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:19:36 +1100 Subject: [PATCH 097/409] feat: async window info --- modules/windowinfo/Buttons.qml | 1 + modules/windowinfo/Preview.qml | 1 + 2 files changed, 2 insertions(+) diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 89acfe6d6..b75612479 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -117,6 +117,7 @@ ColumnLayout { } Loader { + asynchronous: true active: root.client?.lastIpcObject.floating Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 4cc0aab86..40846320f 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -33,6 +33,7 @@ Item { radius: Appearance.rounding.small Loader { + asynchronous: true anchors.centerIn: parent active: !root.client From 789c3fb6daae59f3992dbbeaefe2a4c182187313 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:24:13 +1100 Subject: [PATCH 098/409] feat: async sidebar --- modules/sidebar/Notif.qml | 1 + modules/sidebar/NotifActionList.qml | 1 + modules/sidebar/NotifDock.qml | 2 ++ modules/sidebar/NotifGroup.qml | 2 ++ 4 files changed, 6 insertions(+) diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 5a317640f..4ba76ec81 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -154,6 +154,7 @@ StyledRect { component WrappedLoader: Loader { required property bool shouldBeActive + asynchronous: true opacity: shouldBeActive ? 1 : 0 active: opacity > 0 diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index d1f1e1f51..b95b1da8d 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -167,6 +167,7 @@ Item { id: iconComp IconImage { + asynchronous: true source: Quickshell.iconPath(action.modelData.identifier) } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index d039d15d6..9b865d4c3 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -86,6 +86,7 @@ Item { color: "transparent" Loader { + asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: root.notifCount > 0 ? 0 : 1 @@ -166,6 +167,7 @@ Item { } Loader { + asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom anchors.margins: Appearance.padding.normal diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 2c032aa7d..6bd2f55c9 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -136,12 +136,14 @@ StyledRect { radius: Appearance.rounding.full Loader { + asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { + asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image From 344ec92cc5bc146b8b30a76fedc2f444348f366d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:24:20 +1100 Subject: [PATCH 099/409] feat: async notifs and osd --- modules/notifications/Notification.qml | 4 ++++ modules/osd/Content.qml | 1 + 2 files changed, 5 insertions(+) diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index c8efa8d78..1208f025c 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -109,6 +109,7 @@ StyledRect { Loader { id: image + asynchronous: true active: root.hasImage anchors.left: parent.left @@ -137,6 +138,7 @@ StyledRect { Loader { id: appIcon + asynchronous: true active: root.hasAppIcon || !root.hasImage anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter @@ -153,6 +155,7 @@ StyledRect { Loader { id: icon + asynchronous: true active: root.hasAppIcon anchors.centerIn: parent @@ -169,6 +172,7 @@ StyledRect { } Loader { + asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 770fb6968..6776bb8f9 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -109,6 +109,7 @@ Item { component WrappedLoader: Loader { required property bool shouldBeActive + asynchronous: true Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 opacity: shouldBeActive ? 1 : 0 active: opacity > 0 From 3b08cd6594cf98e8855cfa21c63877c55e328935 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 01:45:10 +1100 Subject: [PATCH 100/409] chore: add newline after id --- components/controls/CollapsibleSection.qml | 6 ++++++ components/controls/SplitButtonRow.qml | 2 ++ components/controls/StyledInputField.qml | 2 ++ modules/bar/components/Clock.qml | 1 + modules/bar/popouts/Content.qml | 3 +++ modules/bar/popouts/WirelessPassword.qml | 6 ++++++ modules/bar/popouts/kblayout/KbLayout.qml | 4 ++++ modules/bar/popouts/kblayout/KbLayoutModel.qml | 7 +++++++ modules/controlcenter/Panes.qml | 2 ++ .../controlcenter/appearance/AppearancePane.qml | 8 ++++++++ .../appearance/sections/ColorSchemeSection.qml | 2 ++ .../appearance/sections/ColorVariantSection.qml | 1 + .../appearance/sections/FontsSection.qml | 7 +++++++ modules/controlcenter/audio/AudioPane.qml | 7 +++++++ modules/controlcenter/bluetooth/BtPane.qml | 1 + modules/controlcenter/bluetooth/Details.qml | 1 + .../components/ConnectedButtonGroup.qml | 3 +++ modules/controlcenter/components/SliderInput.qml | 1 + modules/controlcenter/components/WallpaperGrid.qml | 1 + modules/controlcenter/dashboard/DashboardPane.qml | 4 ++++ modules/controlcenter/launcher/LauncherPane.qml | 9 +++++++++ modules/controlcenter/network/NetworkingPane.qml | 4 ++++ modules/controlcenter/network/VpnDetails.qml | 2 ++ modules/controlcenter/network/VpnList.qml | 2 ++ modules/controlcenter/network/WirelessDetails.qml | 1 + .../network/WirelessPasswordDialog.qml | 4 ++++ modules/controlcenter/taskbar/TaskbarPane.qml | 14 ++++++++++++++ modules/dashboard/Content.qml | 4 ++++ modules/dashboard/LyricMenu.qml | 5 +++++ modules/dashboard/LyricsView.qml | 4 ++++ modules/dashboard/Media.qml | 3 +++ services/LyricsService.qml | 6 ++++++ services/Network.qml | 1 + services/NetworkUsage.qml | 3 +++ services/Nmcli.qml | 1 + services/SystemUsage.qml | 1 + services/Time.qml | 1 + 37 files changed, 134 insertions(+) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index e3d8eefd1..a338d4129 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -22,11 +22,13 @@ ColumnLayout { Item { id: sectionHeaderItem + Layout.fillWidth: true Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) RowLayout { id: titleRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -74,6 +76,7 @@ ColumnLayout { Item { id: contentWrapper + Layout.fillWidth: true Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 clip: true @@ -86,6 +89,7 @@ ColumnLayout { StyledRect { id: backgroundRect + anchors.fill: parent radius: Appearance.rounding.normal color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) @@ -101,6 +105,7 @@ ColumnLayout { ColumnLayout { id: contentColumn + anchors.left: parent.left anchors.right: parent.right y: Appearance.spacing.small @@ -118,6 +123,7 @@ ColumnLayout { StyledText { id: descriptionText + Layout.fillWidth: true Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index db9925ff6..a07bf0287 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -33,6 +33,7 @@ StyledRect { RowLayout { id: row + anchors.fill: parent anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal @@ -45,6 +46,7 @@ StyledRect { SplitButton { id: splitButton + enabled: root.enabled type: SplitButton.Filled diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index 0d199c738..cd6717658 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -43,6 +43,7 @@ Item { MouseArea { id: inputHover + anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -52,6 +53,7 @@ Item { StyledTextField { id: inputField + anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: root.horizontalAlignment diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 96885764a..13c9035a5 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -19,6 +19,7 @@ StyledRect { Column { id: layout + anchors.centerIn: parent spacing: Appearance.spacing.small diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 6543e584d..24e7909ec 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -35,6 +35,7 @@ Item { Popout { id: networkPopout + name: "network" sourceComponent: Network { wrapper: root.wrapper @@ -52,9 +53,11 @@ Item { Popout { id: passwordPopout + name: "wirelesspassword" sourceComponent: WirelessPassword { id: passwordComponent + wrapper: root.wrapper network: networkPopout.item?.passwordNetwork ?? null } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 96639e711..54e635da0 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -42,6 +42,7 @@ ColumnLayout { Timer { id: focusTimer + interval: 150 onTriggered: { root.forceActiveFocus(); @@ -140,6 +141,7 @@ ColumnLayout { StyledText { id: networkNameText + Layout.alignment: Qt.AlignHCenter text: { if (root.network) { @@ -206,6 +208,7 @@ ColumnLayout { FocusScope { id: passwordContainer + objectName: "passwordContainer" Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true @@ -259,6 +262,7 @@ ColumnLayout { Timer { id: passwordFocusTimer + interval: 50 onTriggered: { passwordContainer.forceActiveFocus(); @@ -526,6 +530,7 @@ ColumnLayout { Timer { id: connectionMonitor + interval: 1000 repeat: true triggeredOnStart: false @@ -545,6 +550,7 @@ ColumnLayout { Timer { id: connectionSuccessTimer + interval: 500 onTriggered: { // Double-check connection is still active diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 94b6f7ec5..aea25ceb8 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -37,6 +37,7 @@ ColumnLayout { ListView { id: list + model: kb.visibleModel Layout.fillWidth: true @@ -95,6 +96,7 @@ ColumnLayout { StateLayer { id: layer + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -111,6 +113,7 @@ ColumnLayout { StyledText { id: rowText + anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right @@ -173,6 +176,7 @@ ColumnLayout { SequentialAnimation { id: popIn + running: false ParallelAnimation { diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 437109530..0d95b7a8e 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -10,6 +10,7 @@ import Caelestia Item { id: model + visible: false ListModel { @@ -44,6 +45,7 @@ Item { Process { id: _xkbXmlBase + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) @@ -54,6 +56,7 @@ Item { Process { id: _xkbXmlEvdev + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] stdout: StdioCollector { onStreamFinished: _buildXmlMap(text) @@ -107,6 +110,7 @@ Item { Process { id: _getKbLayoutOpt + command: ["hyprctl", "-j", "getoption", "input:kb_layout"] stdout: StdioCollector { onStreamFinished: { @@ -126,6 +130,7 @@ Item { Process { id: _fetchLayoutsFromDevices + command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -143,6 +148,7 @@ Item { Process { id: _fetchActiveLayouts + command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -165,6 +171,7 @@ Item { Process { id: _switchProc + onRunningChanged: if (!running) _fetchActiveLayouts.running = true } diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index f18bf74f0..a05e6aa65 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -56,6 +56,7 @@ ClippingRectangle { Timer { id: animationDelayTimer + interval: Appearance.anim.durations.normal onTriggered: { layout.animationComplete = true; @@ -64,6 +65,7 @@ ClippingRectangle { Timer { id: initialOpeningTimer + interval: Appearance.anim.durations.large running: true onTriggered: { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index b412ca572..94ca1cb7d 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -155,6 +155,7 @@ Item { StyledFlickable { id: sidebarFlickable + readonly property var rootPane: root flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height @@ -165,6 +166,7 @@ Item { ColumnLayout { id: sidebarLayout + anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.small @@ -219,31 +221,37 @@ Item { AnimationsSection { id: animationsSection + rootPane: sidebarFlickable.rootPane } FontsSection { id: fontsSection + rootPane: sidebarFlickable.rootPane } ScalesSection { id: scalesSection + rootPane: sidebarFlickable.rootPane } TransparencySection { id: transparencySection + rootPane: sidebarFlickable.rootPane } BorderSection { id: borderSection + rootPane: sidebarFlickable.rootPane } BackgroundSection { id: backgroundSection + rootPane: sidebarFlickable.rootPane } } diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 3c58673cb..4b4559ae6 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -53,6 +53,7 @@ CollapsibleSection { Timer { id: reloadTimer + interval: 300 onTriggered: { Schemes.reload(); @@ -82,6 +83,7 @@ CollapsibleSection { MaterialIcon { id: iconPlaceholder + visible: false text: "circle" font.pointSize: Appearance.font.size.large diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index 3aa17dd9c..3de9e4b3c 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -48,6 +48,7 @@ CollapsibleSection { Timer { id: reloadTimer + interval: 300 onTriggered: { Schemes.reload(); diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 4b5f06992..d3a2ce68f 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -20,6 +20,7 @@ CollapsibleSection { CollapsibleSection { id: sansFontSection + title: qsTr("Sans-serif font family") expanded: true showBackground: true @@ -33,6 +34,7 @@ CollapsibleSection { sourceComponent: StyledListView { id: sansFontList + property alias contentHeight: sansFontList.contentHeight clip: true @@ -101,6 +103,7 @@ CollapsibleSection { CollapsibleSection { id: monoFontSection + title: qsTr("Monospace font family") expanded: false showBackground: true @@ -114,6 +117,7 @@ CollapsibleSection { sourceComponent: StyledListView { id: monoFontList + property alias contentHeight: monoFontList.contentHeight clip: true @@ -182,6 +186,7 @@ CollapsibleSection { CollapsibleSection { id: materialFontSection + title: qsTr("Material font family") expanded: false showBackground: true @@ -189,6 +194,7 @@ CollapsibleSection { Loader { id: materialFontLoader + Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 asynchronous: true @@ -196,6 +202,7 @@ CollapsibleSection { sourceComponent: StyledListView { id: materialFontList + property alias contentHeight: materialFontList.contentHeight clip: true diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 01d90be70..deefc965c 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -26,6 +26,7 @@ Item { StyledFlickable { id: leftAudioFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height @@ -217,6 +218,7 @@ Item { rightContent: Component { StyledFlickable { id: rightAudioFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: contentLayout.height @@ -265,6 +267,7 @@ Item { StyledInputField { id: outputVolumeInput + Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -336,6 +339,7 @@ Item { StyledSlider { id: outputVolumeSlider + Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 @@ -380,6 +384,7 @@ Item { StyledInputField { id: inputVolumeInput + Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -451,6 +456,7 @@ Item { StyledSlider { id: inputVolumeSlider + Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 @@ -511,6 +517,7 @@ Item { StyledInputField { id: streamVolumeInput + Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 7d3b9ca33..1b9a7fcfa 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -53,6 +53,7 @@ SplitPaneWithDetails { rightSettingsComponent: Component { StyledFlickable { id: settingsFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index bc276e097..cb216ce2c 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -359,6 +359,7 @@ StyledFlickable { RowLayout { id: batteryPercent + Layout.topMargin: Appearance.spacing.small / 2 Layout.fillWidth: true Layout.preferredHeight: Appearance.padding.smaller diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index ab707fb73..a85b4f3e7 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -40,6 +40,7 @@ StyledRect { GridLayout { id: buttonGrid + Layout.alignment: Qt.AlignHCenter rowSpacing: Appearance.spacing.small columnSpacing: Appearance.spacing.small @@ -48,10 +49,12 @@ StyledRect { Repeater { id: repeater + model: root.options delegate: TextButton { id: button + required property int index required property var modelData diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 11b3f70dd..6b83a7a98 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -78,6 +78,7 @@ ColumnLayout { StyledInputField { id: inputField + Layout.preferredWidth: 70 validator: root.validator diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index ed6bb40a8..8b71809e2 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -201,6 +201,7 @@ GridView { StyledText { id: filenameText + anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 728814e21..489af7814 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -64,6 +64,7 @@ Item { ClippingRectangle { id: dashboardClippingRect + anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 @@ -87,6 +88,7 @@ Item { InnerBorder { id: dashboardBorder + leftThickness: 0 rightThickness: Appearance.padding.normal } @@ -96,6 +98,7 @@ Item { StyledFlickable { id: dashboardFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: dashboardLayout.height @@ -105,6 +108,7 @@ Item { ColumnLayout { id: dashboardLayout + anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 884123cf3..17e89aae0 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -148,6 +148,7 @@ Item { ColumnLayout { id: leftLauncherLayout + anchors.fill: parent spacing: Appearance.spacing.small @@ -284,6 +285,7 @@ Item { Loader { id: appsListLoader + Layout.fillWidth: true Layout.fillHeight: true asynchronous: true @@ -369,6 +371,7 @@ Item { Component { id: hiddenIcon + MaterialIcon { text: "visibility_off" fill: 1 @@ -378,6 +381,7 @@ Item { Component { id: favouriteIcon + MaterialIcon { text: "favorite" fill: 1 @@ -486,6 +490,7 @@ Item { StyledFlickable { id: settingsFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -509,6 +514,7 @@ Item { ColumnLayout { id: appDetailsLayout + anchors.fill: parent readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null @@ -557,6 +563,7 @@ Item { StyledText { id: appTitleText + Layout.alignment: Qt.AlignHCenter text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" font.pointSize: Appearance.font.size.large @@ -574,6 +581,7 @@ Item { StyledFlickable { id: detailsFlickable + anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: debugLayout.height @@ -584,6 +592,7 @@ Item { ColumnLayout { id: debugLayout + anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 0b0a645ce..78d5fc93f 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -281,6 +281,7 @@ Item { StyledFlickable { id: settingsFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -304,6 +305,7 @@ Item { StyledFlickable { id: ethernetFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: ethernetDetailsInner.height @@ -327,6 +329,7 @@ Item { StyledFlickable { id: wirelessFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: wirelessDetailsInner.height @@ -350,6 +353,7 @@ Item { StyledFlickable { id: vpnFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: vpnDetailsInner.height diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 1c71cd716..dd41dde5b 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -302,6 +302,7 @@ DeviceDetails { StyledTextField { id: displayNameField + anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -338,6 +339,7 @@ DeviceDetails { StyledTextField { id: interfaceNameField + anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 81f4a45a3..0d09001ec 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -586,6 +586,7 @@ ColumnLayout { StyledTextField { id: displayNameField + anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft @@ -622,6 +623,7 @@ ColumnLayout { StyledTextField { id: interfaceNameField + anchors.centerIn: parent width: parent.width - Appearance.padding.normal horizontalAlignment: TextInput.AlignLeft diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index e8777cdf7..29b2f6274 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -58,6 +58,7 @@ DeviceDetails { Timer { id: connectionUpdateTimer + interval: 500 repeat: true running: network && network.ssid diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 7ad5204a4..7a666f339 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -150,6 +150,7 @@ Item { Item { id: passwordContainer + Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) @@ -247,6 +248,7 @@ Item { StyledText { id: placeholder + anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline @@ -443,6 +445,7 @@ Item { Timer { id: connectionMonitor + interval: 1000 repeat: true triggeredOnStart: false @@ -462,6 +465,7 @@ Item { Timer { id: connectionSuccessTimer + interval: 500 onTriggered: { if (root.visible && Nmcli.active && Nmcli.active.ssid) { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 99d56c844..f18ac8941 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -124,6 +124,7 @@ Item { ClippingRectangle { id: taskbarClippingRect + anchors.fill: parent anchors.margins: Appearance.padding.normal anchors.leftMargin: 0 @@ -147,6 +148,7 @@ Item { InnerBorder { id: taskbarBorder + leftThickness: 0 rightThickness: Appearance.padding.normal } @@ -156,6 +158,7 @@ Item { StyledFlickable { id: sidebarFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height @@ -165,6 +168,7 @@ Item { ColumnLayout { id: sidebarLayout + anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top @@ -264,11 +268,13 @@ Item { RowLayout { id: mainRowLayout + Layout.fillWidth: true spacing: Appearance.spacing.normal ColumnLayout { id: leftColumnLayout + Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -294,6 +300,7 @@ Item { RowLayout { id: workspacesShownRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -329,6 +336,7 @@ Item { RowLayout { id: workspacesActiveIndicatorRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -362,6 +370,7 @@ Item { RowLayout { id: workspacesOccupiedBgRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -395,6 +404,7 @@ Item { RowLayout { id: workspacesShowWindowsRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -428,6 +438,7 @@ Item { RowLayout { id: workspacesMaxWindowIconsRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -463,6 +474,7 @@ Item { RowLayout { id: workspacesPerMonitorRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -529,6 +541,7 @@ Item { ColumnLayout { id: middleColumnLayout + Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal @@ -654,6 +667,7 @@ Item { ColumnLayout { id: rightColumnLayout + Layout.fillWidth: true Layout.alignment: Qt.AlignTop spacing: Appearance.spacing.normal diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index fba608fe7..978fbc6fe 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -163,6 +163,7 @@ Item { Component { id: dashComponent + Dash { visibilities: root.visibilities state: root.state @@ -172,6 +173,7 @@ Item { Component { id: mediaComponent + MediaWrapper { visibilities: root.visibilities } @@ -179,11 +181,13 @@ Item { Component { id: performanceComponent + Performance {} } Component { id: weatherComponent + Weather {} } diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index f033b0dd7..fe984299d 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -91,6 +91,7 @@ StyledRect { delegate: Item { id: delegateRoot + width: ListView.view.width * 0.98 height: 70 anchors.horizontalCenter: parent?.horizontalCenter @@ -112,6 +113,7 @@ StyledRect { Rectangle { id: background + anchors.fill: parent radius: Appearance.rounding.small @@ -217,6 +219,7 @@ StyledRect { StyledInputField { id: searchTitle + Layout.fillWidth: true horizontalAlignment: TextInput.AlignLeft @@ -229,6 +232,7 @@ StyledRect { StyledInputField { id: searchArtist + Layout.fillWidth: true horizontalAlignment: TextInput.AlignLeft @@ -278,6 +282,7 @@ StyledRect { TextInput { id: offsetInput + horizontalAlignment: TextInput.AlignHCenter color: Colours.palette.m3secondary font.pointSize: Appearance.font.size.normal diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml index bce1b23a0..38972402a 100644 --- a/modules/dashboard/LyricsView.qml +++ b/modules/dashboard/LyricsView.qml @@ -24,6 +24,7 @@ StyledListView { Timer { id: hideTimer + interval: 300 // long enough to bridge the track switch gap running: false repeat: false @@ -50,6 +51,7 @@ StyledListView { delegate: Item { id: delegateRoot + width: ListView.view.width required property string lyricLine @@ -63,6 +65,7 @@ StyledListView { MultiEffect { id: effect + anchors.fill: lyricText source: lyricText scale: lyricText.scale @@ -90,6 +93,7 @@ StyledListView { Text { id: lyricText + text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" width: parent.width * 0.85 anchors.centerIn: parent diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index ec92743ca..69e215249 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -80,6 +80,7 @@ Item { Timer { id: lyricsHideDelay + interval: 300 repeat: false } @@ -248,6 +249,7 @@ Item { LyricsView { id: lyricsViewInDetails + Layout.fillWidth: true Layout.preferredHeight: 200 } @@ -429,6 +431,7 @@ Item { RowLayout { id: playerChanger + parent: !root.lyricsShowingDebounced ? details : leftSection Layout.alignment: Qt.AlignHCenter spacing: Appearance.spacing.small diff --git a/services/LyricsService.qml b/services/LyricsService.qml index 35dba4681..c9f782a94 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -41,6 +41,7 @@ Singleton { Timer { id: seekTimer + interval: 500 onTriggered: root.isManualSeeking = false } @@ -48,6 +49,7 @@ Singleton { // If no local lyrics were loaded within the interval, fall back to NetEase Timer { id: fallbackTimer + interval: 200 onTriggered: { if (lyricsModel.count === 0) { @@ -59,12 +61,14 @@ Singleton { Timer { id: loadDebounce + interval: 50 onTriggered: root._doLoadLyrics() } FileView { id: lyricsMapFileView + path: root.lyricsMapFile printErrors: false onLoaded: { @@ -78,6 +82,7 @@ Singleton { FileView { id: lrcFile + printErrors: false onLoaded: { fallbackTimer.stop(); @@ -111,6 +116,7 @@ Singleton { Process { id: saveLyricsMap + command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] } diff --git a/services/Network.qml b/services/Network.qml index ede37c802..7fd15dd4f 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -230,6 +230,7 @@ Singleton { Component { id: apComp + AccessPoint {} } diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 451864710..55c0c6edf 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -141,16 +141,19 @@ Singleton { CircularBuffer { id: _downloadBuffer + capacity: root.historyLength + 1 } CircularBuffer { id: _uploadBuffer + capacity: root.historyLength + 1 } FileView { id: netDevFile + path: "/proc/net/dev" } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 812387f1e..9fa753cc6 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1285,6 +1285,7 @@ Singleton { Timer { id: monitorRestartTimer + interval: 2000 onTriggered: { monitorProc.running = true; diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 508564461..413ad7654 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -138,6 +138,7 @@ Singleton { Process { id: storage + // Get physical disks with aggregated usage from their partitions // -J triggers JSON output. -b triggers bytes. command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] diff --git a/services/Time.qml b/services/Time.qml index a07d9ef8e..d918d0b6e 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -23,6 +23,7 @@ Singleton { SystemClock { id: clock + precision: SystemClock.Seconds } } From 9e2ccbede3c2a7ed611f104e16c729ce846d3efa Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:41:56 +1100 Subject: [PATCH 101/409] chore: format everything --- components/controls/CollapsibleSection.qml | 13 +- components/controls/CustomSpinBox.qml | 26 ++-- components/controls/IconButton.qml | 6 +- components/controls/IconTextButton.qml | 4 +- components/controls/Menu.qml | 6 +- components/controls/SplitButton.qml | 16 +- components/controls/StyledRadioButton.qml | 8 +- components/controls/StyledScrollBar.qml | 31 ++-- components/controls/TextButton.qml | 4 +- components/controls/ToggleButton.qml | 4 +- components/controls/Tooltip.qml | 74 ++++----- components/filedialog/DialogButtons.qml | 4 +- components/filedialog/HeaderBar.qml | 10 +- components/filedialog/Sidebar.qml | 4 +- modules/background/Wallpaper.qml | 6 +- modules/bar/popouts/ActiveWindow.qml | 4 +- modules/bar/popouts/Battery.qml | 6 +- modules/bar/popouts/Bluetooth.qml | 10 +- modules/bar/popouts/Network.qml | 18 +-- modules/bar/popouts/TrayMenu.qml | 20 +-- modules/bar/popouts/WirelessPassword.qml | 110 ++++++------- modules/bar/popouts/kblayout/KbLayout.qml | 21 +-- .../bar/popouts/kblayout/KbLayoutModel.qml | 144 +++++++++--------- modules/controlcenter/NavRail.qml | 8 +- modules/controlcenter/Panes.qml | 7 +- modules/controlcenter/WindowTitle.qml | 4 +- .../appearance/AppearancePane.qml | 11 +- .../appearance/sections/BackgroundSection.qml | 6 +- modules/controlcenter/bluetooth/Details.qml | 14 +- .../controlcenter/bluetooth/DeviceList.qml | 6 +- modules/controlcenter/bluetooth/Settings.qml | 18 +-- .../components/WallpaperGrid.qml | 8 +- .../controlcenter/dashboard/DashboardPane.qml | 4 +- .../controlcenter/launcher/LauncherPane.qml | 65 ++++---- .../controlcenter/network/EthernetList.qml | 4 +- modules/controlcenter/network/VpnDetails.qml | 8 +- modules/controlcenter/network/VpnList.qml | 77 +++++----- .../network/WirelessPasswordDialog.qml | 87 +++++------ modules/controlcenter/taskbar/TaskbarPane.qml | 30 ++-- modules/dashboard/LyricMenu.qml | 10 +- modules/dashboard/Tabs.qml | 14 +- modules/dashboard/dash/Calendar.qml | 30 ++-- modules/dashboard/dash/Media.qml | 6 +- modules/dashboard/dash/User.qml | 4 +- modules/launcher/items/ActionItem.qml | 4 +- modules/launcher/items/AppItem.qml | 4 +- modules/launcher/items/CalcItem.qml | 18 +-- modules/launcher/items/SchemeItem.qml | 4 +- modules/launcher/items/VariantItem.qml | 4 +- modules/launcher/items/WallpaperItem.qml | 4 +- modules/lock/Center.qml | 10 +- modules/lock/Media.qml | 4 +- modules/lock/NotifGroup.qml | 4 +- modules/notifications/Notification.qml | 12 +- modules/session/Content.qml | 6 +- modules/sidebar/NotifGroup.qml | 4 +- modules/windowinfo/Buttons.qml | 8 +- services/Notifs.qml | 48 +++--- 58 files changed, 551 insertions(+), 553 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index a338d4129..0e4c3dcf3 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -15,6 +15,8 @@ ColumnLayout { property bool showBackground: false property bool nested: false + default property alias content: contentColumn.data + signal toggleRequested spacing: Appearance.spacing.small @@ -61,19 +63,18 @@ ColumnLayout { } StateLayer { - anchors.fill: parent - color: Colours.palette.m3onSurface - radius: Appearance.rounding.normal - showHoverBackground: false function onClicked(): void { root.toggleRequested(); root.expanded = !root.expanded; } + + anchors.fill: parent + color: Colours.palette.m3onSurface + radius: Appearance.rounding.normal + showHoverBackground: false } } - default property alias content: contentColumn.data - Item { id: contentWrapper diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index 438dc0806..eaf2eb591 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -15,13 +15,13 @@ RowLayout { property real step: 1 property alias repeatRate: timer.interval + property bool isEditing: false + property string displayText: root.value.toString() + signal valueModified(value: real) spacing: Appearance.spacing.small - property bool isEditing: false - property string displayText: root.value.toString() - onValueChanged: { if (!root.isEditing) { root.displayText = root.value.toString(); @@ -94,11 +94,6 @@ RowLayout { StateLayer { id: upState - color: Colours.palette.m3onPrimary - - onPressAndHold: timer.start() - onReleased: timer.stop() - function onClicked(): void { let newValue = Math.min(root.max, root.value + root.step); // Round to avoid floating point precision errors @@ -108,6 +103,11 @@ RowLayout { root.displayText = newValue.toString(); root.valueModified(newValue); } + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() } MaterialIcon { @@ -129,11 +129,6 @@ RowLayout { StateLayer { id: downState - color: Colours.palette.m3onPrimary - - onPressAndHold: timer.start() - onReleased: timer.stop() - function onClicked(): void { let newValue = Math.max(root.min, root.value - root.step); // Round to avoid floating point precision errors @@ -143,6 +138,11 @@ RowLayout { root.displayText = newValue.toString(); root.valueModified(newValue); } + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() } MaterialIcon { diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index ffb1d0663..2e5f4a3dd 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -53,14 +53,14 @@ StyledRect { StateLayer { id: stateLayer - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - disabled: root.disabled - function onClicked(): void { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled } MaterialIcon { diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index b2bb96cc0..74c8cf2ba 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -45,13 +45,13 @@ StyledRect { StateLayer { id: stateLayer - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - function onClicked(): void { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } RowLayout { diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 3e834aecb..b5a672df5 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -52,14 +52,14 @@ Elevation { color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) StateLayer { - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - disabled: !root.expanded - function onClicked(): void { root.itemSelected(item.modelData); root.active = item.modelData; root.expanded = false; } + + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + disabled: !root.expanded } RowLayout { diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index c91474eae..d8d256bf1 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -47,14 +47,14 @@ Row { StateLayer { id: stateLayer + function onClicked(): void { + root.active?.clicked(); + } + rect.topRightRadius: parent.topRightRadius rect.bottomRightRadius: parent.bottomRightRadius color: root.textColour disabled: root.disabled - - function onClicked(): void { - root.active?.clicked(); - } } RowLayout { @@ -109,14 +109,14 @@ Row { StateLayer { id: expandStateLayer + function onClicked(): void { + root.expanded = !root.expanded; + } + rect.topLeftRadius: parent.topLeftRadius rect.bottomLeftRadius: parent.bottomLeftRadius color: root.textColour disabled: root.disabled - - function onClicked(): void { - root.expanded = !root.expanded; - } } MaterialIcon { diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index b72fc77f3..d0a242bf6 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -24,13 +24,13 @@ RadioButton { anchors.verticalCenter: parent.verticalCenter StateLayer { - anchors.margins: -Appearance.padding.smaller - color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary - z: -1 - function onClicked(): void { root.click(); } + + anchors.margins: -Appearance.padding.smaller + color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary + z: -1 } StyledRect { diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index de8b679cd..c2831da4c 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -11,6 +11,8 @@ ScrollBar { property bool shouldBeActive property real nonAnimPosition property bool animating + property bool _updatingFromFlickable: false + property bool _updatingFromUser: false onHoveredChanged: { if (hovered) @@ -19,9 +21,6 @@ ScrollBar { shouldBeActive = flickable.moving; } - property bool _updatingFromFlickable: false - property bool _updatingFromUser: false - // Sync nonAnimPosition with Qt's automatic position binding onPositionChanged: { if (_updatingFromUser) { @@ -118,13 +117,14 @@ ScrollBar { CustomMouseArea { id: fullMouse - anchors.fill: parent - preventStealing: true - - onPressed: event => { + function onWheel(event: WheelEvent): void { root.animating = true; root._updatingFromUser = true; - const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + let newPos = root.nonAnimPosition; + if (event.angleDelta.y > 0) + newPos = Math.max(0, root.nonAnimPosition - 0.1); + else if (event.angleDelta.y < 0) + newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); root.nonAnimPosition = newPos; // Update flickable position // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] @@ -140,7 +140,11 @@ ScrollBar { } } - onPositionChanged: event => { + anchors.fill: parent + preventStealing: true + + onPressed: event => { + root.animating = true; root._updatingFromUser = true; const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); root.nonAnimPosition = newPos; @@ -158,14 +162,9 @@ ScrollBar { } } - function onWheel(event: WheelEvent): void { - root.animating = true; + onPositionChanged: event => { root._updatingFromUser = true; - let newPos = root.nonAnimPosition; - if (event.angleDelta.y > 0) - newPos = Math.max(0, root.nonAnimPosition - 0.1); - else if (event.angleDelta.y < 0) - newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); root.nonAnimPosition = newPos; // Update flickable position // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index ecf7eb133..40419e489 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -56,13 +56,13 @@ StyledRect { StateLayer { id: stateLayer - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - function onClicked(): void { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } StyledText { diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index dd42af25f..8286ccae8 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -46,11 +46,11 @@ StyledRect { StateLayer { id: toggleStateLayer - color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] - function onClicked(): void { root.clicked(); } + + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] } RowLayout { diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index b129a37b9..8d16fd486 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -24,6 +24,43 @@ Popup { onTriggered: root.tooltipVisible = false } + function updatePosition() { + if (!target || !parent) + return; + + // Wait for tooltipRect to have its size calculated + Qt.callLater(() => { + if (!target || !parent || !tooltipRect) + return; + + // Get target position in parent's coordinate system + const targetPos = target.mapToItem(parent, 0, 0); + const targetCenterX = targetPos.x + target.width / 2; + + // Get tooltip size (use width/height if available, otherwise implicit) + const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth; + const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight; + + // Center tooltip horizontally on target + let newX = targetCenterX - tooltipWidth / 2; + + // Position tooltip above target + let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; + + // Keep within bounds + const padding = Appearance.padding.normal; + if (newX < padding) { + newX = padding; + } else if (newX + tooltipWidth > (parent.width - padding)) { + newX = parent.width - tooltipWidth - padding; + } + + // Update popup position + x = newX; + y = newY; + }); + } + // Popup properties - doesn't affect layout parent: { let p = target; @@ -73,43 +110,6 @@ Popup { } } - function updatePosition() { - if (!target || !parent) - return; - - // Wait for tooltipRect to have its size calculated - Qt.callLater(() => { - if (!target || !parent || !tooltipRect) - return; - - // Get target position in parent's coordinate system - const targetPos = target.mapToItem(parent, 0, 0); - const targetCenterX = targetPos.x + target.width / 2; - - // Get tooltip size (use width/height if available, otherwise implicit) - const tooltipWidth = tooltipRect.width > 0 ? tooltipRect.width : tooltipRect.implicitWidth; - const tooltipHeight = tooltipRect.height > 0 ? tooltipRect.height : tooltipRect.implicitHeight; - - // Center tooltip horizontally on target - let newX = targetCenterX - tooltipWidth / 2; - - // Position tooltip above target - let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; - - // Keep within bounds - const padding = Appearance.padding.normal; - if (newX < padding) { - newX = padding; - } else if (newX + tooltipWidth > (parent.width - padding)) { - newX = parent.width - tooltipWidth - padding; - } - - // Update popup position - x = newX; - y = newY; - }); - } - enter: Transition { Anim { property: "opacity" diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index bde9ac277..ff24efdb8 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -49,11 +49,11 @@ StyledRect { implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 StateLayer { - disabled: !root.dialog.selectionValid - function onClicked(): void { root.dialog.accepted(root.folder.currentItem.modelData.path); } + + disabled: !root.dialog.selectionValid } StyledText { diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index 43184bfd1..404711643 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -28,12 +28,12 @@ StyledRect { implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 StateLayer { - radius: Appearance.rounding.small - disabled: root.dialog.cwd.length === 1 - function onClicked(): void { root.dialog.cwd.pop(); } + + radius: Appearance.rounding.small + disabled: root.dialog.cwd.length === 1 } MaterialIcon { @@ -94,11 +94,11 @@ StyledRect { anchors.fill: parent active: folder.index < root.dialog.cwd.length - 1 sourceComponent: StateLayer { - radius: Appearance.rounding.small - function onClicked(): void { root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); } + + radius: Appearance.rounding.small } } diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index b55d7b379..4e83318b1 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -51,14 +51,14 @@ StyledRect { color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) StateLayer { - color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - function onClicked(): void { if (place.modelData === "Home") root.dialog.cwd = ["Home"]; else root.dialog.cwd = ["Home", place.modelData]; } + + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } RowLayout { diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 67511d860..ba9677cf0 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -79,12 +79,12 @@ Item { } StateLayer { - radius: parent.radius - color: Colours.palette.m3onPrimary - function onClicked(): void { dialog.open(); } + + radius: parent.radius + color: Colours.palette.m3onPrimary } StyledText { diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index fe89c9e86..9f0162d30 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -65,11 +65,11 @@ Item { Layout.alignment: Qt.AlignVCenter StateLayer { - radius: Appearance.rounding.normal - function onClicked(): void { root.wrapper.detach("winfo"); } + + radius: Appearance.rounding.normal } MaterialIcon { diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 78bd324e3..7c68f4d3c 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -205,12 +205,12 @@ Column { implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 StateLayer { - radius: Appearance.rounding.full - color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - function onClicked(): void { PowerProfiles.profile = parent.profile; } + + radius: Appearance.rounding.full + color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 91ac56049..4201ad305 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -117,12 +117,12 @@ ColumnLayout { } StateLayer { - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: device.loading - function onClicked(): void { device.modelData.connected = !device.modelData.connected; } + + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: device.loading } MaterialIcon { @@ -149,11 +149,11 @@ ColumnLayout { implicitHeight: connectBtn.implicitHeight StateLayer { - radius: Appearance.rounding.full - function onClicked(): void { device.modelData.forget(); } + + radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 5b32e4a6e..367b9ab87 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -123,9 +123,6 @@ ColumnLayout { } StateLayer { - color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Nmcli.wifiEnabled - function onClicked(): void { if (networkItem.modelData.active) { Nmcli.disconnectFromNetwork(); @@ -142,6 +139,9 @@ ColumnLayout { // This is handled by the onActiveChanged connection below } } + + color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: networkItem.loading || !Nmcli.wifiEnabled } MaterialIcon { @@ -173,12 +173,12 @@ ColumnLayout { color: Colours.palette.m3primaryContainer StateLayer { - color: Colours.palette.m3onPrimaryContainer - disabled: Nmcli.scanning || !Nmcli.wifiEnabled - function onClicked(): void { Nmcli.rescanWifi(); } + + color: Colours.palette.m3onPrimaryContainer + disabled: Nmcli.scanning || !Nmcli.wifiEnabled } RowLayout { @@ -303,9 +303,6 @@ ColumnLayout { } StateLayer { - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: ethernetItem.loading - function onClicked(): void { if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); @@ -313,6 +310,9 @@ ColumnLayout { Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); } } + + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading } MaterialIcon { diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index a6680f44e..eac73eeb1 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -91,13 +91,6 @@ StackView { implicitHeight: label.implicitHeight StateLayer { - anchors.margins: -Appearance.padding.small / 2 - anchors.leftMargin: -Appearance.padding.smaller - anchors.rightMargin: -Appearance.padding.smaller - - radius: item.radius - disabled: !item.modelData.enabled - function onClicked(): void { const entry = item.modelData; if (entry.hasChildren) @@ -110,6 +103,13 @@ StackView { root.popouts.hasCurrent = false; } } + + anchors.margins: -Appearance.padding.small / 2 + anchors.leftMargin: -Appearance.padding.smaller + anchors.rightMargin: -Appearance.padding.smaller + + radius: item.radius + disabled: !item.modelData.enabled } Loader { @@ -191,12 +191,12 @@ StackView { color: Colours.palette.m3secondaryContainer StateLayer { - radius: parent.radius - color: Colours.palette.m3onSecondaryContainer - function onClicked(): void { root.pop(); } + + radius: parent.radius + color: Colours.palette.m3onSecondaryContainer } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 54e635da0..c6b37b63a 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -18,6 +18,57 @@ ColumnLayout { readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + // Use Timer for actual delay + connectionSuccessTimer.start(); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.wrapper.currentName === "wirelesspassword") { + root.wrapper.currentName = "network"; + } + } + Connections { target: root.wrapper function onCurrentNameChanged() { @@ -305,13 +356,13 @@ ColumnLayout { } StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - radius: Appearance.rounding.normal - function onClicked(): void { passwordContainer.forceActiveFocus(); } + + hoverEnabled: false + cursorShape: Qt.IBeamCursor + radius: Appearance.rounding.normal } StyledText { @@ -495,39 +546,6 @@ ColumnLayout { } } - function checkConnectionStatus(): void { - if (!root.shouldBeVisible || !connectButton.connecting) { - return; - } - - // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - // Successfully connected - give it a moment for network list to update - // Use Timer for actual delay - connectionSuccessTimer.start(); - return; - } - - // Check for connection failures - if pending connection was cleared but we're not connected - if (Nmcli.pendingConnection === null && connectButton.connecting) { - // Wait a bit more before giving up (allow time for connection to establish) - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - Timer { id: connectionMonitor @@ -590,22 +608,4 @@ ColumnLayout { } } } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - - // Return to network popout - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } - } } diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index aea25ceb8..aba6e0cd2 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -16,18 +16,19 @@ ColumnLayout { required property Item wrapper + function refresh() { + kb.refresh(); + } + spacing: Appearance.spacing.small width: Config.bar.sizes.kbLayoutWidth + Component.onCompleted: kb.start() + KbLayoutModel { id: kb } - function refresh() { - kb.refresh(); - } - Component.onCompleted: kb.start() - StyledText { Layout.topMargin: Appearance.padding.normal Layout.rightMargin: Appearance.padding.small @@ -97,6 +98,11 @@ ColumnLayout { StateLayer { id: layer + function onClicked(): void { + if (!isDisabled) + kb.switchTo(layoutIndex); + } + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -104,11 +110,6 @@ ColumnLayout { radius: Appearance.rounding.full enabled: !isDisabled - - function onClicked(): void { - if (!isDisabled) - kb.switchTo(layoutIndex); - } } StyledText { diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 0d95b7a8e..02042195a 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -36,33 +36,6 @@ Item { _switchProc.running = true; } - ListModel { - id: _layoutsModel - } - - property var _xkbMap: ({}) - property bool _notifiedLimit: false - - Process { - id: _xkbXmlBase - - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] - stdout: StdioCollector { - onStreamFinished: _buildXmlMap(text) - } - onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) - _xkbXmlEvdev.running = true - } - - Process { - id: _xkbXmlEvdev - - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] - stdout: StdioCollector { - onStreamFinished: _buildXmlMap(text) - } - } - function _buildXmlMap(xml) { const map = {}; @@ -108,6 +81,78 @@ Item { return `${lang} (${code})`; } + function _setLayouts(raw) { + const parts = raw.split(",").map(s => s.trim()).filter(Boolean); + _layoutsModel.clear(); + + const seen = new Set(); + let idx = 0; + + for (const p of parts) { + if (seen.has(p)) + continue; + seen.add(p); + _layoutsModel.append({ + layoutIndex: idx, + token: p, + label: _pretty(p) + }); + idx++; + } + } + + function _rebuildVisible() { + _visibleModel.clear(); + + let arr = []; + for (let i = 0; i < _layoutsModel.count; i++) + arr.push(_layoutsModel.get(i)); + + arr = arr.filter(i => i.layoutIndex !== activeIndex); + arr.forEach(i => _visibleModel.append(i)); + + if (!Config.utilities.toasts.kbLimit) + return; + + if (_layoutsModel.count > 4) { + Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); + } + } + + function _pretty(token) { + const code = token.replace(/\(.*\)$/, "").trim(); + if (_xkbMap[code]) + return code.toUpperCase() + " - " + _xkbMap[code]; + return code.toUpperCase() + " - " + code; + } + + ListModel { + id: _layoutsModel + } + + property var _xkbMap: ({}) + property bool _notifiedLimit: false + + Process { + id: _xkbXmlBase + + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] + stdout: StdioCollector { + onStreamFinished: _buildXmlMap(text) + } + onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) + _xkbXmlEvdev.running = true + } + + Process { + id: _xkbXmlEvdev + + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] + stdout: StdioCollector { + onStreamFinished: _buildXmlMap(text) + } + } + Process { id: _getKbLayoutOpt @@ -175,49 +220,4 @@ Item { onRunningChanged: if (!running) _fetchActiveLayouts.running = true } - - function _setLayouts(raw) { - const parts = raw.split(",").map(s => s.trim()).filter(Boolean); - _layoutsModel.clear(); - - const seen = new Set(); - let idx = 0; - - for (const p of parts) { - if (seen.has(p)) - continue; - seen.add(p); - _layoutsModel.append({ - layoutIndex: idx, - token: p, - label: _pretty(p) - }); - idx++; - } - } - - function _rebuildVisible() { - _visibleModel.clear(); - - let arr = []; - for (let i = 0; i < _layoutsModel.count; i++) - arr.push(_layoutsModel.get(i)); - - arr = arr.filter(i => i.layoutIndex !== activeIndex); - arr.forEach(i => _visibleModel.append(i)); - - if (!Config.utilities.toasts.kbLimit) - return; - - if (_layoutsModel.count > 4) { - Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); - } - } - - function _pretty(token) { - const code = token.replace(/\(.*\)$/, "").trim(); - if (_xkbMap[code]) - return code.toUpperCase() + " - " + _xkbMap[code]; - return code.toUpperCase() + " - " + code; - } } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index ef338b207..1de1e19c3 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -59,8 +59,6 @@ Item { StateLayer { id: normalWinState - color: Colours.palette.m3onPrimaryContainer - function onClicked(): void { root.session.root.close(); WindowFactory.create(null, { @@ -68,6 +66,8 @@ Item { navExpanded: root.session.navExpanded }); } + + color: Colours.palette.m3onPrimaryContainer } MaterialIcon { @@ -175,8 +175,6 @@ Item { implicitHeight: icon.implicitHeight + Appearance.padding.small StateLayer { - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - function onClicked(): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!root.initialOpeningComplete) { @@ -184,6 +182,8 @@ Item { } root.session.active = item.label; } + + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index a05e6aa65..5aacd89d3 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -101,10 +101,6 @@ ClippingRectangle { required property int paneIndex required property string componentPath - - implicitWidth: root.width - implicitHeight: root.height - property bool hasBeenLoaded: false function updateActive(): void { @@ -127,6 +123,9 @@ ClippingRectangle { loader.active = shouldBeActive; } + implicitWidth: root.width + implicitHeight: root.height + Loader { id: loader diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index fb7160893..0364fab43 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -34,11 +34,11 @@ StyledRect { implicitHeight: closeIcon.implicitHeight + Appearance.padding.small StateLayer { - radius: Appearance.rounding.full - function onClicked(): void { QsWindow.window.destroy(); } + + radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 94ca1cb7d..2b32ec031 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -54,8 +54,6 @@ Item { property real visualiserRounding: Config.background.visualiser.rounding ?? 1 property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 - anchors.fill: parent - function saveConfig() { Config.appearance.anim.durations.scale = root.animDurationsScale; @@ -97,6 +95,8 @@ Item { Config.save(); } + anchors.fill: parent + Component { id: appearanceRightContentComponent @@ -167,14 +167,13 @@ Item { ColumnLayout { id: sidebarLayout + readonly property var rootPane: sidebarFlickable.rootPane + readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded + anchors.left: parent.left anchors.right: parent.right spacing: Appearance.spacing.small - readonly property var rootPane: sidebarFlickable.rootPane - - readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded - RowLayout { spacing: Appearance.spacing.smaller diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 9d6bc6ebc..08186d03e 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -55,9 +55,6 @@ CollapsibleSection { SectionContainer { id: posContainer - contentSpacing: Appearance.spacing.small - z: 1 - readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') readonly property string currentV: pos[0] readonly property string currentH: pos[1] @@ -67,6 +64,9 @@ CollapsibleSection { rootPane.saveConfig(); } + contentSpacing: Appearance.spacing.small + z: 1 + StyledText { text: qsTr("Positioning") font.pointSize: Appearance.font.size.larger diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index cb216ce2c..9b728fcf6 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -241,13 +241,13 @@ StyledFlickable { scale: root.session.bt.editingDeviceName ? 1 : 0.5 StateLayer { - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingDeviceName - function onClicked(): void { root.session.bt.editingDeviceName = false; deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); } + + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingDeviceName } MaterialIcon { @@ -279,8 +279,6 @@ StyledFlickable { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - function onClicked(): void { root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; if (root.session.bt.editingDeviceName) @@ -288,6 +286,8 @@ StyledFlickable { else deviceNameEdit.accepted(); } + + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { @@ -630,11 +630,11 @@ StyledFlickable { StateLayer { id: fabState - color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - function onClicked(): void { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } + + color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer } MaterialIcon { diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 2a2bde934..a943f2806 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -222,9 +222,6 @@ DeviceList { } StateLayer { - color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - disabled: device.loading - function onClicked(): void { if (device.loading) return; @@ -239,6 +236,9 @@ DeviceList { } } } + + color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + disabled: device.loading } MaterialIcon { diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index c5472406b..557e671b9 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -131,11 +131,11 @@ ColumnLayout { implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 StateLayer { - radius: Appearance.rounding.small - function onClicked(): void { adapterPickerButton.expanded = !adapterPickerButton.expanded; } + + radius: Appearance.rounding.small } RowLayout { @@ -210,12 +210,12 @@ ColumnLayout { implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 StateLayer { - disabled: !adapterPickerButton.expanded - function onClicked(): void { adapterPickerButton.expanded = false; root.session.bt.currentAdapter = adapter.modelData; } + + disabled: !adapterPickerButton.expanded } RowLayout { @@ -381,13 +381,13 @@ ColumnLayout { scale: root.session.bt.editingAdapterName ? 1 : 0.5 StateLayer { - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingAdapterName - function onClicked(): void { root.session.bt.editingAdapterName = false; adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } + + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingAdapterName } MaterialIcon { @@ -419,8 +419,6 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { - color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - function onClicked(): void { root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; if (root.session.bt.editingAdapterName) @@ -428,6 +426,8 @@ ColumnLayout { else adapterNameEdit.accepted(); } + + color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 8b71809e2..500dd8218 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -41,16 +41,16 @@ GridView { readonly property real itemRadius: Appearance.rounding.normal StateLayer { + function onClicked(): void { + Wallpapers.setWallpaper(modelData.path); + } + anchors.fill: parent anchors.leftMargin: itemMargin anchors.rightMargin: itemMargin anchors.topMargin: itemMargin anchors.bottomMargin: itemMargin radius: itemRadius - - function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); - } } StyledClippingRect { diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 489af7814..d7186a790 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -40,8 +40,6 @@ Item { property bool showStorage: Config.dashboard.performance.showStorage ?? true property bool showNetwork: Config.dashboard.performance.showNetwork ?? true - anchors.fill: parent - function saveConfig() { Config.dashboard.enabled = root.enabled; Config.dashboard.showOnHover = root.showOnHover; @@ -62,6 +60,8 @@ Item { Config.save(); } + anchors.fill: parent + ClippingRectangle { id: dashboardClippingRect diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 17e89aae0..5ce5139d9 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -25,21 +25,8 @@ Item { property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false property bool favouriteChecked: false - - anchors.fill: parent - - onSelectedAppChanged: { - root.session.launcher.active = root.selectedApp; - updateToggleState(); - } - - Connections { - target: root.session.launcher - function onActiveChanged() { - root.selectedApp = root.session.launcher.active; - updateToggleState(); - } - } + property string searchText: "" + property list filteredApps: [] function updateToggleState() { if (!root.selectedApp) { @@ -78,16 +65,6 @@ Item { Config.save(); } - AppDb { - id: allAppsDb - - path: `${Paths.state}/apps.sqlite` - favouriteApps: Config.launcher.favouriteApps - entries: DesktopEntries.applications.values - } - - property string searchText: "" - function filterApps(search: string): list { if (!search || search.trim() === "") { const apps = []; @@ -120,12 +97,33 @@ Item { return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); } - property list filteredApps: [] - function updateFilteredApps() { filteredApps = filterApps(searchText); } + anchors.fill: parent + + onSelectedAppChanged: { + root.session.launcher.active = root.selectedApp; + updateToggleState(); + } + + Connections { + target: root.session.launcher + function onActiveChanged() { + root.selectedApp = root.session.launcher.active; + updateToggleState(); + } + } + + AppDb { + id: allAppsDb + + path: `${Paths.state}/apps.sqlite` + favouriteApps: Config.launcher.favouriteApps + entries: DesktopEntries.applications.values + } + onSearchTextChanged: { updateFilteredApps(); } @@ -308,11 +306,11 @@ Item { delegate: StyledRect { required property var modelData - width: parent ? parent.width : 0 - implicitHeight: 40 - readonly property bool isSelected: root.selectedApp === modelData + width: parent ? parent.width : 0 + implicitHeight: 40 + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal @@ -418,6 +416,8 @@ Item { Loader { id: rightLauncherLoader + property var displayedApp: rightLauncherPane.displayedApp + anchors.fill: parent asynchronous: true @@ -429,8 +429,6 @@ Item { sourceComponent: rightLauncherPane.targetComponent active: true - property var displayedApp: rightLauncherPane.displayedApp - onItemChanged: { if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { rightLauncherPane.displayedApp = rightLauncherPane.pane; @@ -515,10 +513,9 @@ Item { ColumnLayout { id: appDetailsLayout - anchors.fill: parent - readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null + anchors.fill: parent spacing: Appearance.spacing.normal SettingsHeader { diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index d1eb95798..87e00015d 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -147,8 +147,6 @@ DeviceList { color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - function onClicked(): void { if (modelData.connected && modelData.connection) { Nmcli.disconnectEthernet(modelData.connection, () => {}); @@ -156,6 +154,8 @@ DeviceList { Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); } } + + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index dd41dde5b..23e4010b4 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -201,6 +201,10 @@ DeviceDetails { property string displayName: "" property string interfaceName: "" + function closeWithAnimation(): void { + close(); + } + parent: Overlay.overlay anchors.centerIn: parent width: Math.min(400, parent.width - Appearance.padding.large * 2) @@ -246,10 +250,6 @@ DeviceDetails { } } - function closeWithAnimation(): void { - close(); - } - Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 0d09001ec..9c3366747 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -181,7 +181,6 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) StateLayer { - enabled: !VPN.connecting function onClicked(): void { const clickedIndex = modelData.index; @@ -216,6 +215,8 @@ ColumnLayout { } } } + + enabled: !VPN.connecting } MaterialIcon { @@ -271,6 +272,43 @@ ColumnLayout { property string displayName: "" property string interfaceName: "" + function showProviderSelection(): void { + currentState = "selection"; + open(); + } + + function closeWithAnimation(): void { + close(); + } + + function showAddForm(providerType: string, defaultDisplayName: string): void { + editIndex = -1; + providerName = providerType; + displayName = defaultDisplayName; + interfaceName = ""; + + if (currentState === "selection") { + transitionToForm.start(); + } else { + currentState = "form"; + isClosing = false; + open(); + } + } + + function showEditForm(index: int): void { + const provider = Config.utilities.vpn.provider[index]; + const isObject = typeof provider === "object"; + + editIndex = index; + providerName = isObject ? (provider.name || "custom") : String(provider); + displayName = isObject ? (provider.displayName || providerName) : providerName; + interfaceName = isObject ? (provider.interface || "") : ""; + + currentState = "form"; + open(); + } + parent: Overlay.overlay x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) @@ -321,43 +359,6 @@ ColumnLayout { } } - function showProviderSelection(): void { - currentState = "selection"; - open(); - } - - function closeWithAnimation(): void { - close(); - } - - function showAddForm(providerType: string, defaultDisplayName: string): void { - editIndex = -1; - providerName = providerType; - displayName = defaultDisplayName; - interfaceName = ""; - - if (currentState === "selection") { - transitionToForm.start(); - } else { - currentState = "form"; - isClosing = false; - open(); - } - } - - function showEditForm(index: int): void { - const provider = Config.utilities.vpn.provider[index]; - const isObject = typeof provider === "object"; - - editIndex = index; - providerName = isObject ? (provider.name || "custom") : String(provider); - displayName = isObject ? (provider.displayName || providerName) : providerName; - interfaceName = isObject ? (provider.interface || "") : ""; - - currentState = "form"; - open(); - } - Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 7a666f339..09d6a826f 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -29,6 +29,47 @@ Item { } property bool isClosing: false + + function checkConnectionStatus(): void { + if (!root.visible || !connectButton.connecting) { + return; + } + + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + connectionSuccessTimer.start(); + return; + } + + if (Nmcli.pendingConnection === null && connectButton.connecting) { + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + } + visible: session.network.showPasswordDialog || isClosing enabled: session.network.showPasswordDialog && !isClosing focus: enabled @@ -238,12 +279,12 @@ Item { } StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - function onClicked(): void { passwordContainer.forceActiveFocus(); } + + hoverEnabled: false + cursorShape: Qt.IBeamCursor } StyledText { @@ -416,33 +457,6 @@ Item { } } - function checkConnectionStatus(): void { - if (!root.visible || !connectButton.connecting) { - return; - } - - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - connectionSuccessTimer.start(); - return; - } - - if (Nmcli.pendingConnection === null && connectButton.connecting) { - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - Timer { id: connectionMonitor @@ -499,17 +513,4 @@ Item { } } } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - } } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index f18ac8941..7fcdec9e5 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -53,21 +53,6 @@ Item { property list monitorNames: Hypr.monitorNames() property list excludedScreens: Config.bar.excludedScreens ?? [] - anchors.fill: parent - - Component.onCompleted: { - if (Config.bar.entries) { - entriesModel.clear(); - for (let i = 0; i < Config.bar.entries.length; i++) { - const entry = Config.bar.entries[i]; - entriesModel.append({ - id: entry.id, - enabled: entry.enabled !== false - }); - } - } - } - function saveConfig(entryIndex, entryEnabled) { Config.bar.activeWindow.compact = root.activeWindowCompact; Config.bar.activeWindow.inverted = root.activeWindowInverted; @@ -118,6 +103,21 @@ Item { Config.save(); } + anchors.fill: parent + + Component.onCompleted: { + if (Config.bar.entries) { + entriesModel.clear(); + for (let i = 0; i < Config.bar.entries.length; i++) { + const entry = Config.bar.entries[i]; + entriesModel.append({ + id: entry.id, + enabled: entry.enabled !== false + }); + } + } + } + ListModel { id: entriesModel } diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index fe984299d..868eb8d30 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -12,16 +12,16 @@ StyledRect { required property real contentHeight - implicitHeight: contentHeight - - radius: Appearance.rounding.large - color: Colours.tPalette.m3surfaceContainer - function searchCandidates(title, artist) { LyricsService.currentRequestId++; LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); } + implicitHeight: contentHeight + + radius: Appearance.rounding.large + color: Colours.tPalette.m3surfaceContainer + Loader { asynchronous: true anchors.fill: parent diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 6e09e767a..d1c78cbd2 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -111,6 +111,13 @@ Item { contentItem: CustomMouseArea { id: mouse + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y < 0) + root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); + else if (event.angleDelta.y > 0) + root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + } + implicitWidth: Math.max(icon.width, label.width) implicitHeight: icon.height + label.height @@ -129,13 +136,6 @@ Item { rippleAnim.restart(); } - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y < 0) - root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); - else if (event.angleDelta.y > 0) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); - } - SequentialAnimation { id: rippleAnim diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index 56c04938c..bf64ac5f7 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -17,13 +17,6 @@ CustomMouseArea { readonly property int currMonth: state.currentDate.getMonth() readonly property int currYear: state.currentDate.getFullYear() - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 - - acceptedButtons: Qt.MiddleButton - onClicked: root.state.currentDate = new Date() - function onWheel(event: WheelEvent): void { if (event.angleDelta.y > 0) root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); @@ -31,6 +24,13 @@ CustomMouseArea { root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 + + acceptedButtons: Qt.MiddleButton + onClicked: root.state.currentDate = new Date() + ColumnLayout { id: inner @@ -51,11 +51,11 @@ CustomMouseArea { StateLayer { id: prevMonthStateLayer - radius: Appearance.rounding.full - function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); } + + radius: Appearance.rounding.full } MaterialIcon { @@ -76,6 +76,10 @@ CustomMouseArea { implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 StateLayer { + function onClicked(): void { + root.state.currentDate = new Date(); + } + anchors.fill: monthYearDisplay anchors.margins: -Appearance.padding.small anchors.leftMargin: -Appearance.padding.normal @@ -86,10 +90,6 @@ CustomMouseArea { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); } - - function onClicked(): void { - root.state.currentDate = new Date(); - } } StyledText { @@ -111,11 +111,11 @@ CustomMouseArea { StateLayer { id: nextMonthStateLayer - radius: Appearance.rounding.full - function onClicked(): void { root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } + + radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index d65066954..2891bd559 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -231,12 +231,12 @@ Item { implicitHeight: implicitWidth StateLayer { - disabled: !control.canUse - radius: Appearance.rounding.full - function onClicked(): void { control.onClicked(); } + + disabled: !control.canUse + radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 5ede24bb5..518ec8aa6 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -71,12 +71,12 @@ Row { opacity: parent.containsMouse ? 1 : 0 StateLayer { - color: Colours.palette.m3onPrimary - function onClicked(): void { root.visibilities.launcher = false; root.facePicker.open(); } + + color: Colours.palette.m3onPrimary } MaterialIcon { diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index e15802907..c3cd48a0a 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -16,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - function onClicked(): void { root.modelData?.onClicked(root.list); } + + radius: Appearance.rounding.normal } Item { diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 75c03a199..84ee06a60 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -19,12 +19,12 @@ Item { anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - function onClicked(): void { Apps.launch(root.modelData); root.visibilities.launcher = false; } + + radius: Appearance.rounding.normal } Item { diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 28a05327a..4c27c5c7f 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -12,27 +12,27 @@ Item { required property var list readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) - onMathChanged: { - if (math.length > 0) - Qalculator.evalAsync(math); - } - function onClicked(): void { Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); root.list.visibilities.launcher = false; } + onMathChanged: { + if (math.length > 0) + Qalculator.evalAsync(math); + } + implicitHeight: Config.launcher.sizes.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - function onClicked(): void { root.onClicked(); } + + radius: Appearance.rounding.normal } RowLayout { @@ -80,12 +80,12 @@ Item { StateLayer { id: stateLayer - color: Colours.palette.m3onTertiary - function onClicked(): void { Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); root.list.visibilities.launcher = false; } + + color: Colours.palette.m3onTertiary } StyledText { diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index aade35f21..65bd65260 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -16,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - function onClicked(): void { root.modelData?.onClicked(root.list); } + + radius: Appearance.rounding.normal } Item { diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index 34cc87faf..53bffe15c 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -16,11 +16,11 @@ Item { anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - function onClicked(): void { root.modelData?.onClicked(root.list); } + + radius: Appearance.rounding.normal } Item { diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 9fdac3f38..1a2eda5f3 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -26,12 +26,12 @@ Item { implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal StateLayer { - radius: Appearance.rounding.normal - function onClicked(): void { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; } + + radius: Appearance.rounding.normal } Elevation { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 07b80d3b2..5d4dec859 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -135,12 +135,12 @@ ColumnLayout { } StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - function onClicked(): void { parent.forceActiveFocus(); } + + hoverEnabled: false + cursorShape: Qt.IBeamCursor } RowLayout { @@ -194,11 +194,11 @@ ColumnLayout { radius: Appearance.rounding.full StateLayer { - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - function onClicked(): void { root.lock.pam.passwd.start(); } + + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index b7e58bbcb..07ec8c5bd 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -171,11 +171,11 @@ Item { StateLayer { id: controlState - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - function onClicked(): void { control.onClicked(); } + + color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] } MaterialIcon { diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 2e8ca5424..7f2b62fbb 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -176,11 +176,11 @@ StyledRect { Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 StateLayer { - color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface - function onClicked(): void { root.expanded = !root.expanded; } + + color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 1208f025c..a1bf97d8a 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -361,12 +361,12 @@ StyledRect { implicitHeight: expandIcon.height StateLayer { - radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - function onClicked() { root.expanded = !root.expanded; } + + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { @@ -491,12 +491,12 @@ StyledRect { implicitHeight: actionText.height + Appearance.padding.small * 2 StateLayer { - radius: Appearance.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface - function onClicked(): void { action.modelData.invoke(); } + + radius: Appearance.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface } StyledText { diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 45152e28a..06e6c85ff 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -115,12 +115,12 @@ Column { } StateLayer { - radius: parent.radius - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - function onClicked(): void { Quickshell.execDetached(button.command); } + + radius: parent.radius + color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 6bd2f55c9..6a92ce05f 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -204,11 +204,11 @@ StyledRect { radius: Appearance.rounding.full StateLayer { - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface - function onClicked(): void { root.toggleExpand(!root.expanded); } + + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface } RowLayout { diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index b75612479..7854045ce 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -35,11 +35,11 @@ ColumnLayout { implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small StateLayer { - color: Colours.palette.m3onPrimary - function onClicked(): void { root.moveToWsExpanded = !root.moveToWsExpanded; } + + color: Colours.palette.m3onPrimary } MaterialIcon { @@ -161,11 +161,11 @@ ColumnLayout { StateLayer { id: stateLayer - color: parent.onColor - function onClicked(): void { parent.onClicked(); } + + color: parent.onColor } StyledText { diff --git a/services/Notifs.qml b/services/Notifs.qml index aff2dfc6a..86b906ddc 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -148,30 +148,6 @@ Singleton { property date time: new Date() property string timeStr: qsTr("now") - function updateTimeStr(): void { - const diff = Date.now() - time.getTime(); - const m = Math.floor(diff / 60000); - - if (m < 1) { - timeStr = qsTr("now"); - timeStrTimer.interval = 5000; - } else { - const h = Math.floor(m / 60); - const d = Math.floor(h / 24); - - if (d > 0) { - timeStr = `${d}d`; - timeStrTimer.interval = 3600000; - } else if (h > 0) { - timeStr = `${h}h`; - timeStrTimer.interval = 300000; - } else { - timeStr = `${m}m`; - timeStrTimer.interval = m < 10 ? 30000 : 60000; - } - } - } - readonly property Timer timeStrTimer: Timer { running: !notif.closed repeat: true @@ -308,6 +284,30 @@ Singleton { } } + function updateTimeStr(): void { + const diff = Date.now() - time.getTime(); + const m = Math.floor(diff / 60000); + + if (m < 1) { + timeStr = qsTr("now"); + timeStrTimer.interval = 5000; + } else { + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); + + if (d > 0) { + timeStr = `${d}d`; + timeStrTimer.interval = 3600000; + } else if (h > 0) { + timeStr = `${h}h`; + timeStrTimer.interval = 300000; + } else { + timeStr = `${m}m`; + timeStrTimer.interval = m < 10 ? 30000 : 60000; + } + } + } + function lock(item: Item): void { locks.add(item); } From 72e534bad88a140a6339ffc49d1d5f4fdd4da959 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 04:50:04 +1100 Subject: [PATCH 102/409] feat: add script to check qml format --- scripts/qml-lint-conventions.py | 306 ++++++++++++++++++++++++++++++++ 1 file changed, 306 insertions(+) create mode 100755 scripts/qml-lint-conventions.py diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py new file mode 100755 index 000000000..d49eddeee --- /dev/null +++ b/scripts/qml-lint-conventions.py @@ -0,0 +1,306 @@ +#!/usr/bin/env python3 +"""Checks QML files for Qt coding convention violations. + +https://doc.qt.io/qt-6/qml-codingconventions.html + +Required ordering within each QML object (with blank line between sections): + 1. id + 2. property declarations + 3. signal declarations + 4. JavaScript functions + 5. object properties (bindings) + 6. child objects / component definitions +""" + +import re +import sys +from enum import IntEnum +from pathlib import Path + +RED = "\033[0;31m" +YELLOW = "\033[0;33m" +CYAN = "\033[0;36m" +GREEN = "\033[0;32m" +MAGENTA = "\033[0;35m" +BOLD = "\033[1m" +RESET = "\033[0m" + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +class Section(IntEnum): + ID = 0 + PROPERTY = 1 + SIGNAL = 2 + FUNCTION = 3 + BINDING = 4 + CHILD = 5 + COMPONENT_DEF = 6 + + +SECTION_NAMES = { + Section.ID: "id", + Section.PROPERTY: "property declarations", + Section.SIGNAL: "signal declarations", + Section.FUNCTION: "functions", + Section.BINDING: "bindings", + Section.CHILD: "child objects", + Section.COMPONENT_DEF: "component definitions", +} + +RULE_COLOURS = { + "missing-blank-after-id": RED, + "section-order": YELLOW, + "missing-section-separator": CYAN, + "blank-after-open-brace": MAGENTA, + "blank-before-close-brace": MAGENTA, +} + +# Regexes +PROPERTY_DECL_RE = re.compile( + r"^(?:required\s+|readonly\s+|default\s+)*property\s" +) +SIGNAL_RE = re.compile(r"^signal\s") +FUNCTION_RE = re.compile(r"^function\s") +ID_RE = re.compile(r"^id\s*:\s*[a-zA-Z_]\w*\s*$") +ENUM_RE = re.compile(r"^enum\s") +COMPONENT_DEF_RE = re.compile(r"^component\s+\w+\s*:") +COMMENT_LINE_RE = re.compile(r"^//") +BLOCK_COMMENT_START = re.compile(r"/\*") +BLOCK_COMMENT_END = re.compile(r"\*/") +BINDING_RE = re.compile(r"^[a-z][a-zA-Z0-9_.]*\s*:") +SIGNAL_HANDLER_RE = re.compile(r"^on[A-Z][a-zA-Z]*\s*:") +# Child object: starts with uppercase or is a known child-like pattern +CHILD_OBJECT_RE = re.compile(r"^[A-Z][a-zA-Z0-9_.]*\s*\{") +# Inline component: Component { ... } +INLINE_COMPONENT_RE = re.compile(r"^Component\s*\{") +# Behavior on {, NumberAnimation on {, etc. +BEHAVIOR_ON_RE = re.compile(r"^[A-Z]\w+\s+on\s+\w[\w.]*\s*\{") +# Attached signal handler: Component.onCompleted:, Drag.onDragStarted:, etc. +ATTACHED_HANDLER_RE = re.compile(r"^[A-Z]\w+\.on[A-Z]\w*\s*:") + + +class Violation: + def __init__(self, file: str, line: int, rule: str, msg: str): + self.file = file + self.line = line + self.rule = rule + self.msg = msg + + def __str__(self): + c = RULE_COLOURS.get(self.rule, "") + return f"{c}[{self.rule}]{RESET} {self.file}:{self.line}: {self.msg}" + + +class ScopeTracker: + """Tracks the current section and last-seen state for one indent level.""" + + def __init__(self): + self.last_section: Section | None = None + self.last_section_line: int = 0 + self.had_blank_before_current: bool = True # no separator needed at start + + +def get_indent(line: str) -> str: + return line[: len(line) - len(line.lstrip())] + + +def classify_line(stripped: str) -> Section | None: + """Classify a stripped QML line into a section category.""" + if ID_RE.match(stripped): + return Section.ID + if PROPERTY_DECL_RE.match(stripped): + return Section.PROPERTY + if SIGNAL_RE.match(stripped): + return Section.SIGNAL + if FUNCTION_RE.match(stripped): + return Section.FUNCTION + if ENUM_RE.match(stripped): + return Section.PROPERTY # enums go with declarations + if COMPONENT_DEF_RE.match(stripped): + return Section.COMPONENT_DEF + if BEHAVIOR_ON_RE.match(stripped): + return Section.CHILD + if CHILD_OBJECT_RE.match(stripped): + return Section.CHILD + if INLINE_COMPONENT_RE.match(stripped): + return Section.CHILD + if BINDING_RE.match(stripped) or SIGNAL_HANDLER_RE.match(stripped): + return Section.BINDING + if ATTACHED_HANDLER_RE.match(stripped): + return Section.BINDING + return None + + +def check_file(filepath: Path) -> list[Violation]: + violations = [] + rel = str(filepath.relative_to(REPO_ROOT)) + + try: + lines = filepath.read_text().splitlines() + except (OSError, UnicodeDecodeError): + return violations + + scopes: dict[str, ScopeTracker] = {} # indent -> tracker + in_block_comment = False + func_skip_depth = 0 # brace depth for skipping function bodies only + prev_blank: dict[str, bool] = {} # indent -> was previous relevant line a blank? + + for i, line in enumerate(lines): + lineno = i + 1 + stripped = line.strip() + indent = get_indent(line) + + # Handle block comments + if in_block_comment: + if BLOCK_COMMENT_END.search(stripped): + in_block_comment = False + continue + if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): + in_block_comment = True + continue + + # Track blank lines per indent + if not stripped: + # Check: blank line right after opening brace of a QML object + if (i > 0 and func_skip_depth == 0 and not in_block_comment + and lines[i - 1].strip().endswith("{")): + violations.append(Violation( + rel, lineno, "blank-after-open-brace", + "no blank line expected after opening brace", + )) + for key in prev_blank: + prev_blank[key] = True + continue + + # Skip line comments + if COMMENT_LINE_RE.match(stripped): + continue + + # Skip inside function bodies (JS code, not QML structure) + if func_skip_depth > 0: + func_skip_depth += stripped.count("{") - stripped.count("}") + if func_skip_depth <= 0: + func_skip_depth = 0 + continue + + # Closing brace: pop scope for this indent and all deeper scopes + if stripped == "}": + # Check: blank line right before closing brace + if i > 0 and not lines[i - 1].strip(): + violations.append(Violation( + rel, lineno, "blank-before-close-brace", + "no blank line expected before closing brace", + )) + scopes.pop(indent, None) + prev_blank.pop(indent, None) + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + continue + + section = classify_line(stripped) + if section is None: + continue + + # Get or create scope tracker for this indent + if indent not in scopes: + scopes[indent] = ScopeTracker() + prev_blank[indent] = True # treat start of object as having separator + + tracker = scopes[indent] + had_blank = prev_blank.get(indent, True) + + # --- Check 1: Missing blank line after id --- + if section == Section.ID: + if i + 1 < len(lines): + next_stripped = lines[i + 1].strip() + if next_stripped and next_stripped != "}": + violations.append(Violation( + rel, lineno, "missing-blank-after-id", + "id should be followed by a blank line", + )) + + # --- Check 2: Section ordering --- + if tracker.last_section is not None and section < tracker.last_section: + violations.append(Violation( + rel, lineno, "section-order", + f"{SECTION_NAMES[section]} should appear before " + f"{SECTION_NAMES[tracker.last_section]} " + f"(seen at line {tracker.last_section_line})", + )) + + # --- Check 3: Missing blank line between different sections --- + if (tracker.last_section is not None + and section != tracker.last_section + and not had_blank): + violations.append(Violation( + rel, lineno, "missing-section-separator", + f"blank line expected between {SECTION_NAMES[tracker.last_section]} " + f"and {SECTION_NAMES[section]}", + )) + + # Update tracker + if tracker.last_section is None or section >= tracker.last_section: + tracker.last_section = section + tracker.last_section_line = lineno + + prev_blank[indent] = False + + # Skip function bodies (they contain JS, not QML structure) + brace_count = stripped.count("{") - stripped.count("}") + if brace_count > 0 and section == Section.FUNCTION: + func_skip_depth = brace_count + + # Skip JS blocks in bindings (signal handlers, attached handlers, + # and expression blocks like `color: { ... }`) + if brace_count > 0 and section == Section.BINDING: + colon_idx = stripped.index(":") + after_colon = stripped[colon_idx + 1:].strip() + # If content after : doesn't start with an uppercase type name, + # it's a JS block (not an inline QML object like `contentItem: Rect {`) + if not re.match(r"^[A-Z]", after_colon): + func_skip_depth = brace_count + + # Child object/component opening resets deeper scopes + if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + + return violations + + +def main(): + qml_files = sorted( + p for p in REPO_ROOT.rglob("*.qml") + if "build" not in p.parts + ) + + print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n") + + all_violations: list[Violation] = [] + for f in qml_files: + all_violations.extend(check_file(f)) + + for v in all_violations: + print(v) + + print() + if all_violations: + by_rule: dict[str, int] = {} + for v in all_violations: + by_rule[v.rule] = by_rule.get(v.rule, 0) + 1 + for rule, count in sorted(by_rule.items()): + print(f" {RULE_COLOURS.get(rule, '')}{rule}{RESET}: {count}") + print(f"\n{BOLD}Found {len(all_violations)} violation(s).{RESET}") + return 1 + else: + print(f"{BOLD}No violations found.{RESET}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) From 796c2e4e76031ec6fe1ea86a58af2d0dd44b8f47 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:03:18 +1100 Subject: [PATCH 103/409] chore: fix prop order --- components/controls/CollapsibleSection.qml | 1 + components/controls/StyledScrollBar.qml | 39 +++--- components/controls/StyledTextField.qml | 4 +- components/controls/ToggleButton.qml | 17 ++- components/controls/Tooltip.qml | 78 ++++++------ components/filedialog/CurrentItem.qml | 4 +- components/filedialog/FolderContents.qml | 8 +- components/images/CachingImage.qml | 4 +- modules/BatteryMonitor.qml | 8 +- modules/Shortcuts.qml | 12 +- modules/areapicker/AreaPicker.qml | 4 +- modules/background/Background.qml | 9 ++ modules/bar/components/Power.qml | 9 +- modules/bar/components/Settings.qml | 13 +- modules/bar/components/SettingsIcon.qml | 13 +- .../workspaces/SpecialWorkspaces.qml | 8 +- modules/bar/popouts/Content.qml | 10 +- modules/bar/popouts/Network.qml | 7 +- modules/bar/popouts/WirelessPassword.qml | 73 ++++++------ modules/bar/popouts/Wrapper.qml | 6 +- modules/bar/popouts/kblayout/KbLayout.qml | 12 +- modules/controlcenter/ControlCenter.qml | 7 +- modules/controlcenter/NavRail.qml | 1 + modules/controlcenter/Panes.qml | 20 ++-- modules/controlcenter/WindowFactory.qml | 8 +- .../appearance/AppearancePane.qml | 2 +- .../appearance/sections/BackgroundSection.qml | 18 ++- .../appearance/sections/FontsSection.qml | 9 +- modules/controlcenter/audio/AudioPane.qml | 13 +- .../components/ConnectedButtonGroup.qml | 5 +- .../controlcenter/components/SliderInput.qml | 23 ++-- .../components/SplitPaneLayout.qml | 6 +- .../components/WallpaperGrid.qml | 8 +- .../controlcenter/launcher/LauncherPane.qml | 58 ++++----- .../controlcenter/network/NetworkingPane.qml | 18 +-- modules/controlcenter/network/VpnList.qml | 63 +++++----- .../controlcenter/network/WirelessDetails.qml | 111 +++++++++--------- .../controlcenter/network/WirelessList.qml | 12 +- .../network/WirelessPasswordDialog.qml | 18 +-- modules/dashboard/LyricMenu.qml | 33 +++--- modules/dashboard/LyricsView.qml | 40 +++---- modules/dashboard/Media.qml | 11 +- modules/dashboard/Performance.qml | 4 +- modules/dashboard/Weather.qml | 5 +- modules/dashboard/dash/Media.qml | 19 +-- modules/drawers/Interactions.qml | 4 +- modules/launcher/Content.qml | 4 +- modules/launcher/Wrapper.qml | 8 +- modules/lock/Center.qml | 4 +- modules/lock/InputField.qml | 4 +- modules/lock/Lock.qml | 4 +- modules/lock/LockSurface.qml | 4 +- modules/lock/Media.qml | 18 +-- modules/lock/Pam.qml | 8 +- modules/notifications/Notification.qml | 1 + modules/osd/Content.qml | 18 +-- modules/osd/Wrapper.qml | 8 +- modules/session/Content.qml | 4 +- modules/windowinfo/Buttons.qml | 32 ++--- services/Audio.qml | 4 +- services/Brightness.qml | 4 +- services/GameMode.qml | 8 +- services/Hypr.qml | 12 +- services/IdleInhibitor.qml | 4 +- services/LyricsService.qml | 20 ++-- services/Network.qml | 110 ++++++++--------- services/Nmcli.qml | 4 +- services/Notifs.qml | 8 +- services/Players.qml | 8 +- services/Recorder.qml | 4 +- services/Wallpapers.qml | 4 +- services/Weather.qml | 3 +- utils/SysInfo.qml | 4 +- 73 files changed, 597 insertions(+), 572 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 0e4c3dcf3..9a4c40276 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -53,6 +53,7 @@ ColumnLayout { rotation: root.expanded ? 180 : 0 color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.normal + Behavior on rotation { Anim { duration: Appearance.anim.durations.small diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index c2831da4c..7ec537d08 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -36,24 +36,6 @@ ScrollBar { } } - // Sync nonAnimPosition with flickable when not animating - Connections { - target: flickable - function onContentYChanged() { - if (!animating && !fullMouse.pressed) { - _updatingFromFlickable = true; - const contentHeight = flickable.contentHeight; - const height = flickable.height; - if (contentHeight > height) { - nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); - } else { - nonAnimPosition = 0; - } - _updatingFromFlickable = false; - } - } - } - Component.onCompleted: { if (flickable) { const contentHeight = flickable.contentHeight; @@ -96,15 +78,34 @@ ScrollBar { } } + // Sync nonAnimPosition with flickable when not animating Connections { - target: root.flickable + function onContentYChanged() { + if (!animating && !fullMouse.pressed) { + _updatingFromFlickable = true; + const contentHeight = flickable.contentHeight; + const height = flickable.height; + if (contentHeight > height) { + nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + } else { + nonAnimPosition = 0; + } + _updatingFromFlickable = false; + } + } + target: flickable + } + + Connections { function onMovingChanged(): void { if (root.flickable.moving) root.shouldBeActive = true; else hideDelay.restart(); } + + target: root.flickable } Timer { diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 60bcff259..6f3532dcf 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -28,8 +28,6 @@ TextField { radius: Appearance.rounding.normal Connections { - target: root - function onCursorPositionChanged(): void { if (root.activeFocus && root.cursorVisible) { cursor.opacity = 1; @@ -37,6 +35,8 @@ TextField { enableBlink.restart(); } } + + target: root } Timer { diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 8286ccae8..b05e7f5a7 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -18,30 +18,29 @@ StyledRect { property real horizontalPadding: Appearance.padding.large property real verticalPadding: Appearance.padding.normal property string tooltip: "" - property bool hovered: false + signal clicked Component.onCompleted: { hovered = toggleStateLayer.containsMouse; } + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 + implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 + radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] Connections { - target: toggleStateLayer function onContainsMouseChanged() { const newHovered = toggleStateLayer.containsMouse; if (hovered !== newHovered) { hovered = newHovered; } } - } - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) - implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 - implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 - - radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) - color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + target: toggleStateLayer + } StateLayer { id: toggleStateLayer diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index 8d16fd486..ab9401f63 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -90,23 +90,9 @@ Popup { Qt.callLater(updatePosition); } } - Connections { - target: root.target - function onXChanged() { - if (root.tooltipVisible) - root.updatePosition(); - } - function onYChanged() { - if (root.tooltipVisible) - root.updatePosition(); - } - function onWidthChanged() { - if (root.tooltipVisible) - root.updatePosition(); - } - function onHeightChanged() { - if (root.tooltipVisible) - root.updatePosition(); + Component.onCompleted: { + if (tooltipVisible) { + updatePosition(); } } @@ -130,24 +116,6 @@ Popup { } } - // Monitor hover state - Connections { - target: root.target - function onHoveredChanged() { - if (target.hovered) { - showTimer.start(); - if (timeout > 0) { - hideTimer.stop(); - hideTimer.start(); - } - } else { - showTimer.stop(); - hideTimer.stop(); - tooltipVisible = false; - } - } - } - contentItem: StyledRect { id: tooltipRect @@ -177,9 +145,43 @@ Popup { } } - Component.onCompleted: { - if (tooltipVisible) { - updatePosition(); + Connections { + function onXChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onYChanged() { + if (root.tooltipVisible) + root.updatePosition(); } + function onWidthChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onHeightChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + + target: root.target + } + + // Monitor hover state + Connections { + function onHoveredChanged() { + if (target.hovered) { + showTimer.start(); + if (timeout > 0) { + hideTimer.stop(); + hideTimer.start(); + } + } else { + showTimer.stop(); + hideTimer.stop(); + tooltipVisible = false; + } + } + + target: root.target } } diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index bb87133c7..a33523b7f 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -80,12 +80,12 @@ Item { anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small Connections { - target: root - function onCurrentItemChanged(): void { if (root.currentItem) content.text = qsTr(`"%1" selected`).arg(root.currentItem.modelData.name); } + + target: root } } } diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 00d7a3d92..12d59598e 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -129,16 +129,16 @@ Item { clip: true StateLayer { + function onClicked(): void { + view.currentIndex = item.index; + } + onDoubleClicked: { if (item.modelData.isDir) root.dialog.cwd.push(item.modelData.name); else if (root.dialog.selectionValid) root.dialog.accepted(item.modelData.path); } - - function onClicked(): void { - view.currentIndex = item.index; - } } CachingIconImage { diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index e8f957a7d..83bd5e47a 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -12,11 +12,11 @@ Image { fillMode: Image.PreserveAspectCrop Connections { - target: QsWindow.window - function onDevicePixelRatioChanged(): void { manager.updateSource(); } + + target: QsWindow.window } CachingImageManager { diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index d24cff274..9d6e4e845 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -10,8 +10,6 @@ Scope { readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) Connections { - target: UPower - function onOnBatteryChanged(): void { if (UPower.onBattery) { if (Config.utilities.toasts.chargingChanged) @@ -23,11 +21,11 @@ Scope { level.warned = false; } } + + target: UPower } Connections { - target: UPower.displayDevice - function onPercentageChanged(): void { if (!UPower.onBattery) return; @@ -45,6 +43,8 @@ Scope { hibernateTimer.start(); } } + + target: UPower.displayDevice } Timer { diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index 3bf20a4f6..a769e160c 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -93,8 +93,6 @@ Scope { } IpcHandler { - target: "drawers" - function toggle(drawer: string): void { if (list().split("\n").includes(drawer)) { if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer)) @@ -110,19 +108,19 @@ Scope { const visibilities = Visibilities.getForActive(); return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n"); } + + target: "drawers" } IpcHandler { - target: "controlCenter" - function open(): void { WindowFactory.create(); } + + target: "controlCenter" } IpcHandler { - target: "toaster" - function info(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Info); } @@ -138,5 +136,7 @@ Scope { function error(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Error); } + + target: "toaster" } } diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml index 308b7d232..fc0eaab08 100644 --- a/modules/areapicker/AreaPicker.qml +++ b/modules/areapicker/AreaPicker.qml @@ -48,8 +48,6 @@ Scope { } IpcHandler { - target: "picker" - function open(): void { root.freeze = false; root.closing = false; @@ -77,6 +75,8 @@ Scope { root.clipboardOnly = true; root.activeAsync = true; } + + target: "picker" } CustomShortcut { diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 06a095292..e932f3817 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -68,6 +68,7 @@ Loader { states: [ State { name: "top-left" + AnchorChanges { target: clockLoader anchors.top: parent.top @@ -76,6 +77,7 @@ Loader { }, State { name: "top-center" + AnchorChanges { target: clockLoader anchors.top: parent.top @@ -84,6 +86,7 @@ Loader { }, State { name: "top-right" + AnchorChanges { target: clockLoader anchors.top: parent.top @@ -92,6 +95,7 @@ Loader { }, State { name: "middle-left" + AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -100,6 +104,7 @@ Loader { }, State { name: "middle-center" + AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -108,6 +113,7 @@ Loader { }, State { name: "middle-right" + AnchorChanges { target: clockLoader anchors.verticalCenter: parent.verticalCenter @@ -116,6 +122,7 @@ Loader { }, State { name: "bottom-left" + AnchorChanges { target: clockLoader anchors.bottom: parent.bottom @@ -124,6 +131,7 @@ Loader { }, State { name: "bottom-center" + AnchorChanges { target: clockLoader anchors.bottom: parent.bottom @@ -132,6 +140,7 @@ Loader { }, State { name: "bottom-right" + AnchorChanges { target: clockLoader anchors.bottom: parent.bottom diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 917bdf7fd..dc9ef6935 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -14,16 +14,15 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent + function onClicked(): void { + root.visibilities.session = !root.visibilities.session; + } + anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 - radius: Appearance.rounding.full - - function onClicked(): void { - root.visibilities.session = !root.visibilities.session; - } } MaterialIcon { diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index 5d562cef1..f193c7c14 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -13,18 +13,17 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent - anchors.fill: undefined - anchors.centerIn: parent - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 - - radius: Appearance.rounding.full - function onClicked(): void { WindowFactory.create(null, { active: "network" }); } + + anchors.fill: undefined + anchors.centerIn: parent + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index 5d562cef1..f193c7c14 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -13,18 +13,17 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent - anchors.fill: undefined - anchors.centerIn: parent - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 - - radius: Appearance.rounding.full - function onClicked(): void { WindowFactory.create(null, { active: "network" }); } + + anchors.fill: undefined + anchors.centerIn: parent + implicitWidth: implicitHeight + implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full } MaterialIcon { diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 555bb3b46..03394463f 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -134,8 +134,6 @@ Item { // Hacky thing cause modelData gets destroyed before the remove anim finishes Connections { - target: ws.modelData - function onIdChanged(): void { if (ws.modelData) ws.wsId = ws.modelData.id; @@ -150,15 +148,17 @@ Item { if (ws.modelData) ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; } + + target: ws.modelData } Connections { - target: Config.bar.workspaces - function onShowWindowsOnSpecialWorkspacesChanged(): void { if (ws.modelData) ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; } + + target: Config.bar.workspaces } Loader { diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 24e7909ec..f866b45cc 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -63,7 +63,6 @@ Item { } Connections { - target: root.wrapper function onCurrentNameChanged() { // Update network immediately when password popout becomes active if (root.wrapper.currentName === "wirelesspassword") { @@ -81,10 +80,11 @@ Item { }, 100); } } + + target: root.wrapper } Connections { - target: networkPopout function onItemChanged() { // When network popout loads, update password popout if it's active if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { @@ -95,6 +95,8 @@ Item { }); } } + + target: networkPopout } } @@ -144,14 +146,14 @@ Item { sourceComponent: trayMenuComp Connections { - target: root.wrapper - function onHasCurrentChanged(): void { if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { trayMenu.sourceComponent = null; trayMenu.sourceComponent = trayMenuComp; } } + + target: root.wrapper } Component { diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 367b9ab87..91991fcbb 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -334,8 +334,6 @@ ColumnLayout { } Connections { - target: Nmcli - function onActiveChanged(): void { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; @@ -354,10 +352,11 @@ ColumnLayout { if (!Nmcli.scanning) scanIcon.rotation = 0; } + + target: Nmcli } Connections { - target: root.wrapper function onCurrentNameChanged(): void { // Clear password network when leaving password dialog if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { @@ -365,6 +364,8 @@ ColumnLayout { root.passwordNetwork = null; } } + + target: root.wrapper } component Toggle: RowLayout { diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index c6b37b63a..0c0f301f4 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -69,8 +69,30 @@ ColumnLayout { } } + spacing: Appearance.spacing.normal + implicitWidth: 400 + implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + visible: shouldBeVisible || isClosing + enabled: shouldBeVisible && !isClosing + focus: enabled + + Component.onCompleted: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + Keys.onEscapePressed: closeDialog() + Connections { - target: root.wrapper function onCurrentNameChanged() { if (root.wrapper.currentName === "wirelesspassword") { // Update network when popout becomes active @@ -89,6 +111,8 @@ ColumnLayout { }); } } + + target: root.wrapper } Timer { @@ -101,41 +125,16 @@ ColumnLayout { } } - spacing: Appearance.spacing.normal - - implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - visible: shouldBeVisible || isClosing - enabled: shouldBeVisible && !isClosing - focus: enabled - - Component.onCompleted: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - onShouldBeVisibleChanged: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - Keys.onEscapePressed: closeDialog() - StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 + Keys.onEscapePressed: root.closeDialog() Behavior on opacity { Anim {} @@ -165,8 +164,6 @@ ColumnLayout { } } - Keys.onEscapePressed: root.closeDialog() - ColumnLayout { id: content @@ -208,10 +205,11 @@ ColumnLayout { } Timer { + property int attempts: 0 + interval: 50 running: root.shouldBeVisible && (!root.network || !root.network.ssid) repeat: true - property int attempts: 0 onTriggered: { attempts++; // Keep trying to get network from Network component @@ -260,16 +258,15 @@ ColumnLayout { FocusScope { id: passwordContainer + property string passwordBuffer: "" + objectName: "passwordContainer" Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) - focus: true activeFocusOnTab: true - property string passwordBuffer: "" - Keys.onPressed: event => { // Ensure we have focus when receiving keyboard input if (!activeFocus) { @@ -300,7 +297,6 @@ ColumnLayout { } Connections { - target: root function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { // Use Timer for actual delay to ensure focus works correctly @@ -309,6 +305,8 @@ ColumnLayout { connectButton.hasError = false; } } + + target: root } Timer { @@ -549,10 +547,11 @@ ColumnLayout { Timer { id: connectionMonitor + property int repeatCount: 0 + interval: 1000 repeat: true triggeredOnStart: false - property int repeatCount: 0 onTriggered: { repeatCount++; @@ -589,12 +588,12 @@ ColumnLayout { } Connections { - target: Nmcli function onActiveChanged() { if (root.shouldBeVisible) { root.checkConnectionStatus(); } } + function onConnectionFailed(ssid: string) { if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); @@ -607,5 +606,7 @@ ColumnLayout { Nmcli.forgetNetwork(ssid); } } + + target: Nmcli } } diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 05a1d3c9e..40479f9a9 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -124,12 +124,12 @@ Item { anchors.centerIn: parent sourceComponent: ControlCenter { - screen: root.screen - active: root.queuedMode - function close(): void { root.close(); } + + screen: root.screen + active: root.queuedMode } } diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index aba6e0cd2..4e903e3e6 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -89,11 +89,12 @@ ColumnLayout { delegate: Item { required property int layoutIndex required property string label + readonly property bool isDisabled: layoutIndex > 3 width: list.width height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) - - readonly property bool isDisabled: layoutIndex > 3 + ToolTip.visible: isDisabled && layer.containsMouse + ToolTip.text: "XKB limitation: maximum 4 layouts allowed" StateLayer { id: layer @@ -107,7 +108,6 @@ ColumnLayout { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height - 4 - radius: Appearance.rounding.full enabled: !isDisabled } @@ -124,9 +124,6 @@ ColumnLayout { elide: Text.ElideRight opacity: isDisabled ? 0.4 : 1.0 } - - ToolTip.visible: isDisabled && layer.containsMouse - ToolTip.text: "XKB limitation: maximum 4 layouts allowed" } } @@ -167,12 +164,13 @@ ColumnLayout { } Connections { - target: kb function onActiveLabelChanged() { if (!activeRow.visible) return; popIn.restart(); } + + target: kb } SequentialAnimation { diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 6478774f0..637d127c7 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -18,6 +18,7 @@ Item { property alias active: session.active property alias navExpanded: session.navExpanded + readonly property bool initialOpeningComplete: panes.initialOpeningComplete readonly property Session session: Session { id: session @@ -61,8 +62,6 @@ Item { color: Colours.tPalette.m3surfaceContainer CustomMouseArea { - anchors.fill: parent - function onWheel(event: WheelEvent): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!panes.initialOpeningComplete) { @@ -74,6 +73,8 @@ Item { else if (event.angleDelta.y > 0) root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); } + + anchors.fill: parent } NavRail { @@ -96,6 +97,4 @@ Item { session: root.session } } - - readonly property bool initialOpeningComplete: panes.initialOpeningComplete } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 1de1e19c3..b4eb4cd81 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -122,6 +122,7 @@ Item { NavItem { required property int index + Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 icon: PaneRegistry.getByIndex(index).icon label: PaneRegistry.getByIndex(index).label diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5aacd89d3..cd7147069 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -37,23 +37,23 @@ ClippingRectangle { } Connections { - target: root.session - function onActiveIndexChanged(): void { root.focus = true; } + + target: root.session } ColumnLayout { id: layout + property bool animationComplete: true + property bool initialOpeningComplete: false + spacing: 0 y: -root.session.activeIndex * root.height clip: true - property bool animationComplete: true - property bool initialOpeningComplete: false - Timer { id: animationDelayTimer @@ -78,6 +78,7 @@ ClippingRectangle { Pane { required property int index + paneIndex: index componentPath: PaneRegistry.getByIndex(index).component } @@ -88,11 +89,12 @@ ClippingRectangle { } Connections { - target: root.session function onActiveIndexChanged(): void { layout.animationComplete = false; animationDelayTimer.restart(); } + + target: root.session } } @@ -158,20 +160,22 @@ ClippingRectangle { } Connections { - target: root.session function onActiveIndexChanged(): void { pane.updateActive(); } + + target: root.session } Connections { - target: layout function onInitialOpeningCompleteChanged(): void { pane.updateActive(); } function onAnimationCompleteChanged(): void { pane.updateActive(); } + + target: layout } } } diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index abcf5df19..068e970be 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -45,13 +45,13 @@ Singleton { ControlCenter { id: cc - anchors.fill: parent - screen: win.screen - floating: true - function close(): void { win.destroy(); } + + anchors.fill: parent + screen: win.screen + floating: true } Behavior on color { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 2b32ec031..2d22425ee 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -152,11 +152,11 @@ Item { anchors.fill: parent leftContent: Component { - StyledFlickable { id: sidebarFlickable readonly property var rootPane: root + flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 08186d03e..7f528e996 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -79,19 +79,22 @@ CollapsibleSection { menuItems: [ MenuItem { + property string val: "top" + text: qsTr("Top") icon: "vertical_align_top" - property string val: "top" }, MenuItem { + property string val: "middle" + text: qsTr("Middle") icon: "vertical_align_center" - property string val: "middle" }, MenuItem { + property string val: "bottom" + text: qsTr("Bottom") icon: "vertical_align_bottom" - property string val: "bottom" } ] @@ -113,19 +116,22 @@ CollapsibleSection { menuItems: [ MenuItem { + property string val: "left" + text: qsTr("Left") icon: "align_horizontal_left" - property string val: "left" }, MenuItem { + property string val: "center" + text: qsTr("Center") icon: "align_horizontal_center" - property string val: "center" }, MenuItem { + property string val: "right" + text: qsTr("Right") icon: "align_horizontal_right" - property string val: "right" } ] diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index d3a2ce68f..8c288608e 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -48,10 +48,9 @@ CollapsibleSection { delegate: StyledRect { required property string modelData required property int index + readonly property bool isCurrent: modelData === rootPane.fontFamilySans width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilySans color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 @@ -131,10 +130,9 @@ CollapsibleSection { delegate: StyledRect { required property string modelData required property int index + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 @@ -216,10 +214,9 @@ CollapsibleSection { delegate: StyledRect { required property string modelData required property int index + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index deefc965c..159c862e2 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -23,7 +23,6 @@ Item { anchors.fill: parent leftContent: Component { - StyledFlickable { id: leftAudioFlickable @@ -280,12 +279,13 @@ Item { } Connections { - target: Audio function onVolumeChanged() { if (!outputVolumeInput.hasFocus) { outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); } } + + target: Audio } onTextEdited: text => { @@ -397,12 +397,13 @@ Item { } Connections { - target: Audio function onSourceVolumeChanged() { if (!inputVolumeInput.hasFocus) { inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); } } + + target: Audio } onTextEdited: text => { @@ -530,12 +531,13 @@ Item { } Connections { - target: modelData function onAudioChanged() { if (!streamVolumeInput.hasFocus && modelData?.audio) { streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); } } + + target: modelData } onTextEdited: text => { @@ -600,12 +602,13 @@ Item { } Connections { - target: modelData function onAudioChanged() { if (modelData?.audio) { value = modelData.audio.volume; } } + + target: modelData } } } diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index a85b4f3e7..0f5098779 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -58,11 +58,10 @@ StyledRect { required property int index required property var modelData - Layout.fillWidth: true - text: modelData.label - property bool _checked: false + Layout.fillWidth: true + text: modelData.label checked: _checked toggle: false type: TextButton.Tonal diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 6b83a7a98..1aed5cbc3 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -21,6 +21,9 @@ ColumnLayout { property int decimals: 1 // Number of decimal places to show (default: 1) property var formatValueFunction: null // Optional custom format function property var parseValueFunction: null // Optional custom parse function + property bool _initialized: false + + signal valueModified(real newValue) function formatValue(val: real): string { if (formatValueFunction) { @@ -49,10 +52,6 @@ ColumnLayout { return parseFloat(text); } - signal valueModified(real newValue) - - property bool _initialized: false - spacing: Appearance.spacing.small Component.onCompleted: { @@ -62,6 +61,14 @@ ColumnLayout { }); } + // Update input field when value changes externally (slider is already bound) + onValueChanged: { + // Only update if component is initialized to avoid issues during creation + if (root._initialized && !inputField.hasFocus) { + inputField.text = root.formatValue(root.value); + } + } + RowLayout { Layout.fillWidth: true spacing: Appearance.spacing.normal @@ -170,12 +177,4 @@ ColumnLayout { } } } - - // Update input field when value changes externally (slider is already bound) - onValueChanged: { - // Only update if component is initialized to avoid issues during creation - if (root._initialized && !inputField.hasFocus) { - inputField.text = root.formatValue(root.value); - } - } } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index bf513e56c..5c1a8be68 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -10,19 +10,17 @@ import QtQuick.Layouts RowLayout { id: root - spacing: 0 - property Component leftContent: null property Component rightContent: null - property real leftWidthRatio: 0.4 property int leftMinimumWidth: 420 property var leftLoaderProperties: ({}) property var rightLoaderProperties: ({}) - property alias leftLoader: leftLoader property alias rightLoader: rightLoader + spacing: 0 + Item { id: leftPane diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 500dd8218..588d51d1f 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -32,14 +32,13 @@ GridView { delegate: Item { required property var modelData required property int index - - width: root.cellWidth - height: root.cellHeight - readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent readonly property real itemMargin: Appearance.spacing.normal / 2 readonly property real itemRadius: Appearance.rounding.normal + width: root.cellWidth + height: root.cellHeight + StateLayer { function onClicked(): void { Wallpapers.setWallpaper(modelData.path); @@ -117,6 +116,7 @@ GridView { id: fallbackTimer property bool triggered: false + interval: 800 running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null onTriggered: triggered = true diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 5ce5139d9..6de344691 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -108,12 +108,21 @@ Item { updateToggleState(); } + onSearchTextChanged: { + updateFilteredApps(); + } + + Component.onCompleted: { + updateFilteredApps(); + } + Connections { - target: root.session.launcher function onActiveChanged() { root.selectedApp = root.session.launcher.active; updateToggleState(); } + + target: root.session.launcher } AppDb { @@ -124,26 +133,18 @@ Item { entries: DesktopEntries.applications.values } - onSearchTextChanged: { - updateFilteredApps(); - } - - Component.onCompleted: { - updateFilteredApps(); - } - Connections { - target: allAppsDb function onAppsChanged() { updateFilteredApps(); } + + target: allAppsDb } SplitPaneLayout { anchors.fill: parent leftContent: Component { - ColumnLayout { id: leftLauncherLayout @@ -358,9 +359,10 @@ Item { } Loader { - Layout.alignment: Qt.AlignVCenter readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false + + Layout.alignment: Qt.AlignVCenter asynchronous: true active: isHidden || isFav @@ -413,6 +415,22 @@ Item { nextComponent = targetComponent; } + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = pane ? (pane.id || pane.entry?.id || "") : ""; + } + + onDisplayedAppChanged: { + if (displayedApp) { + const appId = displayedApp.id || displayedApp.entry?.id; + root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); + root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); + } else { + root.hideFromLauncherChecked = false; + root.favouriteChecked = false; + } + } + Loader { id: rightLauncherLoader @@ -463,22 +481,6 @@ Item { ] } } - - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = pane ? (pane.id || pane.entry?.id || "") : ""; - } - - onDisplayedAppChanged: { - if (displayedApp) { - const appId = displayedApp.id || displayedApp.entry?.id; - root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); - root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); - } else { - root.hideFromLauncherChecked = false; - root.favouriteChecked = false; - } - } } } } diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 78d5fc93f..0a6b20e27 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -199,9 +199,6 @@ Item { } Connections { - target: root.session && root.session.vpn ? root.session.vpn : null - enabled: target !== null - function onActiveChanged() { // Clear others when VPN is selected if (root.session && root.session.vpn && root.session.vpn.active) { @@ -212,12 +209,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - } - Connections { - target: root.session && root.session.ethernet ? root.session.ethernet : null + target: root.session && root.session.vpn ? root.session.vpn : null enabled: target !== null + } + Connections { function onActiveChanged() { // Clear others when ethernet is selected if (root.session && root.session.ethernet && root.session.ethernet.active) { @@ -228,12 +225,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - } - Connections { - target: root.session && root.session.network ? root.session.network : null + target: root.session && root.session.ethernet ? root.session.ethernet : null enabled: target !== null + } + Connections { function onActiveChanged() { // Clear others when wireless is selected if (root.session && root.session.network && root.session.network.active) { @@ -244,6 +241,9 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } + + target: root.session && root.session.network ? root.session.network : null + enabled: target !== null } Loader { diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 9c3366747..e8b49d38c 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -21,7 +21,6 @@ ColumnLayout { spacing: Appearance.spacing.normal Connections { - target: VPN function onConnectedChanged() { if (!VPN.connected && root.pendingSwitchIndex >= 0) { const targetIndex = root.pendingSwitchIndex; @@ -50,6 +49,8 @@ ColumnLayout { }); } } + + target: VPN } TextButton { @@ -367,36 +368,6 @@ ColumnLayout { currentState = "selection"; } - SequentialAnimation { - id: transitionToForm - - ParallelAnimation { - Anim { - target: selectionContent - property: "opacity" - to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - - ScriptAction { - script: { - vpnDialog.currentState = "form"; - } - } - - ParallelAnimation { - Anim { - target: formContent - property: "opacity" - to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - } - background: StyledRect { color: Colours.palette.m3surfaceContainerHigh radius: Appearance.rounding.large @@ -685,5 +656,35 @@ ColumnLayout { } } } + + SequentialAnimation { + id: transitionToForm + + ParallelAnimation { + Anim { + target: selectionContent + property: "opacity" + to: 0 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + ScriptAction { + script: { + vpnDialog.currentState = "form"; + } + } + + ParallelAnimation { + Anim { + target: formContent + property: "opacity" + to: 1 + duration: Appearance.anim.durations.small + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 29b2f6274..beaaff3f0 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -19,67 +19,12 @@ DeviceDetails { required property Session session readonly property var network: root.session.network.active - device: network - - Component.onCompleted: { - updateDeviceDetails(); - checkSavedProfile(); - } - - onNetworkChanged: { - connectionUpdateTimer.stop(); - if (network && network.ssid) { - connectionUpdateTimer.start(); - } - updateDeviceDetails(); - checkSavedProfile(); - } - function checkSavedProfile(): void { if (network && network.ssid) { Nmcli.loadSavedConnections(() => {}); } } - Connections { - target: Nmcli - function onActiveChanged() { - updateDeviceDetails(); - } - function onWirelessDeviceDetailsChanged() { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { - connectionUpdateTimer.stop(); - } - } - } - } - - Timer { - id: connectionUpdateTimer - - interval: 500 - repeat: true - running: network && network.ssid - onTriggered: { - if (network) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive) { - if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { - Nmcli.getWirelessDeviceDetails("", () => {}); - } else { - connectionUpdateTimer.stop(); - } - } else { - if (Nmcli.wirelessDeviceDetails !== null) { - Nmcli.wirelessDeviceDetails = null; - } - } - } - } - } - function updateDeviceDetails(): void { if (network && network.ssid) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); @@ -93,6 +38,22 @@ DeviceDetails { } } + device: network + + Component.onCompleted: { + updateDeviceDetails(); + checkSavedProfile(); + } + + onNetworkChanged: { + connectionUpdateTimer.stop(); + if (network && network.ssid) { + connectionUpdateTimer.start(); + } + updateDeviceDetails(); + checkSavedProfile(); + } + headerComponent: Component { ConnectionHeader { icon: root.network?.isSecure ? "lock" : "wifi" @@ -209,4 +170,44 @@ DeviceDetails { } } ] + + Connections { + function onActiveChanged() { + updateDeviceDetails(); + } + function onWirelessDeviceDetailsChanged() { + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { + connectionUpdateTimer.stop(); + } + } + } + + target: Nmcli + } + + Timer { + id: connectionUpdateTimer + + interval: 500 + repeat: true + running: network && network.ssid + onTriggered: { + if (network) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive) { + if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { + Nmcli.getWirelessDeviceDetails("", () => {}); + } else { + connectionUpdateTimer.stop(); + } + } else { + if (Nmcli.wirelessDeviceDetails !== null) { + Nmcli.wirelessDeviceDetails = null; + } + } + } + } + } } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 57a155fd7..1713022eb 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -19,6 +19,12 @@ DeviceList { required property Session session + function checkSavedProfileForNetwork(ssid: string): void { + if (ssid && ssid.length > 0) { + Nmcli.loadSavedConnections(() => {}); + } + } + title: qsTr("Networks (%1)").arg(Nmcli.networks.length) description: qsTr("All available WiFi networks") activeItem: session.network.active @@ -219,10 +225,4 @@ DeviceList { checkSavedProfileForNetwork(item.ssid); } } - - function checkSavedProfileForNetwork(ssid: string): void { - if (ssid && ssid.length > 0) { - Nmcli.loadSavedConnections(() => {}); - } - } } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 09d6a826f..6db01bc06 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -192,10 +192,11 @@ Item { Item { id: passwordContainer + property string passwordBuffer: "" + Layout.topMargin: Appearance.spacing.large Layout.fillWidth: true implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) - focus: true Keys.onPressed: event => { if (!activeFocus) { @@ -224,10 +225,7 @@ Item { } } - property string passwordBuffer: "" - Connections { - target: root.session.network function onShowPasswordDialogChanged(): void { if (root.session.network.showPasswordDialog) { Qt.callLater(() => { @@ -237,10 +235,11 @@ Item { }); } } + + target: root.session.network } Connections { - target: root function onVisibleChanged(): void { if (root.visible) { Qt.callLater(() => { @@ -248,6 +247,8 @@ Item { }); } } + + target: root } StyledRect { @@ -460,11 +461,11 @@ Item { Timer { id: connectionMonitor + property int repeatCount: 0 + interval: 1000 repeat: true triggeredOnStart: false - property int repeatCount: 0 - onTriggered: { repeatCount++; checkConnectionStatus(); @@ -495,7 +496,6 @@ Item { } Connections { - target: Nmcli function onActiveChanged() { if (root.visible) { checkConnectionStatus(); @@ -512,5 +512,7 @@ Item { Nmcli.forgetNetwork(ssid); } } + + target: Nmcli } } diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index 868eb8d30..0dc923cc3 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -92,18 +92,17 @@ StyledRect { delegate: Item { id: delegateRoot - width: ListView.view.width * 0.98 - height: 70 - anchors.horizontalCenter: parent?.horizontalCenter - required property real id required property string title required property string artist - property bool hovered: false property bool pressed: false + width: ListView.view.width * 0.98 + height: 70 + anchors.horizontalCenter: parent?.horizontalCenter scale: hovered ? 1.02 : 1.0 + Behavior on scale { NumberAnimation { duration: Appearance.anim.durations.small @@ -158,6 +157,7 @@ StyledRect { radius: 2 anchors.verticalCenter: parent.verticalCenter color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" + Behavior on color { ColorAnimation { duration: Appearance.anim.durations.small @@ -177,6 +177,7 @@ StyledRect { color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface width: parent.width elide: Text.ElideRight + Behavior on color { ColorAnimation { duration: Appearance.anim.durations.small @@ -288,6 +289,16 @@ StyledRect { font.pointSize: Appearance.font.size.normal selectByMouse: true text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" + onEditingFinished: { + let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); + let val = parseFloat(cleaned); + if (!isNaN(val)) { + LyricsService.offset = parseFloat(val.toFixed(1)); + LyricsService.savePrefs(); + } else { + offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; + } + } Binding { target: offsetInput @@ -297,21 +308,11 @@ StyledRect { } Connections { - target: LyricsService function onCurrentRequestIdChanged() { offsetInput.focus = false; } - } - onEditingFinished: { - let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); - let val = parseFloat(cleaned); - if (!isNaN(val)) { - LyricsService.offset = parseFloat(val.toFixed(1)); - LyricsService.savePrefs(); - } else { - offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; - } + target: LyricsService } } diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml index 38972402a..29fd96455 100644 --- a/modules/dashboard/LyricsView.qml +++ b/modules/dashboard/LyricsView.qml @@ -14,55 +14,40 @@ StyledListView { clip: true model: LyricsService.model currentIndex: LyricsService.currentIndex - visible: lyricsActuallyVisible || hideTimer.running - - onLyricsActuallyVisibleChanged: { - if (!lyricsActuallyVisible) - hideTimer.restart(); - } - - Timer { - id: hideTimer - - interval: 300 // long enough to bridge the track switch gap - running: false - repeat: false - } - preferredHighlightBegin: height / 2 - 30 preferredHighlightEnd: height / 2 + 30 highlightRangeMode: ListView.ApplyRange highlightFollowsCurrentItem: true highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Appearance.anim.durations.normal - layer.enabled: true layer.effect: ShaderEffect { required property Item source property real fadeMargin: 0.5 + fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") } - + onLyricsActuallyVisibleChanged: { + if (!lyricsActuallyVisible) + hideTimer.restart(); + } onModelChanged: { if (model && model.count > 0) { Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); } } - delegate: Item { id: delegateRoot - width: ListView.view.width - required property string lyricLine required property real time required property int index - readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 - height: hasContent ? (lyricText.contentHeight + Appearance.spacing.large) : 0 - property bool isCurrent: ListView.isCurrentItem + width: ListView.view.width + height: hasContent ? (lyricText.contentHeight + Appearance.spacing.large) : 0 + MultiEffect { id: effect @@ -103,6 +88,7 @@ StyledListView { color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant font.bold: delegateRoot.isCurrent scale: delegateRoot.isCurrent ? 1.15 : 1.0 + Behavior on color { CAnim { duration: Appearance.anim.durations.small @@ -115,4 +101,12 @@ StyledListView { } } } + + Timer { + id: hideTimer + + interval: 300 // long enough to bridge the track switch gap + running: false + repeat: false + } } diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 69e215249..25511e8c3 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -86,10 +86,11 @@ Item { } Connections { - target: lyricsHideDelay function onTriggered() { root.lyricsShowingDebounced = false; } + + target: lyricsHideDelay } ServiceRef { @@ -327,9 +328,6 @@ Item { } CustomMouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - function onWheel(event: WheelEvent) { const active = Players.active; if (!active?.canSeek || !active?.positionSupported) @@ -341,6 +339,9 @@ Item { active.position = Math.max(0, Math.min(active.length, active.position + delta)); }); } + + anchors.fill: parent + acceptedButtons: Qt.NoButton } } @@ -380,6 +381,7 @@ Item { visible: lyricMenu.height === 0 || opacity > 0 opacity: lyricMenu.height === 0 ? 1 : 0 + Behavior on opacity { NumberAnimation { duration: Appearance.anim.durations.normal @@ -421,6 +423,7 @@ Item { visible: root.lyricMenuOpen || height > 0 height: root.lyricMenuOpen ? implicitHeight : 0 clip: true + Behavior on height { NumberAnimation { duration: Appearance.anim.durations.normal diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 339c731fc..2551bc952 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -695,12 +695,12 @@ Item { historyLength: NetworkUsage.historyLength Connections { - target: NetworkUsage.downloadBuffer - function onValuesChanged(): void { sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); slideAnim.restart(); } + + target: NetworkUsage.downloadBuffer } NumberAnimation { diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/Weather.qml index 3981633ac..d15d1ed6f 100644 --- a/modules/dashboard/Weather.qml +++ b/modules/dashboard/Weather.qml @@ -7,11 +7,10 @@ import QtQuick.Layouts Item { id: root - implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 - implicitHeight: layout.implicitHeight - readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null + implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 + implicitHeight: layout.implicitHeight Component.onCompleted: Weather.reload() ColumnLayout { diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 2891bd559..cb764dc0a 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -174,30 +174,30 @@ Item { spacing: Appearance.spacing.small Control { - icon: "skip_previous" - canUse: Players.active?.canGoPrevious ?? false - function onClicked(): void { Players.active?.previous(); } + + icon: "skip_previous" + canUse: Players.active?.canGoPrevious ?? false } Control { - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - canUse: Players.active?.canTogglePlaying ?? false - function onClicked(): void { Players.active?.togglePlaying(); } + + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + canUse: Players.active?.canTogglePlaying ?? false } Control { - icon: "skip_next" - canUse: Players.active?.canGoNext ?? false - function onClicked(): void { Players.active?.next(); } + + icon: "skip_next" + canUse: Players.active?.canGoNext ?? false } } @@ -224,6 +224,7 @@ Item { required property string icon required property bool canUse + function onClicked(): void { } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 10807fcef..d017decbf 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -209,8 +209,6 @@ CustomMouseArea { // Monitor individual visibility changes Connections { - target: root.visibilities - function onLauncherChanged() { // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { @@ -270,5 +268,7 @@ CustomMouseArea { root.utilitiesShortcutActive = false; } } + + target: root.visibilities } } diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index c08597698..55982d316 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -130,8 +130,6 @@ Item { Component.onCompleted: forceActiveFocus() Connections { - target: root.visibilities - function onLauncherChanged(): void { if (!root.visibilities.launcher) search.text = ""; @@ -141,6 +139,8 @@ Item { if (!root.visibilities.session) search.forceActiveFocus(); } + + target: root.visibilities } } diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index d62d726ab..749b41c2a 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -69,8 +69,6 @@ Item { } Connections { - target: Config.launcher - function onEnabledChanged(): void { timer.start(); } @@ -78,15 +76,17 @@ Item { function onMaxShownChanged(): void { timer.start(); } + + target: Config.launcher } Connections { - target: DesktopEntries.applications - function onValuesChanged(): void { if (DesktopEntries.applications.values.length < Config.launcher.maxShown) timer.start(); } + + target: DesktopEntries.applications } Timer { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 5d4dec859..24fb8d4d0 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -357,8 +357,6 @@ ColumnLayout { } Connections { - target: root.lock.pam - function onFlashMsg(): void { exitAnim.stop(); if (message.scale < 1) @@ -366,6 +364,8 @@ ColumnLayout { else flashAnim.restart(); } + + target: root.lock.pam } Anim { diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index 358093f39..885487b90 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -20,8 +20,6 @@ Item { clip: true Connections { - target: root.pam - function onBufferChanged(): void { if (root.pam.buffer.length > root.buffer.length) { charList.bindImWidth(); @@ -32,6 +30,8 @@ Item { root.buffer = root.pam.buffer; } + + target: root.pam } StyledText { diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index 6fd5277fe..a4795016c 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -38,8 +38,6 @@ Scope { } IpcHandler { - target: "lock" - function lock(): void { lock.locked = true; } @@ -51,5 +49,7 @@ Scope { function isLocked(): bool { return lock.locked; } + + target: "lock" } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 279c55138..54fa1b943 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -18,11 +18,11 @@ WlSessionLockSurface { color: "transparent" Connections { - target: root.lock - function onUnlock(): void { unlockAnim.start(); } + + target: root.lock } SequentialAnimation { diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 07ec8c5bd..d06d374aa 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -110,34 +110,34 @@ Item { spacing: Appearance.spacing.large PlayerControl { - icon: "skip_previous" - function onClicked(): void { if (Players.active?.canGoPrevious) Players.active.previous(); } + + icon: "skip_previous" } PlayerControl { + function onClicked(): void { + if (Players.active?.canTogglePlaying) + Players.active.togglePlaying(); + } + animate: true icon: active ? "pause" : "play_arrow" colour: "Primary" level: active ? 2 : 1 active: Players.active?.isPlaying ?? false - - function onClicked(): void { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } } PlayerControl { - icon: "skip_next" - function onClicked(): void { if (Players.active?.canGoNext) Players.active.next(); } + + icon: "skip_next" } } } diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 0186c2f84..31f9fdfa1 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -166,8 +166,6 @@ Scope { } Connections { - target: root.lock - function onSecureChanged(): void { if (root.lock.secure) { availProc.running = true; @@ -181,13 +179,15 @@ Scope { function onUnlock(): void { fprint.abort(); } + + target: root.lock } Connections { - target: Config.lock - function onEnableFprintChanged(): void { fprint.checkAvail(); } + + target: Config.lock } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index a1bf97d8a..e3ed784c7 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -460,6 +460,7 @@ StyledRect { Action { modelData: QtObject { readonly property string text: qsTr("Close") + function invoke(): void { root.modelData.close(); } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 6776bb8f9..53c50d7f1 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -31,9 +31,6 @@ Item { // Speaker volume CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementVolume(); @@ -41,6 +38,9 @@ Item { Audio.decrementVolume(); } + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + FilledSlider { anchors.fill: parent @@ -56,9 +56,6 @@ Item { shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) sourceComponent: CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementSourceVolume(); @@ -66,6 +63,9 @@ Item { Audio.decrementSourceVolume(); } + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + FilledSlider { anchors.fill: parent @@ -82,9 +82,6 @@ Item { shouldBeActive: Config.osd.enableBrightness sourceComponent: CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - function onWheel(event: WheelEvent) { const monitor = root.monitor; if (!monitor) @@ -95,6 +92,9 @@ Item { monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); } + implicitWidth: Config.osd.sizes.sliderWidth + implicitHeight: Config.osd.sizes.sliderHeight + FilledSlider { anchors.fill: parent diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 2519609dd..e674d638e 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -71,8 +71,6 @@ Item { ] Connections { - target: Audio - function onMutedChanged(): void { root.show(); root.muted = Audio.muted; @@ -92,15 +90,17 @@ Item { root.show(); root.sourceVolume = Audio.sourceVolume; } + + target: Audio } Connections { - target: root.monitor - function onBrightnessChanged(): void { root.show(); root.brightness = root.monitor?.brightness ?? 0; } + + target: root.monitor } Timer { diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 06e6c85ff..726d24b3a 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -26,12 +26,12 @@ Column { Component.onCompleted: forceActiveFocus() Connections { - target: root.visibilities - function onLauncherChanged(): void { if (!root.visibilities.launcher) logout.forceActiveFocus(); } + + target: root.visibilities } } diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 7854045ce..fe4c621aa 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -81,14 +81,14 @@ ColumnLayout { readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 readonly property bool isCurrent: root.client?.workspace.id === wsId + function onClicked(): void { + Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); + } + color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer text: wsId disabled: isCurrent - - function onClicked(): void { - Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); - } } } } @@ -107,13 +107,13 @@ ColumnLayout { spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small Button { - color: Colours.palette.m3secondaryContainer - onColor: Colours.palette.m3onSecondaryContainer - text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") - function onClicked(): void { Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); } + + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") } Loader { @@ -124,24 +124,24 @@ ColumnLayout { Layout.rightMargin: active ? 0 : -parent.spacing sourceComponent: Button { - color: Colours.palette.m3secondaryContainer - onColor: Colours.palette.m3onSecondaryContainer - text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") - function onClicked(): void { Hypr.dispatch(`pin address:0x${root.client?.address}`); } + + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") } } Button { - color: Colours.palette.m3errorContainer - onColor: Colours.palette.m3onErrorContainer - text: qsTr("Kill") - function onClicked(): void { Hypr.dispatch(`killwindow address:0x${root.client?.address}`); } + + color: Colours.palette.m3errorContainer + onColor: Colours.palette.m3onErrorContainer + text: qsTr("Kill") } } diff --git a/services/Audio.qml b/services/Audio.qml index 14d0a4e81..d3e73ab6f 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -125,8 +125,6 @@ Singleton { } Connections { - target: Pipewire.nodes - function onValuesChanged(): void { const newSinks = []; const newSources = []; @@ -147,6 +145,8 @@ Singleton { root.sources = newSources; root.streams = newStreams; } + + target: Pipewire.nodes } PwObjectTracker { diff --git a/services/Brightness.qml b/services/Brightness.qml index 567824042..907c0b092 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -105,8 +105,6 @@ Singleton { } IpcHandler { - target: "brightness" - function get(): real { return getFor("active"); } @@ -155,6 +153,8 @@ Singleton { return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; } + + target: "brightness" } component Monitor: QtObject { diff --git a/services/GameMode.qml b/services/GameMode.qml index 83770b79f..6e9d9604b 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -46,17 +46,15 @@ Singleton { } Connections { - target: Hypr - function onConfigReloaded(): void { if (props.enabled) root.setDynamicConfs(); } + + target: Hypr } IpcHandler { - target: "gameMode" - function isEnabled(): bool { return props.enabled; } @@ -72,5 +70,7 @@ Singleton { function disable(): void { props.enabled = false; } + + target: "gameMode" } } diff --git a/services/Hypr.qml b/services/Hypr.qml index c703f7047..52e3d28fe 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -120,8 +120,6 @@ Singleton { } Connections { - target: Hyprland - function onRawEvent(event: HyprlandEvent): void { const n = event.name; if (n.endsWith("v2")) @@ -144,11 +142,11 @@ Singleton { Hyprland.refreshToplevels(); } } + + target: Hyprland } Connections { - target: root.focusedMonitor - function onLastIpcObjectChanged(): void { const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; @@ -156,6 +154,8 @@ Singleton { root.lastSpecialWorkspace = specialName; } } + + target: root.focusedMonitor } FileView { @@ -192,8 +192,6 @@ Singleton { } IpcHandler { - target: "hypr" - function refreshDevices(): void { extras.refreshDevices(); } @@ -205,6 +203,8 @@ Singleton { function listSpecialWorkspaces(): string { return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); } + + target: "hypr" } CustomShortcut { diff --git a/services/IdleInhibitor.qml b/services/IdleInhibitor.qml index 29409abc1..9f556b3a4 100644 --- a/services/IdleInhibitor.qml +++ b/services/IdleInhibitor.qml @@ -35,8 +35,6 @@ Singleton { } IpcHandler { - target: "idleInhibitor" - function isEnabled(): bool { return props.enabled; } @@ -52,5 +50,7 @@ Singleton { function disable(): void { props.enabled = false; } + + target: "idleInhibitor" } } diff --git a/services/LyricsService.qml b/services/LyricsService.qml index c9f782a94..213a69a9e 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -32,6 +32,12 @@ Singleton { property var lyricsMap: ({}) + // shared headers for all NetEase requests + readonly property var _netEaseHeaders: ({ + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", + "Referer": "https://music.163.com/" + }) + ListModel { id: lyricsModel } @@ -99,19 +105,21 @@ Singleton { } Connections { - target: Players function onActiveChanged() { root.player = Players.active; loadLyrics(); } + + target: Players } Connections { - target: root.player - ignoreUnknownSignals: true function onMetadataChanged() { loadLyrics(); } + + target: root.player + ignoreUnknownSignals: true } Process { @@ -226,12 +234,6 @@ Singleton { // NetEase - // shared headers for all NetEase requests - readonly property var _netEaseHeaders: ({ - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", - "Referer": "https://music.163.com/" - }) - // searches NetEase and populates the candidates model. returns the result array via the onResults callback function _searchNetEase(title, artist, reqId, onResults) { Requests.resetCookies(); diff --git a/services/Network.qml b/services/Network.qml index 7fd15dd4f..b32f84a21 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -8,37 +8,21 @@ import qs.services Singleton { id: root - Component.onCompleted: { - // Trigger ethernet device detection after initialization - Qt.callLater(() => { - getEthernetDevices(); - }); - // Load saved connections on startup - Nmcli.loadSavedConnections(() => { - root.savedConnections = Nmcli.savedConnections; - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - }); - // Get initial WiFi status - Nmcli.getWifiStatus(enabled => { - root.wifiEnabled = enabled; - }); - // Sync networks from Nmcli on startup - Qt.callLater(() => { - syncNetworksFromNmcli(); - }, 100); - } - readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true readonly property bool scanning: Nmcli.scanning - property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property int ethernetDeviceCount: 0 property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null + property var pendingConnection: null + property list savedConnections: [] + property list savedConnectionSsids: [] + + signal connectionFailed(string ssid) function enableWifi(enabled: bool): void { Nmcli.enableWifi(enabled, result => { @@ -66,9 +50,6 @@ Singleton { Nmcli.rescanWifi(); } - property var pendingConnection: null - signal connectionFailed(string ssid) - function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { // Set up pending connection tracking if callback provided if (callback) { @@ -159,20 +140,6 @@ Singleton { }); } - property list savedConnections: [] - property list savedConnectionSsids: [] - - // Sync saved connections from Nmcli when they're updated - Connections { - target: Nmcli - function onSavedConnectionsChanged() { - root.savedConnections = Nmcli.savedConnections; - } - function onSavedConnectionSsidsChanged() { - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - } - } - function syncNetworksFromNmcli(): void { const rNetworks = root.networks; const nNetworks = Nmcli.networks; @@ -217,23 +184,6 @@ Singleton { } } - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } - - Component { - id: apComp - - AccessPoint {} - } - function hasSavedProfile(ssid: string): bool { // Use Nmcli's hasSavedProfile which has the same logic return Nmcli.hasSavedProfile(ssid); @@ -310,6 +260,39 @@ Singleton { return octets.join("."); } + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + // Load saved connections on startup + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); + } + + // Sync saved connections from Nmcli when they're updated + Connections { + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + + target: Nmcli + } + Timer { id: monitorDebounce @@ -329,4 +312,21 @@ Singleton { onRead: monitorDebounce.start() } } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } + + Component { + id: apComp + + AccessPoint {} + } } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 9fa753cc6..18fb02c8e 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -24,12 +24,10 @@ Singleton { property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null - signal connectionFailed(string ssid) property var wirelessDeviceDetails: null property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - property list activeProcesses: [] // Constants @@ -55,6 +53,8 @@ Singleton { readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" + signal connectionFailed(string ssid) + function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; diff --git a/services/Notifs.qml b/services/Notifs.qml index 86b906ddc..2ada0f39e 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -114,8 +114,6 @@ Singleton { } IpcHandler { - target: "notifs" - function clear(): void { for (const notif of root.list.slice()) notif.close(); @@ -136,6 +134,8 @@ Singleton { function disableDnd(): void { props.dnd = false; } + + target: "notifs" } component Notif: QtObject { @@ -227,8 +227,6 @@ Singleton { } readonly property Connections conn: Connections { - target: notif.notification - function onClosed(): void { notif.close(); } @@ -282,6 +280,8 @@ Singleton { function onHintsChanged(): void { notif.hints = notif.notification.hints; } + + target: notif.notification } function updateTimeStr(): void { diff --git a/services/Players.qml b/services/Players.qml index 1191696ae..c55cc09a9 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -21,8 +21,6 @@ Singleton { } Connections { - target: active - function onPostTrackChanged() { if (!Config.utilities.toasts.nowPlaying) { return; @@ -31,6 +29,8 @@ Singleton { Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note"); } } + + target: active } PersistentProperties { @@ -78,8 +78,6 @@ Singleton { } IpcHandler { - target: "mpris" - function getActive(prop: string): string { const active = root.active; return active ? active[prop] ?? "Invalid property" : "No active player"; @@ -122,5 +120,7 @@ Singleton { function stop(): void { root.active?.stop(); } + + target: "mpris" } } diff --git a/services/Recorder.qml b/services/Recorder.qml index 6eddce949..4c9f9fd55 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -72,11 +72,11 @@ Singleton { } Connections { - target: Time // enabled: props.running && !props.paused - function onSecondsChanged(): void { props.elapsed++; } + + target: Time } } diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index cb96bc565..c1f3c1840 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -46,8 +46,6 @@ Searcher { }) IpcHandler { - target: "wallpaper" - function get(): string { return root.actualCurrent; } @@ -59,6 +57,8 @@ Searcher { function list(): string { return root.list.map(w => w.path).join("\n"); } + + target: "wallpaper" } FileView { diff --git a/services/Weather.qml b/services/Weather.qml index 98e29bbba..36e69265f 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -210,10 +210,11 @@ Singleton { onLocChanged: fetchWeatherData() Connections { - target: Config.services function onWeatherLocationChanged(): void { root.reload(); } + + target: Config.services } // Refresh current location hourly diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index 19aa4a7a7..aaa1ad31d 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -50,11 +50,11 @@ Singleton { } Connections { - target: Config.general - function onLogoChanged(): void { osRelease.reload(); } + + target: Config.general } Timer { From beddbb111ec8f274f3c97520332e4a1763079d89 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:06:40 +1100 Subject: [PATCH 104/409] chore: run qmlformat --- modules/Shortcuts.qml | 1 - modules/bar/components/Clock.qml | 4 ++-- modules/controlcenter/components/ConnectedButtonGroup.qml | 3 +-- modules/utilities/cards/Toggles.qml | 6 ++---- 4 files changed, 5 insertions(+), 9 deletions(-) diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index a769e160c..adf660ff1 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -69,7 +69,6 @@ Scope { onPressed: root.launcherInterrupted = true } - CustomShortcut { name: "sidebar" description: "Toggle sidebar" diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 13c9035a5..96ad65564 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -35,7 +35,7 @@ StyledRect { color: root.colour } } - + StyledText { anchors.horizontalCenter: parent.horizontalCenter @@ -52,7 +52,7 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter visible: Config.bar.clock.showDate height: visible ? 1 : 0 - + width: parent.width * 0.8 color: root.colour opacity: 0.2 diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 0f5098779..4115003f2 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -70,8 +70,7 @@ StyledRect { Component.onCompleted: { if (modelData.state !== undefined && modelData.state) { _checked = modelData.state; - } - else if (root.rootItem && modelData.propertyName) { + } else if (root.rootItem && modelData.propertyName) { const propName = modelData.propertyName; const rootItem = root.rootItem; _checked = Qt.binding(function () { diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index d610586bf..1fc3be8b6 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -20,15 +20,13 @@ StyledRect { return Config.utilities.quickToggles.filter(item => { if (!item.enabled) return false; - + if (seenIds.has(item.id)) { return false; } if (item.id === "vpn") { - return Config.utilities.vpn.provider.some(p => - typeof p === "object" ? (p.enabled === true) : false - ); + return Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); } seenIds.add(item.id); From 0ea106634e5fe291b51fe679e0e645b10a0383dd Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:55:48 +1100 Subject: [PATCH 105/409] ci: add format checker --- .github/actions/setup-arch/action.yml | 30 ++++++++++++++++++++++++ .github/workflows/check-format.yml | 33 +++++++++++++++++++++++++++ 2 files changed, 63 insertions(+) create mode 100644 .github/actions/setup-arch/action.yml create mode 100644 .github/workflows/check-format.yml diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml new file mode 100644 index 000000000..5ec58f96f --- /dev/null +++ b/.github/actions/setup-arch/action.yml @@ -0,0 +1,30 @@ +name: Set up Arch Linux +description: Install deps needed on Arch + +runs: + using: composite + steps: + - name: Get week + id: date + run: echo "week=$(date +%Y-%U)" >> $GITHUB_OUTPUT + + - name: Cache packages + uses: actions/cache@v4 + with: + path: /var/cache/pacman/pkg + key: pacman-${{ steps.date.outputs.week }}-${{ hashFiles('.github/actions/setup-arch/action.yml') }} + restore-keys: | + pacman-${{ steps.date.outputs.week }}- + pacman- + + - name: Install deps + run: | + pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clang qt6-declarative python + + - name: Install quickshell-git + run: | + useradd -m builder + echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + sudo -u builder git clone https://aur.archlinux.org/quickshell-git.git /home/builder/quickshell-git + cd /home/builder/quickshell-git + sudo -u builder makepkg -si --noconfirm diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml new file mode 100644 index 000000000..e404104ad --- /dev/null +++ b/.github/workflows/check-format.yml @@ -0,0 +1,33 @@ +name: Check formatting + +on: + push: + branches: + - main + pull_request: + +jobs: + check-format: + runs-on: ubuntu-latest + + container: + image: archlinux:latest + + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup-arch + + - name: Check QML format + shell: fish {0} + run: | + for file in (string match -v 'build/*' **.qml) + /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 + end + python3 scripts/qml-lint-conventions.py + + - name: Check C++ format + shell: fish {0} + run: | + for file in (string match -v 'build/*' **.cpp **.hpp) + clang-format $file | diff -u $file - || exit 1 + end From 48a6bb92738e19299acb5c16ffc5f98cceb2e81a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 16:59:15 +1100 Subject: [PATCH 106/409] ci: add shell to setup action run steps --- .github/actions/setup-arch/action.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml index 5ec58f96f..751cf30e0 100644 --- a/.github/actions/setup-arch/action.yml +++ b/.github/actions/setup-arch/action.yml @@ -6,6 +6,7 @@ runs: steps: - name: Get week id: date + shell: bash run: echo "week=$(date +%Y-%U)" >> $GITHUB_OUTPUT - name: Cache packages @@ -18,10 +19,12 @@ runs: pacman- - name: Install deps + shell: bash run: | pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clang qt6-declarative python - name: Install quickshell-git + shell: bash run: | useradd -m builder echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers From 0766993fbca56f56cbf93937de3e4aa537a46037 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:09:55 +1100 Subject: [PATCH 107/409] ci: install cpptrace dep for qs It isn't in the Arch repos... --- .github/actions/setup-arch/action.yml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml index 751cf30e0..528a7921c 100644 --- a/.github/actions/setup-arch/action.yml +++ b/.github/actions/setup-arch/action.yml @@ -28,6 +28,12 @@ runs: run: | useradd -m builder echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers - sudo -u builder git clone https://aur.archlinux.org/quickshell-git.git /home/builder/quickshell-git - cd /home/builder/quickshell-git - sudo -u builder makepkg -si --noconfirm + + install_pkg() { + sudo -u builder git clone https://aur.archlinux.org/$1.git /home/builder/$1 + cd /home/builder/$1 + sudo -u builder makepkg -si --noconfirm + } + + install_pkg cpptrace # Dep for qs + install_pkg quickshell-git From cf81b3d915325911ce310857f94fdf93cf8c07bf Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:24:06 +1100 Subject: [PATCH 108/409] ci: cache packages after install Instead of at end of workflow --- .github/actions/setup-arch/action.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml index 528a7921c..5f4192971 100644 --- a/.github/actions/setup-arch/action.yml +++ b/.github/actions/setup-arch/action.yml @@ -9,8 +9,8 @@ runs: shell: bash run: echo "week=$(date +%Y-%U)" >> $GITHUB_OUTPUT - - name: Cache packages - uses: actions/cache@v4 + - name: Restore package cache + uses: actions/cache/restore@v4 with: path: /var/cache/pacman/pkg key: pacman-${{ steps.date.outputs.week }}-${{ hashFiles('.github/actions/setup-arch/action.yml') }} @@ -37,3 +37,9 @@ runs: install_pkg cpptrace # Dep for qs install_pkg quickshell-git + + - name: Save packages to cache + uses: actions/cache/save@v4 + with: + path: /var/cache/pacman/pkg + key: pacman-${{ steps.date.outputs.week }}-${{ hashFiles('.github/actions/setup-arch/action.yml') }} From 4d3127662f3c71ce8a94e5a070dd1ca1336d865c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:34:52 +1100 Subject: [PATCH 109/409] chore: format c++ --- plugin/src/Caelestia/requests.cpp | 4 ++-- plugin/src/Caelestia/requests.hpp | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp index 4818507ea..dbc746ed1 100644 --- a/plugin/src/Caelestia/requests.cpp +++ b/plugin/src/Caelestia/requests.cpp @@ -1,10 +1,10 @@ #include "requests.hpp" +#include #include +#include #include #include -#include -#include namespace caelestia { diff --git a/plugin/src/Caelestia/requests.hpp b/plugin/src/Caelestia/requests.hpp index 03c8d723f..d07d7e8f0 100644 --- a/plugin/src/Caelestia/requests.hpp +++ b/plugin/src/Caelestia/requests.hpp @@ -14,7 +14,8 @@ class Requests : public QObject { public: explicit Requests(QObject* parent = nullptr); - Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; + Q_INVOKABLE void get( + const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; Q_INVOKABLE void resetCookies() const; private: From 3c82d49849b7dc521157444f58cdc09c77d5b165 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 17:53:09 +1100 Subject: [PATCH 110/409] ci: cache aur packages --- .github/actions/setup-arch/action.yml | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml index 5f4192971..5f0e3dfc3 100644 --- a/.github/actions/setup-arch/action.yml +++ b/.github/actions/setup-arch/action.yml @@ -12,7 +12,9 @@ runs: - name: Restore package cache uses: actions/cache/restore@v4 with: - path: /var/cache/pacman/pkg + path: | + /var/cache/pacman/pkg + /home/builder/.cache/yay key: pacman-${{ steps.date.outputs.week }}-${{ hashFiles('.github/actions/setup-arch/action.yml') }} restore-keys: | pacman-${{ steps.date.outputs.week }}- @@ -23,23 +25,24 @@ runs: run: | pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clang qt6-declarative python - - name: Install quickshell-git + - name: Install yay shell: bash run: | useradd -m builder echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers - install_pkg() { - sudo -u builder git clone https://aur.archlinux.org/$1.git /home/builder/$1 - cd /home/builder/$1 - sudo -u builder makepkg -si --noconfirm - } + sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin + cd /home/builder/yay-bin + sudo -u builder makepkg -si --noconfirm - install_pkg cpptrace # Dep for qs - install_pkg quickshell-git + - name: Install quickshell-git + shell: bash + run: sudo -u builder yay -S --noconfirm quickshell-git - name: Save packages to cache uses: actions/cache/save@v4 with: - path: /var/cache/pacman/pkg + path: | + /var/cache/pacman/pkg + /home/builder/.cache/yay key: pacman-${{ steps.date.outputs.week }}-${{ hashFiles('.github/actions/setup-arch/action.yml') }} From 4e9907c39a611b7183368312d080f7ddff3d291a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 18:06:56 +1100 Subject: [PATCH 111/409] ci: fix cache restore builder home perms --- .github/actions/setup-arch/action.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml index 5f0e3dfc3..be400ef1b 100644 --- a/.github/actions/setup-arch/action.yml +++ b/.github/actions/setup-arch/action.yml @@ -30,6 +30,7 @@ runs: run: | useradd -m builder echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers + chown -R builder:builder /home/builder sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin cd /home/builder/yay-bin From ca28d39ec42ba52f63974a10af4aedd24071f03f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:10:08 +1100 Subject: [PATCH 112/409] ci: update flake use gh app to push --- .github/workflows/update-flake-inputs.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml index 1a8bd071f..c52c5e1bb 100644 --- a/.github/workflows/update-flake-inputs.yml +++ b/.github/workflows/update-flake-inputs.yml @@ -3,17 +3,24 @@ name: Update flake inputs on: workflow_dispatch: schedule: - - cron: '0 0 * * 0' + - cron: "0 0 * * 0" jobs: update-flake: runs-on: ubuntu-latest - permissions: - contents: write - steps: + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + - uses: actions/checkout@v4 + with: + token: ${{ steps.app-token.outputs.token }} + persist-credentials: false - name: Install Nix uses: nixbuild/nix-quick-install-action@v31 @@ -80,6 +87,7 @@ jobs: if: steps.check.outputs.modified == 'true' uses: EndBug/add-and-commit@v9 with: + token: ${{ steps.app-token.outputs.token }} add: flake.lock default_author: github_actions message: "[CI] chore: update flake" From b85ad9d7f4795f064767bf1ec43f88969ca0fae1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:29:18 +1100 Subject: [PATCH 113/409] ci: update action versions --- .github/actions/setup-arch/action.yml | 4 ++-- .github/workflows/check-format.yml | 2 +- .github/workflows/release.yml | 4 ++-- .github/workflows/update-flake-inputs.yml | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml index be400ef1b..456bb4443 100644 --- a/.github/actions/setup-arch/action.yml +++ b/.github/actions/setup-arch/action.yml @@ -10,7 +10,7 @@ runs: run: echo "week=$(date +%Y-%U)" >> $GITHUB_OUTPUT - name: Restore package cache - uses: actions/cache/restore@v4 + uses: actions/cache/restore@v5 with: path: | /var/cache/pacman/pkg @@ -41,7 +41,7 @@ runs: run: sudo -u builder yay -S --noconfirm quickshell-git - name: Save packages to cache - uses: actions/cache/save@v4 + uses: actions/cache/save@v5 with: path: | /var/cache/pacman/pkg diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index e404104ad..f40586db8 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -14,7 +14,7 @@ jobs: image: archlinux:latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ./.github/actions/setup-arch - name: Check QML format diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac4377231..b0dff7dce 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Create release on: push: tags: - - 'v*' + - "v*" jobs: build-and-release: @@ -13,7 +13,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Create packages run: | diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml index c52c5e1bb..8980b7608 100644 --- a/.github/workflows/update-flake-inputs.yml +++ b/.github/workflows/update-flake-inputs.yml @@ -17,7 +17,7 @@ jobs: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} persist-credentials: false From 2d5be8d36bcb1486a6c2159d19256ca875d0ad57 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 19:57:57 +1100 Subject: [PATCH 114/409] ci: fix flake update push token --- .github/workflows/update-flake-inputs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml index 8980b7608..c90822157 100644 --- a/.github/workflows/update-flake-inputs.yml +++ b/.github/workflows/update-flake-inputs.yml @@ -87,7 +87,7 @@ jobs: if: steps.check.outputs.modified == 'true' uses: EndBug/add-and-commit@v9 with: - token: ${{ steps.app-token.outputs.token }} + github_token: ${{ steps.app-token.outputs.token }} add: flake.lock default_author: github_actions message: "[CI] chore: update flake" From 6be5d8aad7a9966a0d43173d7f210d370023421f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:23:02 +1100 Subject: [PATCH 115/409] ci: add arch docker image --- .github/workflows/update-image.yml | 43 ++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 .github/workflows/update-image.yml diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml new file mode 100644 index 000000000..f57103c9f --- /dev/null +++ b/.github/workflows/update-image.yml @@ -0,0 +1,43 @@ +name: Update Docker CI image + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * 0" + +jobs: + update-flake: + runs-on: ubuntu-latest + + steps: + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Write Dockerfile + run: | + cat > /tmp/Dockerfile <> /etc/sudoers && \ + sudo -u git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ + cd /home/builder/yay-bin && \ + sudo -u builder makepkg -si --noconfirm && \ + sudo -u builder yay -S --noconfirm quickshell-git && \ + sudo -u builder yay -Yc --noconfirm && \ + pacman -Rns --noconfirm yay-bin && \ + sed -i '/builder ALL=(ALL) NOPASSWD:ALL/d' /etc/sudoers && \ + userdel -r builder && \ + pacman -Scc --noconfirm + EOF + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: . + file: /tmp/Dockerfile + push: true + tags: ghcr.io/${{ github.repository }}/arch-env:latest From 7637e794badb992a77ff29280d47b0ea304975e6 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:26:30 +1100 Subject: [PATCH 116/409] ci: fix image update Also fix job name --- .github/workflows/update-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index f57103c9f..18337ad17 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -6,7 +6,7 @@ on: - cron: "0 0 * * 0" jobs: - update-flake: + build-image: runs-on: ubuntu-latest steps: @@ -23,7 +23,7 @@ jobs: RUN pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clang qt6-declarative python && \ useradd -m builder && \ echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers && \ - sudo -u git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ + sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ cd /home/builder/yay-bin && \ sudo -u builder makepkg -si --noconfirm && \ sudo -u builder yay -S --noconfirm quickshell-git && \ From 0ed92c419a002d7b88542363b4096d5df32de860 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:39:09 +1100 Subject: [PATCH 117/409] ci: fix update image perms and path --- .github/workflows/update-image.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 18337ad17..3a5647d1a 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -5,6 +5,9 @@ on: schedule: - cron: "0 0 * * 0" +permissions: + packages: write + jobs: build-image: runs-on: ubuntu-latest @@ -40,4 +43,4 @@ jobs: context: . file: /tmp/Dockerfile push: true - tags: ghcr.io/${{ github.repository }}/arch-env:latest + tags: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest From d81d471740e3e6bfb6359d9cdc4875a7e0412975 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 20:51:17 +1100 Subject: [PATCH 118/409] ci: use ghcr image --- .github/actions/setup-arch/action.yml | 49 --------------------------- .github/workflows/check-format.yml | 3 +- 2 files changed, 1 insertion(+), 51 deletions(-) delete mode 100644 .github/actions/setup-arch/action.yml diff --git a/.github/actions/setup-arch/action.yml b/.github/actions/setup-arch/action.yml deleted file mode 100644 index 456bb4443..000000000 --- a/.github/actions/setup-arch/action.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Set up Arch Linux -description: Install deps needed on Arch - -runs: - using: composite - steps: - - name: Get week - id: date - shell: bash - run: echo "week=$(date +%Y-%U)" >> $GITHUB_OUTPUT - - - name: Restore package cache - uses: actions/cache/restore@v5 - with: - path: | - /var/cache/pacman/pkg - /home/builder/.cache/yay - key: pacman-${{ steps.date.outputs.week }}-${{ hashFiles('.github/actions/setup-arch/action.yml') }} - restore-keys: | - pacman-${{ steps.date.outputs.week }}- - pacman- - - - name: Install deps - shell: bash - run: | - pacman -Syu --needed --noconfirm sudo base-devel cmake ninja fish git clang qt6-declarative python - - - name: Install yay - shell: bash - run: | - useradd -m builder - echo 'builder ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers - chown -R builder:builder /home/builder - - sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin - cd /home/builder/yay-bin - sudo -u builder makepkg -si --noconfirm - - - name: Install quickshell-git - shell: bash - run: sudo -u builder yay -S --noconfirm quickshell-git - - - name: Save packages to cache - uses: actions/cache/save@v5 - with: - path: | - /var/cache/pacman/pkg - /home/builder/.cache/yay - key: pacman-${{ steps.date.outputs.week }}-${{ hashFiles('.github/actions/setup-arch/action.yml') }} diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index f40586db8..2f25cb1b3 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -11,11 +11,10 @@ jobs: runs-on: ubuntu-latest container: - image: archlinux:latest + image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest steps: - uses: actions/checkout@v6 - - uses: ./.github/actions/setup-arch - name: Check QML format shell: fish {0} From d2acd1014d093e83913b65059ccdb1c000f5b9d9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:17:36 +1100 Subject: [PATCH 119/409] chore: fix PersistentProperties missing-prop linter errors --- components/DashboardState.qml | 6 ++++++ components/DrawerVisibilities.qml | 11 +++++++++++ modules/Shortcuts.qml | 1 + modules/bar/Bar.qml | 3 ++- modules/bar/BarWrapper.qml | 2 +- modules/bar/components/Power.qml | 2 +- modules/dashboard/Content.qml | 4 ++-- modules/dashboard/Dash.qml | 4 ++-- modules/dashboard/Media.qml | 2 +- modules/dashboard/Tabs.qml | 2 +- modules/dashboard/Wrapper.qml | 8 +++----- modules/dashboard/dash/User.qml | 4 ++-- modules/drawers/Drawers.qml | 10 +--------- modules/drawers/Interactions.qml | 3 ++- modules/drawers/Panels.qml | 3 ++- modules/launcher/AppList.qml | 2 +- modules/launcher/Content.qml | 2 +- modules/launcher/ContentList.qml | 2 +- modules/launcher/Wrapper.qml | 2 +- modules/launcher/items/AppItem.qml | 2 +- modules/launcher/items/WallpaperItem.qml | 2 +- modules/notifications/Content.qml | 3 ++- modules/session/Content.qml | 2 +- modules/session/Wrapper.qml | 2 +- services/Notifs.qml | 1 + services/Visibilities.qml | 6 ++++-- 26 files changed, 53 insertions(+), 38 deletions(-) create mode 100644 components/DashboardState.qml create mode 100644 components/DrawerVisibilities.qml diff --git a/components/DashboardState.qml b/components/DashboardState.qml new file mode 100644 index 000000000..b4355cd7b --- /dev/null +++ b/components/DashboardState.qml @@ -0,0 +1,6 @@ +import Quickshell + +PersistentProperties { + property int currentTab + property date currentDate: new Date() +} diff --git a/components/DrawerVisibilities.qml b/components/DrawerVisibilities.qml new file mode 100644 index 000000000..3286e319f --- /dev/null +++ b/components/DrawerVisibilities.qml @@ -0,0 +1,11 @@ +import Quickshell + +PersistentProperties { + property bool bar + property bool osd + property bool session + property bool launcher + property bool dashboard + property bool utilities + property bool sidebar +} diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index adf660ff1..41b25ee8e 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -1,3 +1,4 @@ +import qs.components import qs.components.misc import qs.modules.controlcenter import qs.services diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 5a7c3ce6a..83815d1fa 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import qs.components import qs.services import qs.config import "popouts" as BarPopouts @@ -13,7 +14,7 @@ ColumnLayout { id: root required property ShellScreen screen - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts readonly property int vPadding: Appearance.padding.large diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 5fddf5cd3..450e6a529 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -10,7 +10,7 @@ Item { id: root required property ShellScreen screen - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts required property bool disabled diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index dc9ef6935..c15379902 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -7,7 +7,7 @@ import QtQuick Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 implicitHeight: icon.implicitHeight diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 978fbc6fe..9012639ca 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -11,7 +11,7 @@ import QtQuick.Layouts Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities readonly property bool needsKeyboard: { const count = repeater.count; for (let i = 0; i < count; i++) { @@ -21,7 +21,7 @@ Item { } return false; } - required property PersistentProperties state + required property DashboardState state required property FileDialog facePicker readonly property var dashboardTabs: { diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 71e224fbe..89067b5cd 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -9,8 +9,8 @@ import QtQuick.Layouts GridLayout { id: root - required property PersistentProperties visibilities - required property PersistentProperties state + required property DrawerVisibilities visibilities + required property DashboardState state required property FileDialog facePicker rowSpacing: Appearance.spacing.normal diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 25511e8c3..8a8b6bdc5 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -15,7 +15,7 @@ import QtQuick.Shapes Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities readonly property bool needsKeyboard: lyricMenuOpen readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index d1c78cbd2..5ac454556 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -13,7 +13,7 @@ Item { id: root required property real nonAnimWidth - required property PersistentProperties state + required property DashboardState state required property var tabs readonly property alias count: bar.count diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 01eddcc3f..522fdc883 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.components.filedialog import qs.config +import qs.services import qs.utils import Caelestia import Quickshell @@ -11,12 +12,9 @@ import QtQuick Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities readonly property bool needsKeyboard: content.item?.needsKeyboard ?? false - readonly property PersistentProperties dashState: PersistentProperties { - property int currentTab - property date currentDate: new Date() - + readonly property DashboardState dashState: DashboardState { reloadableId: "dashboardState" } readonly property FileDialog facePicker: FileDialog { diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 518ec8aa6..09fec9ebc 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -11,8 +11,8 @@ import QtQuick Row { id: root - required property PersistentProperties visibilities - required property PersistentProperties state + required property DrawerVisibilities visibilities + required property DashboardState state required property FileDialog facePicker padding: Appearance.padding.large diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 1423cd21b..ffa4f8d2c 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -132,17 +132,9 @@ Variants { } } - PersistentProperties { + DrawerVisibilities { id: visibilities - property bool bar - property bool osd - property bool session - property bool launcher - property bool dashboard - property bool utilities - property bool sidebar - Component.onCompleted: Visibilities.load(scope.modelData, this) } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index d017decbf..29461e803 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -1,3 +1,4 @@ +import qs.components import qs.components.controls import qs.config import qs.modules.bar.popouts as BarPopouts @@ -9,7 +10,7 @@ CustomMouseArea { required property ShellScreen screen required property BarPopouts.Wrapper popouts - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property Panels panels required property Item bar diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 7705732a7..deb7af163 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -1,3 +1,4 @@ +import qs.components import qs.config import qs.modules.osd as Osd import qs.modules.notifications as Notifications @@ -15,7 +16,7 @@ Item { id: root required property ShellScreen screen - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property Item bar readonly property alias osd: osd diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 7f7b843a9..6a2901bc2 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -14,7 +14,7 @@ StyledListView { id: root required property StyledTextField search - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities model: ScriptModel { id: model diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 55982d316..f5bffcd95 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -11,7 +11,7 @@ import QtQuick Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property var panels required property real maxHeight diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 730fcdb51..aeba6ccc7 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -12,7 +12,7 @@ Item { id: root required property var content - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property var panels required property real maxHeight required property StyledTextField search diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 749b41c2a..a69cc99e0 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -9,7 +9,7 @@ Item { id: root required property ShellScreen screen - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property var panels readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 84ee06a60..62ca0dd5f 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -11,7 +11,7 @@ Item { id: root required property DesktopEntry modelData - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities implicitHeight: Config.launcher.sizes.itemHeight diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 1a2eda5f3..ddd37eab4 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -11,7 +11,7 @@ Item { id: root required property FileSystemEntry modelData - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities scale: 0.5 opacity: 0 diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 2d4590e0e..42fb1fce8 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,3 +1,4 @@ +import qs.components import qs.components.containers import qs.components.widgets import qs.services @@ -9,7 +10,7 @@ import QtQuick Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property Item panels readonly property int padding: Appearance.padding.large diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 726d24b3a..722f08047 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -10,7 +10,7 @@ import QtQuick Column { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities padding: Appearance.padding.large spacing: Appearance.spacing.large diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 14b03a809..23530fbad 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -8,7 +8,7 @@ import QtQuick Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property var panels readonly property real nonAnimWidth: content.implicitWidth diff --git a/services/Notifs.qml b/services/Notifs.qml index 2ada0f39e..c36a4f3f1 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound import qs.components.misc import qs.config +import qs.services import qs.utils import Caelestia import Quickshell diff --git a/services/Visibilities.qml b/services/Visibilities.qml index 5ddde0c95..9c1484d7d 100644 --- a/services/Visibilities.qml +++ b/services/Visibilities.qml @@ -1,16 +1,18 @@ pragma Singleton +import qs.components import Quickshell +import Quickshell.Hyprland Singleton { property var screens: new Map() property var bars: new Map() - function load(screen: ShellScreen, visibilities: var): void { + function load(screen: ShellScreen, visibilities: DrawerVisibilities): void { screens.set(Hypr.monitorFor(screen), visibilities); } - function getForActive(): PersistentProperties { + function getForActive(): DrawerVisibilities { return screens.get(Hypr.focusedMonitor); } } From 8b64d38e26a9e53f4eb2d2c9ba6107e483d899c7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:23:28 +1100 Subject: [PATCH 120/409] chore: remove unused imports --- components/ConnectionHeader.qml | 1 - components/ConnectionInfoSection.qml | 1 - components/SectionContainer.qml | 1 - components/controls/CollapsibleSection.qml | 1 - components/controls/SpinBoxRow.qml | 1 - components/controls/SplitButtonRow.qml | 1 - components/controls/SwitchRow.qml | 1 - components/controls/ToggleButton.qml | 1 - components/controls/ToggleRow.qml | 1 - components/controls/Tooltip.qml | 1 - components/filedialog/FolderContents.qml | 1 - modules/Shortcuts.qml | 1 - modules/background/Background.qml | 1 - modules/bar/components/Power.qml | 1 - modules/bar/components/Settings.qml | 1 - modules/bar/components/SettingsIcon.qml | 1 - modules/bar/popouts/Audio.qml | 2 -- modules/bar/popouts/Bluetooth.qml | 1 - modules/bar/popouts/kblayout/KbLayout.qml | 2 -- modules/bar/popouts/kblayout/KbLayoutModel.qml | 1 - modules/controlcenter/Panes.qml | 8 -------- modules/controlcenter/appearance/AppearancePane.qml | 8 -------- .../appearance/sections/AnimationsSection.qml | 3 --- .../appearance/sections/BackgroundSection.qml | 3 --- .../controlcenter/appearance/sections/BorderSection.qml | 3 --- .../appearance/sections/ColorSchemeSection.qml | 2 -- .../appearance/sections/ColorVariantSection.qml | 2 -- .../controlcenter/appearance/sections/FontsSection.qml | 1 - .../controlcenter/appearance/sections/ScalesSection.qml | 3 --- .../appearance/sections/ThemeModeSection.qml | 4 ---- .../appearance/sections/TransparencySection.qml | 3 --- modules/controlcenter/audio/AudioPane.qml | 2 -- modules/controlcenter/bluetooth/BtPane.qml | 4 ---- modules/controlcenter/bluetooth/DeviceList.qml | 1 - modules/controlcenter/components/ConnectedButtonGroup.qml | 2 -- modules/controlcenter/components/DeviceDetails.qml | 4 ---- modules/controlcenter/components/DeviceList.qml | 2 -- modules/controlcenter/components/ReadonlySlider.qml | 3 --- modules/controlcenter/components/SliderInput.qml | 1 - modules/controlcenter/components/SplitPaneLayout.qml | 1 - modules/controlcenter/components/SplitPaneWithDetails.qml | 7 ------- modules/controlcenter/components/WallpaperGrid.qml | 2 -- modules/controlcenter/dashboard/DashboardPane.qml | 4 ---- modules/controlcenter/dashboard/GeneralSection.qml | 3 +-- modules/controlcenter/dashboard/PerformanceSection.qml | 2 -- modules/controlcenter/launcher/LauncherPane.qml | 2 -- modules/controlcenter/launcher/Settings.qml | 3 +-- modules/controlcenter/network/EthernetDetails.qml | 2 -- modules/controlcenter/network/EthernetList.qml | 1 - modules/controlcenter/network/EthernetPane.qml | 3 --- modules/controlcenter/network/EthernetSettings.qml | 2 -- modules/controlcenter/network/NetworkSettings.qml | 1 - modules/controlcenter/network/NetworkingPane.qml | 4 ---- modules/controlcenter/network/VpnDetails.qml | 2 -- modules/controlcenter/network/VpnSettings.qml | 3 --- modules/controlcenter/network/WirelessDetails.qml | 3 --- modules/controlcenter/network/WirelessList.qml | 3 --- modules/controlcenter/network/WirelessPane.qml | 3 --- modules/controlcenter/network/WirelessPasswordDialog.qml | 3 --- modules/controlcenter/network/WirelessSettings.qml | 1 - modules/controlcenter/taskbar/TaskbarPane.qml | 1 - modules/dashboard/Dash.qml | 1 - modules/dashboard/Wrapper.qml | 1 - modules/dashboard/dash/User.qml | 1 - modules/dashboard/dash/Weather.qml | 1 - modules/drawers/Backgrounds.qml | 1 - modules/launcher/Content.qml | 1 - modules/launcher/ContentList.qml | 1 - modules/launcher/items/ActionItem.qml | 1 - modules/launcher/items/WallpaperItem.qml | 1 - modules/lock/WeatherInfo.qml | 1 - modules/session/Wrapper.qml | 1 - modules/sidebar/NotifGroupList.qml | 2 +- modules/utilities/cards/RecordingList.qml | 1 - modules/utilities/cards/Toggles.qml | 2 -- services/Visibilities.qml | 1 - 76 files changed, 3 insertions(+), 151 deletions(-) diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml index 12b427648..f88dc4ed8 100644 --- a/components/ConnectionHeader.qml +++ b/components/ConnectionHeader.qml @@ -1,5 +1,4 @@ import qs.components -import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml index 927ef287d..cdb2cb029 100644 --- a/components/ConnectionInfoSection.qml +++ b/components/ConnectionInfoSection.qml @@ -1,5 +1,4 @@ import qs.components -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 2b653a5d9..775456c77 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -1,5 +1,4 @@ import qs.components -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 9a4c40276..68f806c10 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -1,6 +1,5 @@ import ".." import qs.components -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index fe6a19822..2109bfa4f 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -1,6 +1,5 @@ import ".." import qs.components -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index a07bf0287..b8e5f9ced 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import ".." import qs.components -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 6dda3f0cc..3c460e8b0 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -1,6 +1,5 @@ import ".." import qs.components -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index b05e7f5a7..814a558cc 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -1,7 +1,6 @@ import ".." import qs.components import qs.components.controls -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml index 269d3d6a5..130af7158 100644 --- a/components/controls/ToggleRow.qml +++ b/components/controls/ToggleRow.qml @@ -1,6 +1,5 @@ import qs.components import qs.components.controls -import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index ab9401f63..4330d5717 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -4,7 +4,6 @@ import qs.services import qs.config import QtQuick import QtQuick.Controls -import QtQuick.Layouts Popup { id: root diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 12d59598e..44f4854e5 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -5,7 +5,6 @@ import "../controls" import "../images" import qs.services import qs.config -import qs.utils import Caelestia.Models import Quickshell import QtQuick diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index 41b25ee8e..adf660ff1 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -1,4 +1,3 @@ -import qs.components import qs.components.misc import qs.modules.controlcenter import qs.services diff --git a/modules/background/Background.qml b/modules/background/Background.qml index e932f3817..5c7cc388a 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -1,6 +1,5 @@ pragma ComponentBehavior: Bound -import qs.components import qs.components.containers import qs.services import qs.config diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index c15379902..d56d37c89 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -1,7 +1,6 @@ import qs.components import qs.services import qs.config -import Quickshell import QtQuick Item { diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index f193c7c14..c235c3ceb 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -2,7 +2,6 @@ import qs.components import qs.modules.controlcenter import qs.services import qs.config -import Quickshell import QtQuick Item { diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index f193c7c14..c235c3ceb 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -2,7 +2,6 @@ import qs.components import qs.modules.controlcenter import qs.services import qs.config -import Quickshell import QtQuick Item { diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 58b29ba8d..fbc62a583 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -4,12 +4,10 @@ import qs.components import qs.components.controls import qs.services import qs.config -import Quickshell import Quickshell.Services.Pipewire import QtQuick import QtQuick.Layouts import QtQuick.Controls -import "../../controlcenter/network" Item { id: root diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 4201ad305..4ac4a6674 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -9,7 +9,6 @@ import Quickshell import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts -import "../../controlcenter/network" ColumnLayout { id: root diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 4e903e3e6..d1b2d8157 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -4,10 +4,8 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import qs.components -import qs.components.controls import qs.services import qs.config -import qs.utils import "." diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 02042195a..213469aa9 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import QtQuick -import Quickshell import Quickshell.Io import qs.config diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index cd7147069..3c0f4df9d 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -1,14 +1,6 @@ pragma ComponentBehavior: Bound -import "bluetooth" -import "network" -import "audio" -import "appearance" -import "taskbar" -import "launcher" -import "dashboard" import qs.components -import qs.services import qs.config import qs.modules.controlcenter import Quickshell.Widgets diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 2d22425ee..cd9474050 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -3,18 +3,10 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "./sections" -import "../../launcher/services" import qs.components import qs.components.controls -import qs.components.effects import qs.components.containers -import qs.components.images -import qs.services import qs.config -import qs.utils -import Caelestia.Models -import Quickshell -import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index 0cba5cecd..8fc8a5d29 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -1,11 +1,8 @@ pragma ComponentBehavior: Bound -import ".." import "../../components" import qs.components import qs.components.controls -import qs.components.containers -import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 7f528e996..a3d09cd78 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -1,11 +1,8 @@ pragma ComponentBehavior: Bound -import ".." import "../../components" import qs.components import qs.components.controls -import qs.components.containers -import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index a259f934e..5df0bc752 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -1,11 +1,8 @@ pragma ComponentBehavior: Bound -import ".." import "../../components" import qs.components import qs.components.controls -import qs.components.containers -import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 4b4559ae6..ef18fc938 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -1,10 +1,8 @@ pragma ComponentBehavior: Bound -import ".." import "../../../launcher/services" import qs.components import qs.components.controls -import qs.components.containers import qs.services import qs.config import Quickshell diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index 3de9e4b3c..62fd13544 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -1,10 +1,8 @@ pragma ComponentBehavior: Bound -import ".." import "../../../launcher/services" import qs.components import qs.components.controls -import qs.components.containers import qs.services import qs.config import Quickshell diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 8c288608e..5bee6bbdc 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -1,6 +1,5 @@ pragma ComponentBehavior: Bound -import ".." import "../../components" import qs.components import qs.components.controls diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index b0e6e38b8..b7c71dd6b 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -1,11 +1,8 @@ pragma ComponentBehavior: Bound -import ".." import "../../components" import qs.components import qs.components.controls -import qs.components.containers -import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index 04eed9113..67084a7f9 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -1,11 +1,7 @@ pragma ComponentBehavior: Bound -import ".." -import qs.components import qs.components.controls -import qs.components.containers import qs.services -import qs.config import QtQuick CollapsibleSection { diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 9a48629c1..89b65c552 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -1,11 +1,8 @@ pragma ComponentBehavior: Bound -import ".." import "../../components" import qs.components import qs.components.controls -import qs.components.containers -import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 159c862e2..32c018c18 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -4,11 +4,9 @@ import ".." import "../components" import qs.components import qs.components.controls -import qs.components.effects import qs.components.containers import qs.services import qs.config -import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 1b9a7fcfa..84913b7ad 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -3,12 +3,8 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." -import qs.components import qs.components.controls import qs.components.containers -import qs.config -import Quickshell.Widgets -import Quickshell.Bluetooth import QtQuick SplitPaneWithDetails { diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index a943f2806..be1b0258e 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -4,7 +4,6 @@ import ".." import "../components" import qs.components import qs.components.controls -import qs.components.containers import qs.services import qs.config import qs.utils diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 4115003f2..d3dfb291d 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -1,7 +1,5 @@ -import ".." import qs.components import qs.components.controls -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index 8e5cdb2ce..072a20a31 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -1,10 +1,6 @@ pragma ComponentBehavior: Bound import ".." -import qs.components -import qs.components.controls -import qs.components.effects -import qs.components.containers import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 7c5292da1..fe09afdef 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -2,11 +2,9 @@ pragma ComponentBehavior: Bound import ".." import qs.components -import qs.components.controls import qs.components.containers import qs.services import qs.config -import Quickshell import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index 169d63653..45774749f 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -1,7 +1,4 @@ -import ".." -import "../components" import qs.components -import qs.components.controls import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 1aed5cbc3..95eb6de1f 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import qs.components import qs.components.controls -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 5c1a8be68..d656c38b6 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -1,6 +1,5 @@ pragma ComponentBehavior: Bound -import qs.components import qs.components.effects import qs.config import Quickshell.Widgets diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index 79b23abc0..4636e28e8 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -1,13 +1,6 @@ pragma ComponentBehavior: Bound -import ".." -import qs.components -import qs.components.effects -import qs.components.containers -import qs.config -import Quickshell.Widgets import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 588d51d1f..a6db661bb 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -3,11 +3,9 @@ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.controls -import qs.components.effects import qs.components.images import qs.services import qs.config -import Caelestia.Models import QtQuick GridView { diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index d7186a790..285ad55f6 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -1,15 +1,11 @@ pragma ComponentBehavior: Bound import ".." -import "../components" import qs.components import qs.components.controls import qs.components.effects import qs.components.containers -import qs.services import qs.config -import qs.utils -import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 95e7531ed..288db2ca1 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -1,9 +1,8 @@ -import ".." import "../components" import qs.components import qs.components.controls -import qs.services import qs.config +import qs.services import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index ac84752b6..faaa05c21 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -1,10 +1,8 @@ -import ".." import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower import qs.components -import qs.components.controls import qs.config import qs.services diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 6de344691..7444b102a 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -2,10 +2,8 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import "../../launcher/services" import qs.components import qs.components.controls -import qs.components.effects import qs.components.containers import qs.services import qs.config diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 5eaf6e0e0..a96584356 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -4,9 +4,8 @@ import ".." import "../components" import qs.components import qs.components.controls -import qs.components.effects -import qs.services import qs.config +import qs.services import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 4e60b3d48..9e702f979 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -4,8 +4,6 @@ import ".." import "../components" import qs.components import qs.components.controls -import qs.components.effects -import qs.components.containers import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 87e00015d..256f02d51 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -4,7 +4,6 @@ import ".." import "../components" import qs.components import qs.components.controls -import qs.components.containers import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 59d82bb08..63cdb16f3 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -2,10 +2,7 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import qs.components import qs.components.containers -import qs.config -import Quickshell.Widgets import QtQuick SplitPaneWithDetails { diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 90bfcf46a..50f8fd335 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -3,8 +3,6 @@ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components -import qs.components.controls -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index bda7cb18a..63e62058c 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -5,7 +5,6 @@ import "../components" import qs.components import qs.components.controls import qs.components.containers -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 0a6b20e27..234c888fe 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -5,13 +5,9 @@ import "../components" import "." import qs.components import qs.components.controls -import qs.components.effects import qs.components.containers import qs.services import qs.config -import qs.utils -import Quickshell -import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 23e4010b4..88c7b08c9 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -5,10 +5,8 @@ import "../components" import qs.components import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config -import qs.utils import QtQuick import QtQuick.Controls import QtQuick.Layouts diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 49d801d9a..2204479be 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -4,13 +4,10 @@ import ".." import "../components" import qs.components import qs.components.controls -import qs.components.containers -import qs.components.effects import qs.services import qs.config import Quickshell import QtQuick -import QtQuick.Controls import QtQuick.Layouts ColumnLayout { diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index beaaff3f0..3ed54abda 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -2,11 +2,8 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import "." import qs.components import qs.components.controls -import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 1713022eb..aa0863c1b 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -2,11 +2,8 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import "." import qs.components import qs.components.controls -import qs.components.containers -import qs.components.effects import qs.services import qs.config import qs.utils diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 8150af9cf..e928d0617 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -2,10 +2,7 @@ pragma ComponentBehavior: Bound import ".." import "../components" -import qs.components import qs.components.containers -import qs.config -import Quickshell.Widgets import QtQuick SplitPaneWithDetails { diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 6db01bc06..a7fae489e 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -1,11 +1,8 @@ pragma ComponentBehavior: Bound import ".." -import "." import qs.components import qs.components.controls -import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index b4eb391d4..1753dc125 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -4,7 +4,6 @@ import ".." import "../components" import qs.components import qs.components.controls -import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 7fcdec9e5..4cae7dff6 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -9,7 +9,6 @@ import qs.components.containers import qs.services import qs.config import qs.utils -import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 89067b5cd..c39decbf4 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -3,7 +3,6 @@ import qs.components.filedialog import qs.services import qs.config import "dash" -import Quickshell import QtQuick.Layouts GridLayout { diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 522fdc883..ce6330a44 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import qs.components import qs.components.filedialog import qs.config -import qs.services import qs.utils import Caelestia import Quickshell diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 09fec9ebc..1ae2ea2d0 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -5,7 +5,6 @@ import qs.components.filedialog import qs.services import qs.config import qs.utils -import Quickshell import QtQuick Row { diff --git a/modules/dashboard/dash/Weather.qml b/modules/dashboard/dash/Weather.qml index c90ccf0a4..766802f74 100644 --- a/modules/dashboard/dash/Weather.qml +++ b/modules/dashboard/dash/Weather.qml @@ -1,7 +1,6 @@ import qs.components import qs.services import qs.config -import qs.utils import QtQuick Item { diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index 7fa2ca176..c99243239 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -1,4 +1,3 @@ -import qs.services import qs.config import qs.modules.osd as Osd import qs.modules.notifications as Notifications diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index f5bffcd95..885b979e8 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -5,7 +5,6 @@ import qs.components import qs.components.controls import qs.services import qs.config -import Quickshell import QtQuick Item { diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index aeba6ccc7..b52940472 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -5,7 +5,6 @@ import qs.components.controls import qs.services import qs.config import qs.utils -import Quickshell import QtQuick Item { diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index c3cd48a0a..ce540ffe9 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -1,4 +1,3 @@ -import "../services" import qs.components import qs.services import qs.config diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index ddd37eab4..476efc452 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -4,7 +4,6 @@ import qs.components.images import qs.services import qs.config import Caelestia.Models -import Quickshell import QtQuick Item { diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index e69a09dfd..edabff57e 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import qs.components import qs.services import qs.config -import qs.utils import QtQuick import QtQuick.Layouts diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 23530fbad..a4a16ce82 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import qs.components import qs.config -import Quickshell import QtQuick Item { diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index e586b5f7a..6f4a8dd12 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound import qs.components -import qs.services import qs.config +import qs.services import Quickshell import QtQuick import QtQuick.Layouts diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 375812b68..355ee0194 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -6,7 +6,6 @@ import qs.components.containers import qs.services import qs.config import qs.utils -import Caelestia import Caelestia.Models import Quickshell import Quickshell.Widgets diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 1fc3be8b6..ac4eae98b 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -2,8 +2,6 @@ import qs.components import qs.components.controls import qs.services import qs.config -import qs.modules.controlcenter -import Quickshell import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts diff --git a/services/Visibilities.qml b/services/Visibilities.qml index 9c1484d7d..f26e43286 100644 --- a/services/Visibilities.qml +++ b/services/Visibilities.qml @@ -2,7 +2,6 @@ pragma Singleton import qs.components import Quickshell -import Quickshell.Hyprland Singleton { property var screens: new Map() From e88bbe425f3e2e25c567225daee3e18435691e6f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:34:08 +1100 Subject: [PATCH 121/409] refactor: move nested Notifs.Notif type to NotifData --- .../dashboard/GeneralSection.qml | 1 - modules/controlcenter/launcher/Settings.qml | 1 - modules/lock/NotifGroup.qml | 2 +- modules/notifications/Content.qml | 2 +- modules/notifications/Notification.qml | 2 +- modules/sidebar/Notif.qml | 2 +- modules/sidebar/NotifActionList.qml | 2 +- modules/sidebar/NotifGroupList.qml | 2 +- services/NotifData.qml | 220 +++++++++++++++++ services/Notifs.qml | 222 +----------------- 10 files changed, 230 insertions(+), 226 deletions(-) create mode 100644 services/NotifData.qml diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 288db2ca1..3db044ee4 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -2,7 +2,6 @@ import "../components" import qs.components import qs.components.controls import qs.config -import qs.services import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index a96584356..a2ed2b121 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -5,7 +5,6 @@ import "../components" import qs.components import qs.components.controls import qs.config -import qs.services import QtQuick import QtQuick.Layouts diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 7f2b62fbb..1c8483449 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -306,7 +306,7 @@ StyledRect { component NotifLine: StyledText { id: notifLine - required property Notifs.Notif modelData + required property NotifData modelData Layout.fillWidth: true textFormat: Text.MarkdownText diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 42fb1fce8..46075a20e 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -68,7 +68,7 @@ Item { delegate: Item { id: wrapper - required property Notifs.Notif modelData + required property NotifData modelData required property int index readonly property alias nonAnimHeight: notif.nonAnimHeight property int idx diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index e3ed784c7..d53cc7cbd 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -15,7 +15,7 @@ import QtQuick.Shapes StyledRect { id: root - required property Notifs.Notif modelData + required property NotifData modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 4ba76ec81..bfe7dd556 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -10,7 +10,7 @@ import QtQuick.Layouts StyledRect { id: root - required property Notifs.Notif modelData + required property NotifData modelData required property Props props required property bool expanded required property var visibilities diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index b95b1da8d..472152aef 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -13,7 +13,7 @@ import QtQuick.Layouts Item { id: root - required property Notifs.Notif notif + required property NotifData notif Layout.fillWidth: true implicitHeight: flickable.contentHeight diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 6f4a8dd12..2d9ba38b5 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -63,7 +63,7 @@ Item { id: notif required property int index - required property Notifs.Notif modelData + required property NotifData modelData readonly property alias nonAnimHeight: notifInner.nonAnimHeight readonly property bool previewHidden: { diff --git a/services/NotifData.qml b/services/NotifData.qml new file mode 100644 index 000000000..f3bb9eb68 --- /dev/null +++ b/services/NotifData.qml @@ -0,0 +1,220 @@ +import qs.config +import qs.utils +import Caelestia +import Quickshell +import Quickshell.Services.Notifications +import QtQuick + +QtObject { + id: notif + + property bool popup + property bool closed + property var locks: new Set() + + property date time: new Date() + property string timeStr: qsTr("now") + + readonly property Timer timeStrTimer: Timer { + running: !notif.closed + repeat: true + interval: 5000 + onTriggered: notif.updateTimeStr() + } + + property Notification notification + property string id + property string summary + property string body + property string appIcon + property string appName + property string image + property var hints // Hints are not persisted across restarts + property real expireTimeout: Config.notifs.defaultExpireTimeout + property int urgency: NotificationUrgency.Normal + property bool resident + property bool hasActionIcons + property list actions + + readonly property Timer timer: Timer { + running: true + interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout + onTriggered: { + if (Config.notifs.expire) + notif.popup = false; + } + } + + readonly property LazyLoader dummyImageLoader: LazyLoader { + active: false + + PanelWindow { + implicitWidth: Config.notifs.sizes.image + implicitHeight: Config.notifs.sizes.image + color: "transparent" + mask: Region {} + + Image { + function tryCache(): void { + if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) + return; + + const cacheKey = notif.appName + notif.summary + notif.id; + let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; + for (let i = 0; i < cacheKey.length; i++) { + ch = cacheKey.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); + + const cache = `${Paths.notifimagecache}/${hash}.png`; + CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { + notif.image = cache; + notif.dummyImageLoader.active = false; + }); + } + + anchors.fill: parent + source: Qt.resolvedUrl(notif.image) + fillMode: Image.PreserveAspectCrop + cache: false + asynchronous: true + opacity: 0 + + onStatusChanged: tryCache() + onWidthChanged: tryCache() + onHeightChanged: tryCache() + } + } + } + + readonly property Connections conn: Connections { + function onClosed(): void { + notif.close(); + } + + function onSummaryChanged(): void { + notif.summary = notif.notification.summary; + } + + function onBodyChanged(): void { + notif.body = notif.notification.body; + } + + function onAppIconChanged(): void { + notif.appIcon = notif.notification.appIcon; + } + + function onAppNameChanged(): void { + notif.appName = notif.notification.appName; + } + + function onImageChanged(): void { + notif.image = notif.notification.image; + if (notif.notification?.image) + notif.dummyImageLoader.active = true; + } + + function onExpireTimeoutChanged(): void { + notif.expireTimeout = notif.notification.expireTimeout; + } + + function onUrgencyChanged(): void { + notif.urgency = notif.notification.urgency; + } + + function onResidentChanged(): void { + notif.resident = notif.notification.resident; + } + + function onHasActionIconsChanged(): void { + notif.hasActionIcons = notif.notification.hasActionIcons; + } + + function onActionsChanged(): void { + notif.actions = notif.notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + + function onHintsChanged(): void { + notif.hints = notif.notification.hints; + } + + target: notif.notification + } + + function updateTimeStr(): void { + const diff = Date.now() - time.getTime(); + const m = Math.floor(diff / 60000); + + if (m < 1) { + timeStr = qsTr("now"); + timeStrTimer.interval = 5000; + } else { + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); + + if (d > 0) { + timeStr = `${d}d`; + timeStrTimer.interval = 3600000; + } else if (h > 0) { + timeStr = `${h}h`; + timeStrTimer.interval = 300000; + } else { + timeStr = `${m}m`; + timeStrTimer.interval = m < 10 ? 30000 : 60000; + } + } + } + + function lock(item: Item): void { + locks.add(item); + } + + function unlock(item: Item): void { + locks.delete(item); + if (closed) + close(); + } + + function close(): void { + closed = true; + if (locks.size === 0 && Notifs.list.includes(this)) { + Notifs.list = Notifs.list.filter(n => n !== this); + notification?.dismiss(); + destroy(); + } + } + + Component.onCompleted: { + if (!notification) + return; + + id = notification.id; + summary = notification.summary; + body = notification.body; + appIcon = notification.appIcon; + appName = notification.appName; + image = notification.image; + if (notification?.image) + dummyImageLoader.active = true; + expireTimeout = notification.expireTimeout; + hints = notification.hints; + urgency = notification.urgency; + resident = notification.resident; + hasActionIcons = notification.hasActionIcons; + actions = notification.actions.map(a => ({ + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } +} diff --git a/services/Notifs.qml b/services/Notifs.qml index c36a4f3f1..ed187ce51 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -14,9 +14,9 @@ import QtQuick Singleton { id: root - property list list: [] - readonly property list notClosed: list.filter(n => !n.closed) - readonly property list popups: list.filter(n => n.popup) + property list list: [] + readonly property list notClosed: list.filter(n => !n.closed) + readonly property list popups: list.filter(n => n.popup) property alias dnd: props.dnd property bool loaded @@ -139,223 +139,9 @@ Singleton { target: "notifs" } - component Notif: QtObject { - id: notif - - property bool popup - property bool closed - property var locks: new Set() - - property date time: new Date() - property string timeStr: qsTr("now") - - readonly property Timer timeStrTimer: Timer { - running: !notif.closed - repeat: true - interval: 5000 - onTriggered: notif.updateTimeStr() - } - - property Notification notification - property string id - property string summary - property string body - property string appIcon - property string appName - property string image - property var hints // Hints are not persisted across restarts - property real expireTimeout: Config.notifs.defaultExpireTimeout - property int urgency: NotificationUrgency.Normal - property bool resident - property bool hasActionIcons - property list actions - - readonly property Timer timer: Timer { - running: true - interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout - onTriggered: { - if (Config.notifs.expire) - notif.popup = false; - } - } - - readonly property LazyLoader dummyImageLoader: LazyLoader { - active: false - - PanelWindow { - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image - color: "transparent" - mask: Region {} - - Image { - function tryCache(): void { - if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) - return; - - const cacheKey = notif.appName + notif.summary + notif.id; - let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; - for (let i = 0; i < cacheKey.length; i++) { - ch = cacheKey.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); - h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); - h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); - const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - - const cache = `${Paths.notifimagecache}/${hash}.png`; - CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { - notif.image = cache; - notif.dummyImageLoader.active = false; - }); - } - - anchors.fill: parent - source: Qt.resolvedUrl(notif.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - opacity: 0 - - onStatusChanged: tryCache() - onWidthChanged: tryCache() - onHeightChanged: tryCache() - } - } - } - - readonly property Connections conn: Connections { - function onClosed(): void { - notif.close(); - } - - function onSummaryChanged(): void { - notif.summary = notif.notification.summary; - } - - function onBodyChanged(): void { - notif.body = notif.notification.body; - } - - function onAppIconChanged(): void { - notif.appIcon = notif.notification.appIcon; - } - - function onAppNameChanged(): void { - notif.appName = notif.notification.appName; - } - - function onImageChanged(): void { - notif.image = notif.notification.image; - if (notif.notification?.image) - notif.dummyImageLoader.active = true; - } - - function onExpireTimeoutChanged(): void { - notif.expireTimeout = notif.notification.expireTimeout; - } - - function onUrgencyChanged(): void { - notif.urgency = notif.notification.urgency; - } - - function onResidentChanged(): void { - notif.resident = notif.notification.resident; - } - - function onHasActionIconsChanged(): void { - notif.hasActionIcons = notif.notification.hasActionIcons; - } - - function onActionsChanged(): void { - notif.actions = notif.notification.actions.map(a => ({ - identifier: a.identifier, - text: a.text, - invoke: () => a.invoke() - })); - } - - function onHintsChanged(): void { - notif.hints = notif.notification.hints; - } - - target: notif.notification - } - - function updateTimeStr(): void { - const diff = Date.now() - time.getTime(); - const m = Math.floor(diff / 60000); - - if (m < 1) { - timeStr = qsTr("now"); - timeStrTimer.interval = 5000; - } else { - const h = Math.floor(m / 60); - const d = Math.floor(h / 24); - - if (d > 0) { - timeStr = `${d}d`; - timeStrTimer.interval = 3600000; - } else if (h > 0) { - timeStr = `${h}h`; - timeStrTimer.interval = 300000; - } else { - timeStr = `${m}m`; - timeStrTimer.interval = m < 10 ? 30000 : 60000; - } - } - } - - function lock(item: Item): void { - locks.add(item); - } - - function unlock(item: Item): void { - locks.delete(item); - if (closed) - close(); - } - - function close(): void { - closed = true; - if (locks.size === 0 && root.list.includes(this)) { - root.list = root.list.filter(n => n !== this); - notification?.dismiss(); - destroy(); - } - } - - Component.onCompleted: { - if (!notification) - return; - - id = notification.id; - summary = notification.summary; - body = notification.body; - appIcon = notification.appIcon; - appName = notification.appName; - image = notification.image; - if (notification?.image) - dummyImageLoader.active = true; - expireTimeout = notification.expireTimeout; - hints = notification.hints; - urgency = notification.urgency; - resident = notification.resident; - hasActionIcons = notification.hasActionIcons; - actions = notification.actions.map(a => ({ - identifier: a.identifier, - text: a.text, - invoke: () => a.invoke() - })); - } - } - Component { id: notifComp - Notif {} + NotifData {} } } From a3b3c88f0243793ce816d3b89f57d1c4ca6aa89d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 22:38:09 +1100 Subject: [PATCH 122/409] fix: template string unused import false positives --- components/effects/OpacityMask.qml | 2 +- modules/sidebar/NotifDock.qml | 2 +- services/NotifData.qml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/components/effects/OpacityMask.qml b/components/effects/OpacityMask.qml index 22e424960..f852f4478 100644 --- a/components/effects/OpacityMask.qml +++ b/components/effects/OpacityMask.qml @@ -5,5 +5,5 @@ ShaderEffect { required property Item source required property Item maskSource - fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) + fragmentShader: Quickshell.shellPath("assets/shaders/opacitymask.frag.qsb") } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 9b865d4c3..c38bddcea 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -96,7 +96,7 @@ Item { Image { asynchronous: true - source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) + source: Quickshell.shellPath("assets/dino.png") fillMode: Image.PreserveAspectFit sourceSize.width: clipRect.width * 0.8 diff --git a/services/NotifData.qml b/services/NotifData.qml index f3bb9eb68..031c87e82 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -72,7 +72,7 @@ QtObject { h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - const cache = `${Paths.notifimagecache}/${hash}.png`; + const cache = Paths.notifimagecache + "/${hash}.png"; CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { notif.image = cache; notif.dummyImageLoader.active = false; From c3fd5aa9059f9795893a6739e6ff36d1e7be66f3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:42:36 +1100 Subject: [PATCH 123/409] chore: fix unqualified access --- components/controls/StyledScrollBar.qml | 16 ++-- components/controls/ToggleButton.qml | 6 +- components/filedialog/FolderContents.qml | 3 +- .../bar/components/workspaces/Workspace.qml | 5 +- modules/bar/popouts/kblayout/KbLayout.qml | 12 ++- .../bar/popouts/kblayout/KbLayoutModel.qml | 20 ++-- .../appearance/sections/AnimationsSection.qml | 6 +- .../appearance/sections/BackgroundSection.qml | 94 +++++++++---------- .../appearance/sections/BorderSection.qml | 12 +-- .../sections/ColorSchemeSection.qml | 18 ++-- .../sections/ColorVariantSection.qml | 14 +-- .../appearance/sections/FontsSection.qml | 42 +++++---- .../appearance/sections/ScalesSection.qml | 18 ++-- .../sections/TransparencySection.qml | 18 ++-- modules/controlcenter/audio/AudioPane.qml | 68 ++++++++------ .../components/ConnectedButtonGroup.qml | 16 ++-- .../components/WallpaperGrid.qml | 46 ++++----- .../controlcenter/launcher/LauncherPane.qml | 28 +++--- .../controlcenter/network/EthernetList.qml | 32 +++---- .../controlcenter/network/WirelessDetails.qml | 12 +-- .../controlcenter/network/WirelessList.qml | 42 +++++---- .../network/WirelessPasswordDialog.qml | 14 ++- modules/launcher/items/AppItem.qml | 2 +- modules/utilities/cards/Toggles.qml | 2 + modules/windowinfo/Buttons.qml | 2 + services/NotifData.qml | 2 + services/Players.qml | 6 +- 27 files changed, 297 insertions(+), 259 deletions(-) diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index 7ec537d08..bd7a7af08 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -81,20 +81,20 @@ ScrollBar { // Sync nonAnimPosition with flickable when not animating Connections { function onContentYChanged() { - if (!animating && !fullMouse.pressed) { - _updatingFromFlickable = true; - const contentHeight = flickable.contentHeight; - const height = flickable.height; + if (!root.animating && !fullMouse.pressed) { + root._updatingFromFlickable = true; + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; if (contentHeight > height) { - nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); + root.nonAnimPosition = Math.max(0, Math.min(1, root.flickable.contentY / (contentHeight - height))); } else { - nonAnimPosition = 0; + root.nonAnimPosition = 0; } - _updatingFromFlickable = false; + root._updatingFromFlickable = false; } } - target: flickable + target: root.flickable } Connections { diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 814a558cc..c054db8b3 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import ".." import qs.components import qs.components.controls @@ -33,8 +35,8 @@ StyledRect { Connections { function onContainsMouseChanged() { const newHovered = toggleStateLayer.containsMouse; - if (hovered !== newHovered) { - hovered = newHovered; + if (root.hovered !== newHovered) { + root.hovered = newHovered; } } diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 44f4854e5..58fe3fe1c 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -5,6 +5,7 @@ import "../controls" import "../images" import qs.services import qs.config +import qs.utils import Caelestia.Models import Quickshell import QtQuick @@ -104,7 +105,7 @@ Item { model: FileSystemModel { path: { if (root.dialog.cwd[0] === "Home") - return `${Paths.home}/${root.dialog.cwd.slice(1).join("/")}`; + return Paths.home + `/${root.dialog.cwd.slice(1).join("/")}`; else return root.dialog.cwd.join("/"); } diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index 32efc1253..7c35f96eb 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import qs.components import qs.services import qs.utils @@ -90,7 +92,8 @@ ColumnLayout { Repeater { model: ScriptModel { values: { - const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws); + const ws = root.ws; + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); const maxIcons = Config.bar.workspaces.maxWindowIcons; return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; } diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index d1b2d8157..74ee67e12 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -85,6 +85,8 @@ ColumnLayout { } delegate: Item { + id: kbDelegate + required property int layoutIndex required property string label readonly property bool isDisabled: layoutIndex > 3 @@ -98,8 +100,8 @@ ColumnLayout { id: layer function onClicked(): void { - if (!isDisabled) - kb.switchTo(layoutIndex); + if (!kbDelegate.isDisabled) + kb.switchTo(kbDelegate.layoutIndex); } anchors.left: parent.left @@ -107,7 +109,7 @@ ColumnLayout { anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height - 4 radius: Appearance.rounding.full - enabled: !isDisabled + enabled: !kbDelegate.isDisabled } StyledText { @@ -118,9 +120,9 @@ ColumnLayout { anchors.right: layer.right anchors.leftMargin: Appearance.padding.small anchors.rightMargin: Appearance.padding.small - text: label + text: kbDelegate.label elide: Text.ElideRight - opacity: isDisabled ? 0.4 : 1.0 + opacity: kbDelegate.isDisabled ? 0.4 : 1.0 } } } diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 213469aa9..36032e290 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -137,9 +137,9 @@ Item { command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] stdout: StdioCollector { - onStreamFinished: _buildXmlMap(text) + onStreamFinished: model._buildXmlMap(text) } - onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) + onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) _xkbXmlEvdev.running = true } @@ -148,7 +148,7 @@ Item { command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] stdout: StdioCollector { - onStreamFinished: _buildXmlMap(text) + onStreamFinished: model._buildXmlMap(text) } } @@ -162,7 +162,7 @@ Item { const j = JSON.parse(text); const raw = (j?.str || j?.value || "").toString().trim(); if (raw.length) { - _setLayouts(raw); + model._setLayouts(raw); _fetchActiveLayouts.running = true; return; } @@ -183,7 +183,7 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const raw = (kb?.layout || "").trim(); if (raw.length) - _setLayouts(raw); + model._setLayouts(raw); } catch (e) {} _fetchActiveLayouts.running = true; } @@ -201,14 +201,14 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const idx = kb?.active_layout_index ?? -1; - activeIndex = idx >= 0 ? idx : -1; - activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; + model.activeIndex = idx >= 0 ? idx : -1; + model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; } catch (e) { - activeIndex = -1; - activeLabel = ""; + model.activeIndex = -1; + model.activeLabel = ""; } - _rebuildVisible(); + model._rebuildVisible(); } } } diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index 8fc8a5d29..f6f9b7a47 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -22,7 +22,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Animation duration scale") - value: rootPane.animDurationsScale + value: root.rootPane.animDurationsScale from: 0.1 to: 5.0 decimals: 1 @@ -33,8 +33,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.animDurationsScale = newValue; - rootPane.saveConfig(); + root.rootPane.animDurationsScale = newValue; + root.rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index a3d09cd78..0488daac7 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -17,19 +17,19 @@ CollapsibleSection { SwitchRow { label: qsTr("Background enabled") - checked: rootPane.backgroundEnabled + checked: root.rootPane.backgroundEnabled onToggled: checked => { - rootPane.backgroundEnabled = checked; - rootPane.saveConfig(); + root.rootPane.backgroundEnabled = checked; + root.rootPane.saveConfig(); } } SwitchRow { label: qsTr("Wallpaper enabled") - checked: rootPane.wallpaperEnabled + checked: root.rootPane.wallpaperEnabled onToggled: checked => { - rootPane.wallpaperEnabled = checked; - rootPane.saveConfig(); + root.rootPane.wallpaperEnabled = checked; + root.rootPane.saveConfig(); } } @@ -42,23 +42,23 @@ CollapsibleSection { SwitchRow { label: qsTr("Desktop Clock enabled") - checked: rootPane.desktopClockEnabled + checked: root.rootPane.desktopClockEnabled onToggled: checked => { - rootPane.desktopClockEnabled = checked; - rootPane.saveConfig(); + root.rootPane.desktopClockEnabled = checked; + root.rootPane.saveConfig(); } } SectionContainer { id: posContainer - readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') + readonly property var pos: (root.rootPane.desktopClockPosition || "top-left").split('-') readonly property string currentV: pos[0] readonly property string currentH: pos[1] function updateClockPos(v, h) { - rootPane.desktopClockPosition = v + "-" + h; - rootPane.saveConfig(); + root.rootPane.desktopClockPosition = v + "-" + h; + root.rootPane.saveConfig(); } contentSpacing: Appearance.spacing.small @@ -72,7 +72,7 @@ CollapsibleSection { SplitButtonRow { label: qsTr("Vertical Position") - enabled: rootPane.desktopClockEnabled + enabled: root.rootPane.desktopClockEnabled menuItems: [ MenuItem { @@ -108,7 +108,7 @@ CollapsibleSection { SplitButtonRow { label: qsTr("Horizontal Position") - enabled: rootPane.desktopClockEnabled + enabled: root.rootPane.desktopClockEnabled expandedZ: 99 menuItems: [ @@ -145,10 +145,10 @@ CollapsibleSection { SwitchRow { label: qsTr("Invert colors") - checked: rootPane.desktopClockInvertColors + checked: root.rootPane.desktopClockInvertColors onToggled: checked => { - rootPane.desktopClockInvertColors = checked; - rootPane.saveConfig(); + root.rootPane.desktopClockInvertColors = checked; + root.rootPane.saveConfig(); } } @@ -163,10 +163,10 @@ CollapsibleSection { SwitchRow { label: qsTr("Enabled") - checked: rootPane.desktopClockShadowEnabled + checked: root.rootPane.desktopClockShadowEnabled onToggled: checked => { - rootPane.desktopClockShadowEnabled = checked; - rootPane.saveConfig(); + root.rootPane.desktopClockShadowEnabled = checked; + root.rootPane.saveConfig(); } } @@ -177,7 +177,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Opacity") - value: rootPane.desktopClockShadowOpacity * 100 + value: root.rootPane.desktopClockShadowOpacity * 100 from: 0 to: 100 suffix: "%" @@ -189,8 +189,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - rootPane.desktopClockShadowOpacity = newValue / 100; - rootPane.saveConfig(); + root.rootPane.desktopClockShadowOpacity = newValue / 100; + root.rootPane.saveConfig(); } } } @@ -202,7 +202,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Blur") - value: rootPane.desktopClockShadowBlur * 100 + value: root.rootPane.desktopClockShadowBlur * 100 from: 0 to: 100 suffix: "%" @@ -214,8 +214,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - rootPane.desktopClockShadowBlur = newValue / 100; - rootPane.saveConfig(); + root.rootPane.desktopClockShadowBlur = newValue / 100; + root.rootPane.saveConfig(); } } } @@ -232,19 +232,19 @@ CollapsibleSection { SwitchRow { label: qsTr("Enabled") - checked: rootPane.desktopClockBackgroundEnabled + checked: root.rootPane.desktopClockBackgroundEnabled onToggled: checked => { - rootPane.desktopClockBackgroundEnabled = checked; - rootPane.saveConfig(); + root.rootPane.desktopClockBackgroundEnabled = checked; + root.rootPane.saveConfig(); } } SwitchRow { label: qsTr("Blur enabled") - checked: rootPane.desktopClockBackgroundBlur + checked: root.rootPane.desktopClockBackgroundBlur onToggled: checked => { - rootPane.desktopClockBackgroundBlur = checked; - rootPane.saveConfig(); + root.rootPane.desktopClockBackgroundBlur = checked; + root.rootPane.saveConfig(); } } @@ -255,7 +255,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Opacity") - value: rootPane.desktopClockBackgroundOpacity * 100 + value: root.rootPane.desktopClockBackgroundOpacity * 100 from: 0 to: 100 suffix: "%" @@ -267,8 +267,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - rootPane.desktopClockBackgroundOpacity = newValue / 100; - rootPane.saveConfig(); + root.rootPane.desktopClockBackgroundOpacity = newValue / 100; + root.rootPane.saveConfig(); } } } @@ -283,19 +283,19 @@ CollapsibleSection { SwitchRow { label: qsTr("Visualiser enabled") - checked: rootPane.visualiserEnabled + checked: root.rootPane.visualiserEnabled onToggled: checked => { - rootPane.visualiserEnabled = checked; - rootPane.saveConfig(); + root.rootPane.visualiserEnabled = checked; + root.rootPane.saveConfig(); } } SwitchRow { label: qsTr("Visualiser auto hide") - checked: rootPane.visualiserAutoHide + checked: root.rootPane.visualiserAutoHide onToggled: checked => { - rootPane.visualiserAutoHide = checked; - rootPane.saveConfig(); + root.rootPane.visualiserAutoHide = checked; + root.rootPane.saveConfig(); } } @@ -306,7 +306,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Visualiser rounding") - value: rootPane.visualiserRounding + value: root.rootPane.visualiserRounding from: 0 to: 10 stepSize: 1 @@ -318,8 +318,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - rootPane.visualiserRounding = Math.round(newValue); - rootPane.saveConfig(); + root.rootPane.visualiserRounding = Math.round(newValue); + root.rootPane.saveConfig(); } } } @@ -331,7 +331,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Visualiser spacing") - value: rootPane.visualiserSpacing + value: root.rootPane.visualiserSpacing from: 0 to: 2 validator: DoubleValidator { @@ -340,8 +340,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.visualiserSpacing = newValue; - rootPane.saveConfig(); + root.rootPane.visualiserSpacing = newValue; + root.rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index 5df0bc752..167c3f144 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -22,7 +22,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Border rounding") - value: rootPane.borderRounding + value: root.rootPane.borderRounding from: 0.1 to: 100 decimals: 1 @@ -33,8 +33,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.borderRounding = newValue; - rootPane.saveConfig(); + root.rootPane.borderRounding = newValue; + root.rootPane.saveConfig(); } } } @@ -46,7 +46,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Border thickness") - value: rootPane.borderThickness + value: root.rootPane.borderThickness from: 0 to: 100 decimals: 1 @@ -57,8 +57,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.borderThickness = newValue; - rootPane.saveConfig(); + root.rootPane.borderThickness = newValue; + root.rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index ef18fc938..98cbe9061 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -22,6 +22,8 @@ CollapsibleSection { model: Schemes.list delegate: StyledRect { + id: schemeDelegate + required property var modelData Layout.fillWidth: true @@ -36,8 +38,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - const name = modelData.name; - const flavour = modelData.flavour; + const name = schemeDelegate.modelData.name; + const flavour = schemeDelegate.modelData.flavour; const schemeKey = `${name} ${flavour}`; Schemes.currentScheme = schemeKey; @@ -72,9 +74,9 @@ CollapsibleSection { Layout.alignment: Qt.AlignVCenter border.width: 1 - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + border.color: Qt.alpha(`#${schemeDelegate.modelData.colours?.outline}`, 0.5) - color: `#${modelData.colours?.surface}` + color: `#${schemeDelegate.modelData.colours?.surface}` radius: Appearance.rounding.full implicitWidth: iconPlaceholder.implicitWidth implicitHeight: iconPlaceholder.implicitWidth @@ -101,7 +103,7 @@ CollapsibleSection { anchors.right: parent.right implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` + color: `#${schemeDelegate.modelData.colours?.primary}` radius: Appearance.rounding.full } } @@ -112,12 +114,12 @@ CollapsibleSection { spacing: 0 StyledText { - text: modelData.flavour ?? "" + text: schemeDelegate.modelData.flavour ?? "" font.pointSize: Appearance.font.size.normal } StyledText { - text: modelData.name ?? "" + text: schemeDelegate.modelData.name ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline @@ -129,7 +131,7 @@ CollapsibleSection { Loader { asynchronous: true - active: isCurrent + active: schemeDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index 62fd13544..afbf9b29f 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -22,6 +22,8 @@ CollapsibleSection { model: M3Variants.list delegate: StyledRect { + id: variantDelegate + required property var modelData Layout.fillWidth: true @@ -33,7 +35,7 @@ CollapsibleSection { StateLayer { function onClicked(): void { - const variant = modelData.variant; + const variant = variantDelegate.modelData.variant; Schemes.currentVariant = variant; Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); @@ -64,19 +66,19 @@ CollapsibleSection { spacing: Appearance.spacing.normal MaterialIcon { - text: modelData.icon + text: variantDelegate.modelData.icon font.pointSize: Appearance.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + fill: variantDelegate.modelData.variant === Schemes.currentVariant ? 1 : 0 } StyledText { Layout.fillWidth: true - text: modelData.name - font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 + text: variantDelegate.modelData.name + font.weight: variantDelegate.modelData.variant === Schemes.currentVariant ? 500 : 400 } MaterialIcon { - visible: modelData.variant === Schemes.currentVariant + visible: variantDelegate.modelData.variant === Schemes.currentVariant text: "check" color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 5bee6bbdc..fb44f1a68 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -45,9 +45,11 @@ CollapsibleSection { } delegate: StyledRect { + id: sansDelegate + required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilySans + readonly property bool isCurrent: modelData === root.rootPane.fontFamilySans width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -57,8 +59,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); + root.rootPane.fontFamilySans = sansDelegate.modelData; + root.rootPane.saveConfig(); } } @@ -73,7 +75,7 @@ CollapsibleSection { spacing: Appearance.spacing.normal StyledText { - text: modelData + text: sansDelegate.modelData font.pointSize: Appearance.font.size.normal } @@ -83,7 +85,7 @@ CollapsibleSection { Loader { asynchronous: true - active: isCurrent + active: sansDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" @@ -127,9 +129,11 @@ CollapsibleSection { } delegate: StyledRect { + id: monoDelegate + required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMono width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -139,8 +143,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); + root.rootPane.fontFamilyMono = monoDelegate.modelData; + root.rootPane.saveConfig(); } } @@ -155,7 +159,7 @@ CollapsibleSection { spacing: Appearance.spacing.normal StyledText { - text: modelData + text: monoDelegate.modelData font.pointSize: Appearance.font.size.normal } @@ -165,7 +169,7 @@ CollapsibleSection { Loader { asynchronous: true - active: isCurrent + active: monoDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" @@ -211,9 +215,11 @@ CollapsibleSection { } delegate: StyledRect { + id: materialDelegate + required property string modelData required property int index - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMaterial width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -223,8 +229,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); + root.rootPane.fontFamilyMaterial = materialDelegate.modelData; + root.rootPane.saveConfig(); } } @@ -239,7 +245,7 @@ CollapsibleSection { spacing: Appearance.spacing.normal StyledText { - text: modelData + text: materialDelegate.modelData font.pointSize: Appearance.font.size.normal } @@ -249,7 +255,7 @@ CollapsibleSection { Loader { asynchronous: true - active: isCurrent + active: materialDelegate.isCurrent sourceComponent: MaterialIcon { text: "check" @@ -272,7 +278,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Font size scale") - value: rootPane.fontSizeScale + value: root.rootPane.fontSizeScale from: 0.7 to: 1.5 decimals: 2 @@ -283,8 +289,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.fontSizeScale = newValue; - rootPane.saveConfig(); + root.rootPane.fontSizeScale = newValue; + root.rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index b7c71dd6b..b6f6a409b 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -22,7 +22,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Padding scale") - value: rootPane.paddingScale + value: root.rootPane.paddingScale from: 0.5 to: 2.0 decimals: 1 @@ -33,8 +33,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.paddingScale = newValue; - rootPane.saveConfig(); + root.rootPane.paddingScale = newValue; + root.rootPane.saveConfig(); } } } @@ -46,7 +46,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Rounding scale") - value: rootPane.roundingScale + value: root.rootPane.roundingScale from: 0.1 to: 5.0 decimals: 1 @@ -57,8 +57,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.roundingScale = newValue; - rootPane.saveConfig(); + root.rootPane.roundingScale = newValue; + root.rootPane.saveConfig(); } } } @@ -70,7 +70,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Spacing scale") - value: rootPane.spacingScale + value: root.rootPane.spacingScale from: 0.1 to: 2.0 decimals: 1 @@ -81,8 +81,8 @@ CollapsibleSection { } onValueModified: newValue => { - rootPane.spacingScale = newValue; - rootPane.saveConfig(); + root.rootPane.spacingScale = newValue; + root.rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 89b65c552..7a29ec4bd 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -17,10 +17,10 @@ CollapsibleSection { SwitchRow { label: qsTr("Transparency enabled") - checked: rootPane.transparencyEnabled + checked: root.rootPane.transparencyEnabled onToggled: checked => { - rootPane.transparencyEnabled = checked; - rootPane.saveConfig(); + root.rootPane.transparencyEnabled = checked; + root.rootPane.saveConfig(); } } @@ -31,7 +31,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Transparency base") - value: rootPane.transparencyBase * 100 + value: root.rootPane.transparencyBase * 100 from: 0 to: 100 suffix: "%" @@ -43,8 +43,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - rootPane.transparencyBase = newValue / 100; - rootPane.saveConfig(); + root.rootPane.transparencyBase = newValue / 100; + root.rootPane.saveConfig(); } } } @@ -56,7 +56,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Transparency layers") - value: rootPane.transparencyLayers * 100 + value: root.rootPane.transparencyLayers * 100 from: 0 to: 100 suffix: "%" @@ -68,8 +68,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - rootPane.transparencyLayers = newValue / 100; - rootPane.saveConfig(); + root.rootPane.transparencyLayers = newValue / 100; + root.rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 32c018c18..28f82fc99 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -86,16 +86,18 @@ Item { model: Audio.sinks delegate: StyledRect { + id: outputDeviceDelegate + required property var modelData Layout.fillWidth: true - color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + color: Audio.sink?.id === outputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { function onClicked(): void { - Audio.setAudioSink(modelData); + Audio.setAudioSink(outputDeviceDelegate.modelData); } } @@ -110,9 +112,9 @@ Item { spacing: Appearance.spacing.normal MaterialIcon { - text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" + text: Audio.sink?.id === outputDeviceDelegate.modelData.id ? "speaker" : "speaker_group" font.pointSize: Appearance.font.size.large - fill: Audio.sink?.id === modelData.id ? 1 : 0 + fill: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 1 : 0 } StyledText { @@ -120,8 +122,8 @@ Item { elide: Text.ElideRight maximumLineCount: 1 - text: modelData.description || qsTr("Unknown") - font.weight: Audio.sink?.id === modelData.id ? 500 : 400 + text: outputDeviceDelegate.modelData.description || qsTr("Unknown") + font.weight: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 500 : 400 } } @@ -164,16 +166,18 @@ Item { model: Audio.sources delegate: StyledRect { + id: inputDeviceDelegate + required property var modelData Layout.fillWidth: true - color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + color: Audio.source?.id === inputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { function onClicked(): void { - Audio.setAudioSource(modelData); + Audio.setAudioSource(inputDeviceDelegate.modelData); } } @@ -190,7 +194,7 @@ Item { MaterialIcon { text: "mic" font.pointSize: Appearance.font.size.large - fill: Audio.source?.id === modelData.id ? 1 : 0 + fill: Audio.source?.id === inputDeviceDelegate.modelData.id ? 1 : 0 } StyledText { @@ -198,8 +202,8 @@ Item { elide: Text.ElideRight maximumLineCount: 1 - text: modelData.description || qsTr("Unknown") - font.weight: Audio.source?.id === modelData.id ? 500 : 400 + text: inputDeviceDelegate.modelData.description || qsTr("Unknown") + font.weight: Audio.source?.id === inputDeviceDelegate.modelData.id ? 500 : 400 } } @@ -489,6 +493,8 @@ Item { Layout.fillWidth: true delegate: ColumnLayout { + id: streamDelegate + required property var modelData required property int index @@ -509,7 +515,7 @@ Item { Layout.fillWidth: true elide: Text.ElideRight maximumLineCount: 1 - text: Audio.getStreamName(modelData) + text: Audio.getStreamName(streamDelegate.modelData) font.pointSize: Appearance.font.size.normal font.weight: 500 } @@ -522,27 +528,27 @@ Item { bottom: 0 top: 100 } - enabled: !Audio.getStreamMuted(modelData) + enabled: !Audio.getStreamMuted(streamDelegate.modelData) Component.onCompleted: { - text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); + text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString(); } Connections { function onAudioChanged() { - if (!streamVolumeInput.hasFocus && modelData?.audio) { - streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); + if (!streamVolumeInput.hasFocus && streamDelegate.modelData?.audio) { + streamVolumeInput.text = Math.round(streamDelegate.modelData.audio.volume * 100).toString(); } } - target: modelData + target: streamDelegate.modelData } onTextEdited: text => { if (hasFocus) { const val = parseInt(text); if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setStreamVolume(modelData, val / 100); + Audio.setStreamVolume(streamDelegate.modelData, val / 100); } } } @@ -550,7 +556,7 @@ Item { onEditingFinished: { const val = parseInt(text); if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); + text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString(); } } } @@ -559,7 +565,7 @@ Item { text: "%" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal - opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 + opacity: Audio.getStreamMuted(streamDelegate.modelData) ? 0.5 : 1 } StyledRect { @@ -567,11 +573,11 @@ Item { implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { function onClicked(): void { - Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); + Audio.setStreamMuted(streamDelegate.modelData, !Audio.getStreamMuted(streamDelegate.modelData)); } } @@ -579,21 +585,23 @@ Item { id: streamMuteIcon anchors.centerIn: parent - text: Audio.getStreamMuted(modelData) ? "volume_off" : "volume_up" - color: Audio.getStreamMuted(modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + text: Audio.getStreamMuted(streamDelegate.modelData) ? "volume_off" : "volume_up" + color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } StyledSlider { + id: streamSlider + Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - value: Audio.getStreamVolume(modelData) - enabled: !Audio.getStreamMuted(modelData) + value: Audio.getStreamVolume(streamDelegate.modelData) + enabled: !Audio.getStreamMuted(streamDelegate.modelData) opacity: enabled ? 1 : 0.5 onMoved: { - Audio.setStreamVolume(modelData, value); + Audio.setStreamVolume(streamDelegate.modelData, value); if (!streamVolumeInput.hasFocus) { streamVolumeInput.text = Math.round(value * 100).toString(); } @@ -601,12 +609,12 @@ Item { Connections { function onAudioChanged() { - if (modelData?.audio) { - value = modelData.audio.volume; + if (streamDelegate.modelData?.audio) { + streamSlider.value = streamDelegate.modelData.audio.volume; } } - target: modelData + target: streamDelegate.modelData } } } diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index d3dfb291d..c4398ebc2 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import qs.components import qs.components.controls import qs.services @@ -66,10 +68,10 @@ StyledRect { // Create binding in Component.onCompleted Component.onCompleted: { - if (modelData.state !== undefined && modelData.state) { - _checked = modelData.state; - } else if (root.rootItem && modelData.propertyName) { - const propName = modelData.propertyName; + if (button.modelData.state !== undefined && button.modelData.state) { + _checked = button.modelData.state; + } else if (root.rootItem && button.modelData.propertyName) { + const propName = button.modelData.propertyName; const rootItem = root.rootItem; _checked = Qt.binding(function () { return rootItem[propName] ?? false; @@ -88,9 +90,9 @@ StyledRect { Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) onClicked: { - if (modelData.onToggled && root.rootItem && modelData.propertyName) { - const currentValue = root.rootItem[modelData.propertyName] ?? false; - modelData.onToggled(!currentValue); + if (button.modelData.onToggled && root.rootItem && button.modelData.propertyName) { + const currentValue = root.rootItem[button.modelData.propertyName] ?? false; + button.modelData.onToggled(!currentValue); } } diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index a6db661bb..851140045 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -28,6 +28,8 @@ GridView { } delegate: Item { + id: wpDelegate + required property var modelData required property int index readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent @@ -39,27 +41,27 @@ GridView { StateLayer { function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); + Wallpapers.setWallpaper(wpDelegate.modelData.path); } anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - radius: itemRadius + anchors.leftMargin: wpDelegate.itemMargin + anchors.rightMargin: wpDelegate.itemMargin + anchors.topMargin: wpDelegate.itemMargin + anchors.bottomMargin: wpDelegate.itemMargin + radius: wpDelegate.itemRadius } StyledClippingRect { id: image anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin + anchors.leftMargin: wpDelegate.itemMargin + anchors.rightMargin: wpDelegate.itemMargin + anchors.topMargin: wpDelegate.itemMargin + anchors.bottomMargin: wpDelegate.itemMargin color: Colours.tPalette.m3surfaceContainer - radius: itemRadius + radius: wpDelegate.itemRadius antialiasing: true layer.enabled: true layer.smooth: true @@ -67,7 +69,7 @@ GridView { CachingImage { id: cachingImage - path: modelData.path + path: wpDelegate.modelData.path anchors.fill: parent fillMode: Image.PreserveAspectCrop cache: true @@ -91,7 +93,7 @@ GridView { id: fallbackImage anchors.fill: parent - source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" + source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? wpDelegate.modelData.path : "" asynchronous: true fillMode: Image.PreserveAspectCrop cache: true @@ -167,13 +169,13 @@ GridView { Rectangle { anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin + anchors.leftMargin: wpDelegate.itemMargin + anchors.rightMargin: wpDelegate.itemMargin + anchors.topMargin: wpDelegate.itemMargin + anchors.bottomMargin: wpDelegate.itemMargin color: "transparent" - radius: itemRadius + border.width - border.width: isCurrent ? 2 : 0 + radius: wpDelegate.itemRadius + border.width + border.width: wpDelegate.isCurrent ? 2 : 0 border.color: Colours.palette.m3primary antialiasing: true smooth: true @@ -190,7 +192,7 @@ GridView { anchors.top: parent.top anchors.margins: Appearance.padding.small - visible: isCurrent + visible: wpDelegate.isCurrent text: "check_circle" color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large @@ -207,10 +209,10 @@ GridView { anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 anchors.bottomMargin: Appearance.padding.normal - text: modelData.name + text: wpDelegate.modelData.name font.pointSize: Appearance.font.size.smaller font.weight: 500 - color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface + color: wpDelegate.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface elide: Text.ElideMiddle maximumLineCount: 1 horizontalAlignment: Text.AlignHCenter diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 7444b102a..7f82457ca 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -102,7 +102,7 @@ Item { anchors.fill: parent onSelectedAppChanged: { - root.session.launcher.active = root.selectedApp; + session.launcher.active = selectedApp; updateToggleState(); } @@ -117,7 +117,7 @@ Item { Connections { function onActiveChanged() { root.selectedApp = root.session.launcher.active; - updateToggleState(); + root.updateToggleState(); } target: root.session.launcher @@ -133,7 +133,7 @@ Item { Connections { function onAppsChanged() { - updateFilteredApps(); + root.updateFilteredApps(); } target: allAppsDb @@ -299,10 +299,12 @@ Item { clip: true StyledScrollBar.vertical: StyledScrollBar { - flickable: parent + flickable: appsListView } delegate: StyledRect { + id: appDelegate + required property var modelData readonly property bool isSelected: root.selectedApp === modelData @@ -328,7 +330,7 @@ Item { StateLayer { function onClicked(): void { - root.session.launcher.active = modelData; + root.session.launcher.active = appDelegate.modelData; } } @@ -345,20 +347,20 @@ Item { Layout.alignment: Qt.AlignVCenter implicitSize: 32 source: { - const entry = modelData.entry; + const entry = appDelegate.modelData.entry; return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; } } StyledText { Layout.fillWidth: true - text: modelData.name || modelData.entry?.name || qsTr("Unknown") + text: appDelegate.modelData.name || appDelegate.modelData.entry?.name || qsTr("Unknown") font.pointSize: Appearance.font.size.normal } Loader { - readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false - readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false + readonly property bool isHidden: appDelegate.modelData ? Strings.testRegexList(Config.launcher.hiddenApps, appDelegate.modelData.id) : false + readonly property bool isFav: appDelegate.modelData ? Strings.testRegexList(Config.launcher.favouriteApps, appDelegate.modelData.id) : false Layout.alignment: Qt.AlignVCenter asynchronous: true @@ -513,7 +515,7 @@ Item { ColumnLayout { id: appDetailsLayout - readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null + readonly property var displayedApp: parent?.displayedApp ?? null // qmllint disable missing-property anchors.fill: parent spacing: Appearance.spacing.normal @@ -522,7 +524,7 @@ Item { Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.topMargin: Appearance.padding.large * 2 - visible: displayedApp === null + visible: appDetailsLayout.displayedApp === null icon: "apps" title: qsTr("Launcher Applications") } @@ -532,7 +534,7 @@ Item { Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.topMargin: Appearance.padding.large * 2 - visible: displayedApp !== null + visible: appDetailsLayout.displayedApp !== null implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight @@ -562,7 +564,7 @@ Item { id: appTitleText Layout.alignment: Qt.AlignHCenter - text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" + text: appDetailsLayout.displayedApp.displayedApp ? (appDetailsLayout.displayedApp.displayedApp.displayedApp.name || appDetailsLayout.displayedApp.displayedApp.displayedApp.entry?.name || qsTr("Application Details")) : "" font.pointSize: Appearance.font.size.large font.bold: true } diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 256f02d51..e9f241682 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -70,7 +70,7 @@ DeviceList { id: stateLayer function onClicked(): void { - root.session.ethernet.active = modelData; + root.session.ethernet.active = ethernetItem.modelData; } } @@ -87,12 +87,12 @@ DeviceList { implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + color: ethernetItem.modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { anchors.fill: parent radius: parent.radius - color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) + color: Qt.alpha(ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) } MaterialIcon { @@ -101,8 +101,8 @@ DeviceList { anchors.centerIn: parent text: "cable" font.pointSize: Appearance.font.size.large - fill: modelData.connected ? 1 : 0 - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + fill: ethernetItem.modelData.connected ? 1 : 0 + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface Behavior on fill { Anim {} @@ -117,7 +117,7 @@ DeviceList { StyledText { Layout.fillWidth: true - text: modelData.interface || qsTr("Unknown") + text: ethernetItem.modelData.interface || qsTr("Unknown") elide: Text.ElideRight } @@ -127,10 +127,10 @@ DeviceList { StyledText { Layout.fillWidth: true - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + text: ethernetItem.modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline font.pointSize: Appearance.font.size.small - font.weight: modelData.connected ? 500 : 400 + font.weight: ethernetItem.modelData.connected ? 500 : 400 elide: Text.ElideRight } } @@ -143,18 +143,18 @@ DeviceList { implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + color: Qt.alpha(Colours.palette.m3primaryContainer, ethernetItem.modelData.connected ? 1 : 0) StateLayer { function onClicked(): void { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); + if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { + Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); } } - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } MaterialIcon { @@ -162,8 +162,8 @@ DeviceList { anchors.centerIn: parent animate: true - text: modelData.connected ? "link_off" : "link" - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + text: ethernetItem.modelData.connected ? "link_off" : "link" + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 3ed54abda..67d7f065c 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -170,11 +170,11 @@ DeviceDetails { Connections { function onActiveChanged() { - updateDeviceDetails(); + root.updateDeviceDetails(); } function onWirelessDeviceDetailsChanged() { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (root.network && root.network.ssid) { + const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid); if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { connectionUpdateTimer.stop(); } @@ -189,10 +189,10 @@ DeviceDetails { interval: 500 repeat: true - running: network && network.ssid + running: root.network && root.network.ssid onTriggered: { - if (network) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (root.network) { + const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid); if (isActive) { if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { Nmcli.getWirelessDeviceDetails("", () => {}); diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index aa0863c1b..15fa293fa 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -104,18 +104,20 @@ DeviceList { delegate: Component { StyledRect { + id: networkDelegate + required property var modelData width: ListView.view ? ListView.view.width : undefined - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === networkDelegate.modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { function onClicked(): void { - root.session.network.active = modelData; - if (modelData && modelData.ssid) { - root.checkSavedProfileForNetwork(modelData.ssid); + root.session.network.active = networkDelegate.modelData; + if (networkDelegate.modelData && networkDelegate.modelData.ssid) { + root.checkSavedProfileForNetwork(networkDelegate.modelData.ssid); } } } @@ -135,16 +137,16 @@ DeviceList { implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + color: networkDelegate.modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { id: icon anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) + text: Icons.getNetworkIcon(networkDelegate.modelData.strength, networkDelegate.modelData.isSecure) font.pointSize: Appearance.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + fill: networkDelegate.modelData.active ? 1 : 0 + color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } @@ -158,7 +160,7 @@ DeviceList { elide: Text.ElideRight maximumLineCount: 1 - text: modelData.ssid || qsTr("Unknown") + text: networkDelegate.modelData.ssid || qsTr("Unknown") } RowLayout { @@ -168,18 +170,18 @@ DeviceList { StyledText { Layout.fillWidth: true text: { - if (modelData.active) + if (networkDelegate.modelData.active) return qsTr("Connected"); - if (modelData.isSecure && modelData.security && modelData.security.length > 0) { - return modelData.security; + if (networkDelegate.modelData.isSecure && networkDelegate.modelData.security && networkDelegate.modelData.security.length > 0) { + return networkDelegate.modelData.security; } - if (modelData.isSecure) + if (networkDelegate.modelData.isSecure) return qsTr("Secured"); return qsTr("Open"); } - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + color: networkDelegate.modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline font.pointSize: Appearance.font.size.small - font.weight: modelData.active ? 500 : 400 + font.weight: networkDelegate.modelData.active ? 500 : 400 elide: Text.ElideRight } } @@ -190,14 +192,14 @@ DeviceList { implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + color: Qt.alpha(Colours.palette.m3primaryContainer, networkDelegate.modelData.active ? 1 : 0) StateLayer { function onClicked(): void { - if (modelData.active) { + if (networkDelegate.modelData.active) { Nmcli.disconnectFromNetwork(); } else { - NetworkConnection.handleConnect(modelData, root.session, null); + NetworkConnection.handleConnect(networkDelegate.modelData, root.session, null); } } } @@ -206,8 +208,8 @@ DeviceList { id: connectIcon anchors.centerIn: parent - text: modelData.active ? "link_off" : "link" - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + text: networkDelegate.modelData.active ? "link_off" : "link" + color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index a7fae489e..9049ca0d5 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -71,9 +71,7 @@ Item { enabled: session.network.showPasswordDialog && !isClosing focus: enabled - Keys.onEscapePressed: { - closeDialog(); - } + Keys.onEscapePressed: closeDialog() Rectangle { anchors.fill: parent @@ -86,7 +84,7 @@ Item { MouseArea { anchors.fill: parent - onClicked: closeDialog() + onClicked: root.closeDialog() } } @@ -132,7 +130,7 @@ Item { } } - Keys.onEscapePressed: closeDialog() + Keys.onEscapePressed: root.closeDialog() ColumnLayout { id: content @@ -465,7 +463,7 @@ Item { triggeredOnStart: false onTriggered: { repeatCount++; - checkConnectionStatus(); + root.checkConnectionStatus(); } onRunningChanged: { @@ -486,7 +484,7 @@ Item { connectionMonitor.stop(); connectButton.connecting = false; connectButton.text = qsTr("Connect"); - closeDialog(); + root.closeDialog(); } } } @@ -495,7 +493,7 @@ Item { Connections { function onActiveChanged() { if (root.visible) { - checkConnectionStatus(); + root.checkConnectionStatus(); } } function onConnectionFailed(ssid: string) { diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 62ca0dd5f..eb738a84d 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -78,7 +78,7 @@ Item { asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - active: modelData && Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) + active: root.modelData && Strings.testRegexList(Config.launcher.favouriteApps, root.modelData.id) sourceComponent: MaterialIcon { text: "favorite" diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index ac4eae98b..2ca93db6d 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import qs.components import qs.components.controls import qs.services diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index fe4c621aa..101405f79 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import qs.components import qs.services import qs.config diff --git a/services/NotifData.qml b/services/NotifData.qml index 031c87e82..d1648fa75 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import qs.config import qs.utils import Caelestia diff --git a/services/Players.qml b/services/Players.qml index c55cc09a9..65954c690 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -25,12 +25,12 @@ Singleton { if (!Config.utilities.toasts.nowPlaying) { return; } - if (active.trackArtist != "" && active.trackTitle != "") { - Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note"); + if (root.active.trackArtist != "" && root.active.trackTitle != "") { + Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note"); } } - target: active + target: root.active } PersistentProperties { From f5440eb190546e4af3c847bca39a6bef8096d5ff Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:46:38 +1100 Subject: [PATCH 124/409] fix: remove newline-after-id rule in custom linter --- scripts/qml-lint-conventions.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py index d49eddeee..be8d4817d 100755 --- a/scripts/qml-lint-conventions.py +++ b/scripts/qml-lint-conventions.py @@ -49,7 +49,6 @@ class Section(IntEnum): } RULE_COLOURS = { - "missing-blank-after-id": RED, "section-order": YELLOW, "missing-section-separator": CYAN, "blank-after-open-brace": MAGENTA, @@ -212,17 +211,7 @@ def check_file(filepath: Path) -> list[Violation]: tracker = scopes[indent] had_blank = prev_blank.get(indent, True) - # --- Check 1: Missing blank line after id --- - if section == Section.ID: - if i + 1 < len(lines): - next_stripped = lines[i + 1].strip() - if next_stripped and next_stripped != "}": - violations.append(Violation( - rel, lineno, "missing-blank-after-id", - "id should be followed by a blank line", - )) - - # --- Check 2: Section ordering --- + # --- Check 1: Section ordering --- if tracker.last_section is not None and section < tracker.last_section: violations.append(Violation( rel, lineno, "section-order", @@ -231,7 +220,7 @@ def check_file(filepath: Path) -> list[Violation]: f"(seen at line {tracker.last_section_line})", )) - # --- Check 3: Missing blank line between different sections --- + # --- Check 2: Missing blank line between different sections --- if (tracker.last_section is not None and section != tracker.last_section and not had_blank): From 5912d549919c828aafc78720508d0df5586970f1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 20 Mar 2026 23:46:52 +1100 Subject: [PATCH 125/409] chore: format QML lint script --- scripts/qml-lint-conventions.py | 71 ++++++++++++++++++--------------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py index be8d4817d..ff0b58162 100755 --- a/scripts/qml-lint-conventions.py +++ b/scripts/qml-lint-conventions.py @@ -56,9 +56,7 @@ class Section(IntEnum): } # Regexes -PROPERTY_DECL_RE = re.compile( - r"^(?:required\s+|readonly\s+|default\s+)*property\s" -) +PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s") SIGNAL_RE = re.compile(r"^signal\s") FUNCTION_RE = re.compile(r"^function\s") ID_RE = re.compile(r"^id\s*:\s*[a-zA-Z_]\w*\s*$") @@ -162,12 +160,15 @@ def check_file(filepath: Path) -> list[Violation]: # Track blank lines per indent if not stripped: # Check: blank line right after opening brace of a QML object - if (i > 0 and func_skip_depth == 0 and not in_block_comment - and lines[i - 1].strip().endswith("{")): - violations.append(Violation( - rel, lineno, "blank-after-open-brace", - "no blank line expected after opening brace", - )) + if i > 0 and func_skip_depth == 0 and not in_block_comment and lines[i - 1].strip().endswith("{"): + violations.append( + Violation( + rel, + lineno, + "blank-after-open-brace", + "no blank line expected after opening brace", + ) + ) for key in prev_blank: prev_blank[key] = True continue @@ -187,10 +188,14 @@ def check_file(filepath: Path) -> list[Violation]: if stripped == "}": # Check: blank line right before closing brace if i > 0 and not lines[i - 1].strip(): - violations.append(Violation( - rel, lineno, "blank-before-close-brace", - "no blank line expected before closing brace", - )) + violations.append( + Violation( + rel, + lineno, + "blank-before-close-brace", + "no blank line expected before closing brace", + ) + ) scopes.pop(indent, None) prev_blank.pop(indent, None) to_remove = [k for k in scopes if len(k) > len(indent)] @@ -213,22 +218,27 @@ def check_file(filepath: Path) -> list[Violation]: # --- Check 1: Section ordering --- if tracker.last_section is not None and section < tracker.last_section: - violations.append(Violation( - rel, lineno, "section-order", - f"{SECTION_NAMES[section]} should appear before " - f"{SECTION_NAMES[tracker.last_section]} " - f"(seen at line {tracker.last_section_line})", - )) + violations.append( + Violation( + rel, + lineno, + "section-order", + f"{SECTION_NAMES[section]} should appear before " + f"{SECTION_NAMES[tracker.last_section]} " + f"(seen at line {tracker.last_section_line})", + ) + ) # --- Check 2: Missing blank line between different sections --- - if (tracker.last_section is not None - and section != tracker.last_section - and not had_blank): - violations.append(Violation( - rel, lineno, "missing-section-separator", - f"blank line expected between {SECTION_NAMES[tracker.last_section]} " - f"and {SECTION_NAMES[section]}", - )) + if tracker.last_section is not None and section != tracker.last_section and not had_blank: + violations.append( + Violation( + rel, + lineno, + "missing-section-separator", + f"blank line expected between {SECTION_NAMES[tracker.last_section]} and {SECTION_NAMES[section]}", + ) + ) # Update tracker if tracker.last_section is None or section >= tracker.last_section: @@ -246,7 +256,7 @@ def check_file(filepath: Path) -> list[Violation]: # and expression blocks like `color: { ... }`) if brace_count > 0 and section == Section.BINDING: colon_idx = stripped.index(":") - after_colon = stripped[colon_idx + 1:].strip() + after_colon = stripped[colon_idx + 1 :].strip() # If content after : doesn't start with an uppercase type name, # it's a JS block (not an inline QML object like `contentItem: Rect {`) if not re.match(r"^[A-Z]", after_colon): @@ -263,10 +273,7 @@ def check_file(filepath: Path) -> list[Violation]: def main(): - qml_files = sorted( - p for p in REPO_ROOT.rglob("*.qml") - if "build" not in p.parts - ) + qml_files = sorted(p for p in REPO_ROOT.rglob("*.qml") if "build" not in p.parts) print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n") From 3e52aa5fb3f41dd6c6c0dc8999f8574a1f1c6cc3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:21:50 +1100 Subject: [PATCH 126/409] fix: lint format script incorrectly popping scope --- scripts/qml-lint-conventions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py index ff0b58162..77fa0b66f 100755 --- a/scripts/qml-lint-conventions.py +++ b/scripts/qml-lint-conventions.py @@ -9,7 +9,8 @@ 3. signal declarations 4. JavaScript functions 5. object properties (bindings) - 6. child objects / component definitions + 6. child objects + 7. component definitions """ import re @@ -184,7 +185,8 @@ def check_file(filepath: Path) -> list[Violation]: func_skip_depth = 0 continue - # Closing brace: pop scope for this indent and all deeper scopes + # Closing brace: pop all scopes deeper than this indent + # (the scope at this indent belongs to the parent object and must persist) if stripped == "}": # Check: blank line right before closing brace if i > 0 and not lines[i - 1].strip(): @@ -196,8 +198,6 @@ def check_file(filepath: Path) -> list[Violation]: "no blank line expected before closing brace", ) ) - scopes.pop(indent, None) - prev_blank.pop(indent, None) to_remove = [k for k in scopes if len(k) > len(indent)] for k in to_remove: del scopes[k] From 022d509f99d4e1ea5b7911630cfaebe219a34806 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 15:44:03 +1100 Subject: [PATCH 127/409] chore: fix components missing-property warnings --- components/StateLayer.qml | 3 +- components/filedialog/DialogButtons.qml | 2 +- components/filedialog/FolderContents.qml | 157 ++++++++++++----------- components/filedialog/Sidebar.qml | 3 +- components/images/CachingIconImage.qml | 3 +- 5 files changed, 87 insertions(+), 81 deletions(-) diff --git a/components/StateLayer.qml b/components/StateLayer.qml index a20e26616..7cd19b575 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -8,7 +8,8 @@ MouseArea { property bool disabled property bool showHoverBackground: true property color color: Colours.palette.m3onSurface - property real radius: parent?.radius ?? 0 + // Pick up radius from parent if it has one (parent can be anything with a radius property) + property real radius: parent?.radius ?? 0 // qmllint disable missing-property property alias rect: hoverLayer function onClicked(): void { diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index ff24efdb8..5a30a12f2 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -1,4 +1,4 @@ -import ".." +import qs.components import qs.services import qs.config import QtQuick.Layouts diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 58fe3fe1c..9789a3da2 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -1,8 +1,9 @@ pragma ComponentBehavior: Bound -import ".." -import "../controls" -import "../images" +import qs.components +import qs.components.filedialog +import qs.components.controls +import qs.components.images import qs.services import qs.config import qs.utils @@ -16,7 +17,7 @@ Item { id: root required property var dialog - property alias currentItem: view.currentItem + readonly property FileEntry currentItem: view.currentItem as FileEntry StyledRect { anchors.fill: parent @@ -91,11 +92,11 @@ Item { Keys.onReturnPressed: { if (root.dialog.selectionValid) - root.dialog.accepted(currentItem.modelData.path); + root.dialog.accepted((currentItem as FileEntry).modelData.path); } Keys.onEnterPressed: { if (root.dialog.selectionValid) - root.dialog.accepted(currentItem.modelData.path); + root.dialog.accepted((currentItem as FileEntry).modelData.path); } StyledScrollBar.vertical: StyledScrollBar { @@ -112,77 +113,7 @@ Item { onPathChanged: view.currentIndex = -1 } - delegate: StyledRect { - id: item - - required property int index - required property FileSystemEntry modelData - - readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 - - implicitWidth: Sizes.itemWidth - implicitHeight: nonAnimHeight - - radius: Appearance.rounding.normal - color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) - z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 - clip: true - - StateLayer { - function onClicked(): void { - view.currentIndex = item.index; - } - - onDoubleClicked: { - if (item.modelData.isDir) - root.dialog.cwd.push(item.modelData.name); - else if (root.dialog.selectionValid) - root.dialog.accepted(item.modelData.path); - } - } - - CachingIconImage { - id: icon - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Appearance.padding.normal - - implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 - - Component.onCompleted: { - const file = item.modelData; - if (file.isImage) - source = Qt.resolvedUrl(file.path); - else if (!file.isDir) - source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); - else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) - source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); - else - source = Quickshell.iconPath("inode-directory"); - } - } - - StyledText { - id: name - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small - anchors.margins: Appearance.padding.normal - - horizontalAlignment: Text.AlignHCenter - elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight - wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap - - Component.onCompleted: text = item.modelData.name - } - - Behavior on implicitHeight { - Anim {} - } - } + delegate: FileEntry {} add: Transition { Anim { @@ -226,4 +157,76 @@ Item { currentItem: view.currentItem } + + component FileEntry: StyledRect { + id: item + + required property int index + required property FileSystemEntry modelData + + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 + + implicitWidth: Sizes.itemWidth + implicitHeight: nonAnimHeight + + radius: Appearance.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) + z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 + clip: true + + StateLayer { + function onClicked(): void { + view.currentIndex = item.index; + } + + onDoubleClicked: { + if (item.modelData.isDir) + root.dialog.cwd.push(item.modelData.name); + else if (root.dialog.selectionValid) + root.dialog.accepted(item.modelData.path); + } + } + + CachingIconImage { + id: icon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Appearance.padding.normal + + implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 + + Component.onCompleted: { + const file = item.modelData; + if (file.isImage) + source = Qt.resolvedUrl(file.path); + else if (!file.isDir) + source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); + else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) + source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); + else + source = Quickshell.iconPath("inode-directory"); + } + } + + StyledText { + id: name + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: icon.bottom + anchors.topMargin: Appearance.spacing.small + anchors.margins: Appearance.padding.normal + + horizontalAlignment: Text.AlignHCenter + elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight + wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap + + Component.onCompleted: text = item.modelData.name + } + + Behavior on implicitHeight { + Anim {} + } + } } diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index 4e83318b1..a6389864e 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -1,6 +1,7 @@ pragma ComponentBehavior: Bound -import ".." +import qs.components +import qs.components.filedialog import qs.services import qs.config import QtQuick diff --git a/components/images/CachingIconImage.qml b/components/images/CachingIconImage.qml index 52c0d14f7..d22b5131d 100644 --- a/components/images/CachingIconImage.qml +++ b/components/images/CachingIconImage.qml @@ -7,7 +7,8 @@ import QtQuick Item { id: root - readonly property int status: loader.item?.status ?? Image.Null + // Easier (and more efficient) to ignore it than to check type and cast + readonly property int status: loader.item?.status ?? Image.Null // qmllint disable missing-property readonly property real actualSize: Math.min(width, height) property real implicitSize property url source From 20c7482b2b72f7bec3191927e87c2f2cfd7f6230 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:04:57 +1100 Subject: [PATCH 128/409] chore: fix bar linter warnings --- config/BorderConfig.qml | 5 +- modules/bar/Bar.qml | 21 +- modules/bar/BarWrapper.qml | 8 +- .../components/workspaces/ActiveIndicator.qml | 5 +- .../workspaces/SpecialWorkspaces.qml | 286 +++++++++--------- .../bar/components/workspaces/Workspaces.qml | 4 +- modules/bar/popouts/ActiveWindow.qml | 4 +- modules/bar/popouts/Audio.qml | 4 +- modules/bar/popouts/Battery.qml | 2 +- modules/bar/popouts/Bluetooth.qml | 4 +- modules/bar/popouts/Content.qml | 48 ++- modules/bar/popouts/Network.qml | 12 +- modules/bar/popouts/PopoutState.qml | 8 + modules/bar/popouts/TrayMenu.qml | 2 +- modules/bar/popouts/WirelessPassword.qml | 16 +- modules/bar/popouts/Wrapper.qml | 17 +- modules/bar/popouts/kblayout/KbLayout.qml | 4 - 17 files changed, 230 insertions(+), 220 deletions(-) create mode 100644 modules/bar/popouts/PopoutState.qml diff --git a/config/BorderConfig.qml b/config/BorderConfig.qml index b203925d1..662320d9e 100644 --- a/config/BorderConfig.qml +++ b/config/BorderConfig.qml @@ -1,8 +1,9 @@ import Quickshell.Io +import qs.config JsonObject { - property int thickness: Appearance.padding.normal - property int rounding: Appearance.rounding.large + property int thickness: Config.appearance.padding.normal + property int rounding: Config.appearance.rounding.large readonly property int minThickness: 2 readonly property int clampedThickness: Math.max(minThickness, thickness) diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 83815d1fa..d4e8e977b 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -23,9 +23,9 @@ ColumnLayout { return; for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i); - if (item?.enabled && item.id === "tray") { - item.item.expanded = false; + const loader = repeater.itemAt(i) as WrappedLoader; + if (loader?.enabled && loader.id === "tray") { + (loader.item as Tray).expanded = false; } } } @@ -43,11 +43,9 @@ ColumnLayout { const id = ch.id; const top = ch.y; - const item = ch.item; - const itemHeight = item.implicitHeight; if (id === "statusIcons" && Config.bar.popouts.statusIcons) { - const items = item.items; + const items = (ch.item as StatusIcons).items; const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); if (icon) { popouts.currentName = icon.name; @@ -55,9 +53,10 @@ ColumnLayout { popouts.hasCurrent = true; } } else if (id === "tray" && Config.bar.popouts.tray) { - if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) { - const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count); - const trayItem = item.items.itemAt(index); + const tray = ch.item as Tray; + if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { + const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); + const trayItem = tray.items.itemAt(index); if (trayItem) { popouts.currentName = `traymenu${index}`; popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); @@ -67,11 +66,11 @@ ColumnLayout { } } else { popouts.hasCurrent = false; - item.expanded = true; + tray.expanded = true; } } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { popouts.currentName = id.toLowerCase(); - popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y; + popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; popouts.hasCurrent = true; } } diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 450e6a529..3a6bdfee7 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.config -import "popouts" as BarPopouts +import qs.modules.bar.popouts as BarPopouts import Quickshell import QtQuick @@ -22,15 +22,15 @@ Item { property bool isHovered function closeTray(): void { - content.item?.closeTray(); + (content.item as Bar)?.closeTray(); } function checkPopout(y: real): void { - content.item?.checkPopout(y); + (content.item as Bar)?.checkPopout(y); } function handleWheel(y: real, angleDelta: point): void { - content.item?.handleWheel(y, angleDelta); + (content.item as Bar)?.handleWheel(y, angleDelta); } visible: width > Config.border.thickness diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index dae54b371..bad146eb2 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -20,13 +20,12 @@ StyledRect { property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 - property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0 + property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0 property real offset: Math.min(leading, trailing) property real size: { const s = Math.abs(leading - trailing) + currentSize; if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) { - const ws = workspaces.itemAt(lastWs); - // console.log(ws, lastWs); + const ws = workspaces.itemAt(lastWs) as Workspace; return ws ? Math.min(ws.y + ws.size - offset, s) : 0; } return s; diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 03394463f..058a6a5f3 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -15,7 +15,7 @@ Item { required property ShellScreen screen readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) - readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? "" + readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" layer.enabled: true layer.effect: OpacityMask { @@ -105,151 +105,14 @@ Item { highlightFollowsCurrentItem: false highlight: Item { y: view.currentItem?.y ?? 0 - implicitHeight: view.currentItem?.size ?? 0 + implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 Behavior on y { Anim {} } } - delegate: ColumnLayout { - id: ws - - required property HyprlandWorkspace modelData - readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) - property int wsId - property string icon - property bool hasWindows - - anchors.left: view.contentItem.left - anchors.right: view.contentItem.right - - spacing: 0 - - Component.onCompleted: { - wsId = modelData.id; - icon = Icons.getSpecialWsIcon(modelData.name); - hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; - } - - // Hacky thing cause modelData gets destroyed before the remove anim finishes - Connections { - function onIdChanged(): void { - if (ws.modelData) - ws.wsId = ws.modelData.id; - } - - function onNameChanged(): void { - if (ws.modelData) - ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); - } - - function onLastIpcObjectChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - - target: ws.modelData - } - - Connections { - function onShowWindowsOnSpecialWorkspacesChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - - target: Config.bar.workspaces - } - - Loader { - id: label - - asynchronous: true - - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 - - sourceComponent: ws.icon.length === 1 ? letterComp : iconComp - - Component { - id: iconComp - - MaterialIcon { - fill: 1 - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - - Component { - id: letterComp - - StyledText { - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - } - - Loader { - id: windows - - asynchronous: true - - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - Layout.preferredHeight: implicitHeight - - visible: active - active: ws.hasWindows - - sourceComponent: Column { - spacing: 0 - - add: Transition { - Anim { - properties: "scale" - from: 0 - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - } - - move: Transition { - Anim { - properties: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - properties: "x,y" - } - } - - Repeater { - model: ScriptModel { - values: { - const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); - const maxIcons = Config.bar.workspaces.maxWindowIcons; - return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; - } - } - - MaterialIcon { - required property var modelData - - grade: 0 - text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") - color: Colours.palette.m3onSurfaceVariant - } - } - } - - Behavior on Layout.preferredHeight { - Anim {} - } - } - } + delegate: SpecialWsDelegate {} add: Transition { Anim { @@ -296,6 +159,145 @@ Item { } } + component SpecialWsDelegate: ColumnLayout { + id: ws + + required property HyprlandWorkspace modelData + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) + property int wsId + property string icon + property bool hasWindows + + anchors.left: view.contentItem.left + anchors.right: view.contentItem.right + + spacing: 0 + + Component.onCompleted: { + wsId = modelData.id; + icon = Icons.getSpecialWsIcon(modelData.name); + hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; + } + + // Hacky thing cause modelData gets destroyed before the remove anim finishes + Connections { + function onIdChanged(): void { + if (ws.modelData) + ws.wsId = ws.modelData.id; + } + + function onNameChanged(): void { + if (ws.modelData) + ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); + } + + function onLastIpcObjectChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + + target: ws.modelData + } + + Connections { + function onShowWindowsOnSpecialWorkspacesChanged(): void { + if (ws.modelData) + ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + + target: Config.bar.workspaces + } + + Loader { + id: label + + asynchronous: true + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + + sourceComponent: ws.icon.length === 1 ? letterComp : iconComp + + Component { + id: iconComp + + MaterialIcon { + fill: 1 + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + + Component { + id: letterComp + + StyledText { + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + } + + Loader { + id: windows + + asynchronous: true + + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.preferredHeight: implicitHeight + + visible: active + active: ws.hasWindows + + sourceComponent: Column { + spacing: 0 + + add: Transition { + Anim { + properties: "scale" + from: 0 + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + + move: Transition { + Anim { + properties: "scale" + to: 1 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + Anim { + properties: "x,y" + } + } + + Repeater { + model: ScriptModel { + values: { + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); + const maxIcons = Config.bar.workspaces.maxWindowIcons; + return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; + } + } + + MaterialIcon { + required property var modelData + + grade: 0 + text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") + color: Colours.palette.m3onSurfaceVariant + } + } + } + + Behavior on Layout.preferredHeight { + Anim {} + } + } + } + Loader { asynchronous: true active: Config.bar.workspaces.activeIndicator @@ -309,7 +311,7 @@ Item { anchors.right: parent.right y: (view.currentItem?.y ?? 0) - view.contentY - implicitHeight: view.currentItem?.size ?? 0 + implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 color: Colours.palette.m3tertiary radius: Appearance.rounding.full @@ -358,7 +360,7 @@ Item { if (Math.abs(event.y - startY) > drag.threshold) return; - const ws = view.itemAt(event.x, event.y); + const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; if (ws?.modelData) Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); else diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index a2231b74c..43f8fba68 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -13,7 +13,7 @@ StyledClippingRect { required property ShellScreen screen - readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== "" + readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId readonly property var occupied: { @@ -92,7 +92,7 @@ StyledClippingRect { MouseArea { anchors.fill: layout onClicked: event => { - const ws = layout.childAt(event.x, event.y).ws; + const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws; if (Hypr.activeWsId !== ws) Hypr.dispatch(`workspace ${ws}`); else diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 9f0162d30..9a1582a3f 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -10,7 +10,7 @@ import QtQuick.Layouts Item { id: root - required property Item wrapper + required property PopoutState popouts implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 implicitHeight: child.implicitHeight @@ -66,7 +66,7 @@ Item { StateLayer { function onClicked(): void { - root.wrapper.detach("winfo"); + root.popouts.detachRequested("winfo"); } radius: Appearance.rounding.normal diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index fbc62a583..20cf826e0 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -12,7 +12,7 @@ import QtQuick.Controls Item { id: root - required property var wrapper + required property PopoutState popouts implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 @@ -112,7 +112,7 @@ Item { text: qsTr("Open settings") icon: "settings" - onClicked: root.wrapper.detach("audio") + onClicked: root.popouts.detachRequested("audio") } } } diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 7c68f4d3c..50cea105c 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -42,7 +42,7 @@ Column { active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None - height: active ? (item?.implicitHeight ?? 0) : 0 + height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 4ac4a6674..3acace1c9 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -13,7 +13,7 @@ import QtQuick.Layouts ColumnLayout { id: root - required property Item wrapper + required property PopoutState popouts spacing: Appearance.spacing.small @@ -173,7 +173,7 @@ ColumnLayout { text: qsTr("Open settings") icon: "settings" - onClicked: root.wrapper.detach("bluetooth") + onClicked: root.popouts.detachRequested("bluetooth") } component Toggle: RowLayout { diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index f866b45cc..5ba8b3c07 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -11,7 +11,7 @@ import "./kblayout" Item { id: root - required property Item wrapper + required property PopoutState popouts readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null @@ -29,7 +29,7 @@ Item { Popout { name: "activewindow" sourceComponent: ActiveWindow { - wrapper: root.wrapper + popouts: root.popouts } } @@ -38,7 +38,7 @@ Item { name: "network" sourceComponent: Network { - wrapper: root.wrapper + popouts: root.popouts view: "wireless" } } @@ -46,7 +46,7 @@ Item { Popout { name: "ethernet" sourceComponent: Network { - wrapper: root.wrapper + popouts: root.popouts view: "ethernet" } } @@ -58,39 +58,39 @@ Item { sourceComponent: WirelessPassword { id: passwordComponent - wrapper: root.wrapper - network: networkPopout.item?.passwordNetwork ?? null + popouts: root.popouts + network: (networkPopout.item as Network)?.passwordNetwork ?? null } Connections { function onCurrentNameChanged() { // Update network immediately when password popout becomes active - if (root.wrapper.currentName === "wirelesspassword") { + if (root.popouts.currentName === "wirelesspassword") { // Set network immediately if available - if (networkPopout.item && networkPopout.item.passwordNetwork) { + if ((networkPopout.item as Network)?.passwordNetwork) { if (passwordPopout.item) { - passwordPopout.item.network = networkPopout.item.passwordNetwork; + (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } } // Also try after a short delay in case networkPopout.item wasn't ready Qt.callLater(() => { - if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { - passwordPopout.item.network = networkPopout.item.passwordNetwork; + if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) { + (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } }, 100); } } - target: root.wrapper + target: root.popouts } Connections { function onItemChanged() { // When network popout loads, update password popout if it's active - if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { + if (root.popouts.currentName === "wirelesspassword" && passwordPopout.item) { Qt.callLater(() => { - if (networkPopout.item && networkPopout.item.passwordNetwork) { - passwordPopout.item.network = networkPopout.item.passwordNetwork; + if ((networkPopout.item as Network)?.passwordNetwork) { + (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } }); } @@ -103,7 +103,7 @@ Item { Popout { name: "bluetooth" sourceComponent: Bluetooth { - wrapper: root.wrapper + popouts: root.popouts } } @@ -115,15 +115,13 @@ Item { Popout { name: "audio" sourceComponent: Audio { - wrapper: root.wrapper + popouts: root.popouts } } Popout { name: "kblayout" - sourceComponent: KbLayout { - wrapper: root.wrapper - } + sourceComponent: KbLayout {} } Popout { @@ -147,21 +145,21 @@ Item { Connections { function onHasCurrentChanged(): void { - if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { + if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { trayMenu.sourceComponent = null; trayMenu.sourceComponent = trayMenuComp; } } - target: root.wrapper + target: root.popouts } Component { id: trayMenuComp TrayMenu { - popouts: root.wrapper - trayItem: trayMenu.modelData.menu + popouts: root.popouts + trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type } } } @@ -172,7 +170,7 @@ Item { id: popout required property string name - readonly property bool shouldBeActive: root.wrapper.currentName === name + readonly property bool shouldBeActive: root.popouts.currentName === name anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 91991fcbb..fc2f1d408 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -12,7 +12,7 @@ import QtQuick.Layouts ColumnLayout { id: root - required property Item wrapper + required property PopoutState popouts property string connectingToSsid: "" property string view: "wireless" // "wireless" or "ethernet" @@ -132,7 +132,7 @@ ColumnLayout { // Password is required - show password dialog root.passwordNetwork = network; root.showPasswordDialog = true; - root.wrapper.currentName = "wirelesspassword"; + root.popouts.currentName = "wirelesspassword"; }); // Clear connecting state if connection succeeds immediately (saved profile) @@ -341,8 +341,8 @@ ColumnLayout { if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { root.showPasswordDialog = false; root.passwordNetwork = null; - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; + if (root.popouts.currentName === "wirelesspassword") { + root.popouts.currentName = "network"; } } } @@ -359,13 +359,13 @@ ColumnLayout { Connections { function onCurrentNameChanged(): void { // Clear password network when leaving password dialog - if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { + if (root.popouts.currentName !== "wirelesspassword" && root.showPasswordDialog) { root.showPasswordDialog = false; root.passwordNetwork = null; } } - target: root.wrapper + target: root.popouts } component Toggle: RowLayout { diff --git a/modules/bar/popouts/PopoutState.qml b/modules/bar/popouts/PopoutState.qml new file mode 100644 index 000000000..6be8169b4 --- /dev/null +++ b/modules/bar/popouts/PopoutState.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + property string currentName + property bool hasCurrent + + signal detachRequested(mode: string) +} diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index eac73eeb1..09194d97e 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -11,7 +11,7 @@ import QtQuick.Controls StackView { id: root - required property Item popouts + required property PopoutState popouts required property QsMenuHandle trayItem implicitWidth: currentItem?.implicitWidth ?? 0 diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 0c0f301f4..4d0b7aed5 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -12,11 +12,11 @@ import QtQuick.Layouts ColumnLayout { id: root - required property Item wrapper + required property PopoutState popouts property var network: null property bool isClosing: false - readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" + readonly property bool shouldBeVisible: root.popouts.currentName === "wirelesspassword" function checkConnectionStatus(): void { if (!root.shouldBeVisible || !connectButton.connecting) { @@ -64,8 +64,8 @@ ColumnLayout { connectionMonitor.stop(); // Return to network popout - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; + if (root.popouts.currentName === "wirelesspassword") { + root.popouts.currentName = "network"; } } @@ -94,7 +94,7 @@ ColumnLayout { Connections { function onCurrentNameChanged() { - if (root.wrapper.currentName === "wirelesspassword") { + if (root.popouts.currentName === "wirelesspassword") { // Update network when popout becomes active Qt.callLater(() => { // Try to get network from parent Content's networkPopout @@ -112,7 +112,7 @@ ColumnLayout { } } - target: root.wrapper + target: root.popouts } Timer { @@ -578,8 +578,8 @@ ColumnLayout { connectButton.connecting = false; connectButton.text = qsTr("Connect"); // Return to network popout on successful connection - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; + if (root.popouts.currentName === "wirelesspassword") { + root.popouts.currentName = "network"; } closeDialog(); } diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 40479f9a9..888611669 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -17,11 +17,12 @@ Item { readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight - readonly property Item current: content.item?.current ?? null + readonly property Item current: (content.item as Content)?.current ?? null - property string currentName + property alias currentName: popoutState.currentName property real currentCenter - property bool hasCurrent + property alias hasCurrent: popoutState.hasCurrent + readonly property PopoutState state: popoutState property string detachedMode property string queuedMode @@ -59,7 +60,7 @@ Item { Keys.onEscapePressed: { // Forward escape to password popout if active, otherwise close if (currentName === "wirelesspassword" && content.item) { - const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); + const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); if (passwordPopout && passwordPopout.item) { passwordPopout.item.closeDialog(); return; @@ -75,6 +76,12 @@ Item { } } + PopoutState { + id: popoutState + + onDetachRequested: mode => root.detach(mode) + } + HyprlandFocusGrab { active: root.isDetached windows: [QsWindow.window] @@ -105,7 +112,7 @@ Item { anchors.verticalCenter: parent.verticalCenter sourceComponent: Content { - wrapper: root + popouts: popoutState } } diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 74ee67e12..68a7a7947 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -7,13 +7,9 @@ import qs.components import qs.services import qs.config -import "." - ColumnLayout { id: root - required property Item wrapper - function refresh() { kb.refresh(); } From 83cd02b90dd3dd20fdd6d2c8ad1eb6481c9a24be Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:22:19 +1100 Subject: [PATCH 129/409] chore: fix launcher linter warnings --- modules/launcher/AppList.qml | 4 ++-- modules/launcher/Content.qml | 2 +- modules/launcher/ContentList.qml | 2 +- modules/launcher/WallpaperList.qml | 4 ++-- modules/launcher/items/AppItem.qml | 2 +- modules/launcher/items/SchemeItem.qml | 2 +- modules/launcher/items/VariantItem.qml | 2 +- modules/launcher/items/WallpaperItem.qml | 2 +- modules/launcher/services/Schemes.qml | 2 +- 9 files changed, 11 insertions(+), 11 deletions(-) diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 6a2901bc2..2c9d52fea 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound -import "items" -import "services" +import qs.modules.launcher.items +import qs.modules.launcher.services import qs.components import qs.components.controls import qs.components.containers diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 885b979e8..40be12274 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,6 +1,6 @@ pragma ComponentBehavior: Bound -import "services" +import qs.modules.launcher.services import qs.components import qs.components.controls import qs.services diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index b52940472..204f183b1 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -19,7 +19,7 @@ Item { required property int rounding readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) - readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item + readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index 4aba4365b..542a64e8a 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -18,7 +18,7 @@ PathView { readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 readonly property int numItems: { - const screen = QsWindow.window?.screen; + const screen = (QsWindow.window as QsWindow)?.screen; if (!screen) return 0; @@ -58,7 +58,7 @@ PathView { onCurrentItemChanged: { if (currentItem) - Wallpapers.preview(currentItem.modelData.path); + Wallpapers.preview((currentItem as WallpaperItem).modelData.path); } implicitWidth: Math.min(numItems, count) * itemWidth diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index eb738a84d..812172b5a 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -1,4 +1,4 @@ -import "../services" +import qs.modules.launcher.services import qs.components import qs.services import qs.config diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 65bd65260..c041b3258 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -1,4 +1,4 @@ -import "../services" +import qs.modules.launcher.services import qs.components import qs.services import qs.config diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index 53bffe15c..9554f5092 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -1,4 +1,4 @@ -import "../services" +import qs.modules.launcher.services import qs.components import qs.services import qs.config diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 476efc452..22e2bff28 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -14,7 +14,7 @@ Item { scale: 0.5 opacity: 0 - z: PathView.z ?? 0 + z: PathView.z ?? 0 // qmllint disable missing-property Component.onCompleted: { scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml index dbb2dac0a..646dd10a4 100644 --- a/modules/launcher/services/Schemes.qml +++ b/modules/launcher/services/Schemes.qml @@ -55,7 +55,7 @@ Searcher { for (const f of s) flat.push(f); - schemes.model = flat.sort((a, b) => (a.name + a.flavour).localeCompare((b.name + b.flavour))); + schemes.model = flat.sort((a, b) => String(a.name + a.flavour).localeCompare((b.name + b.flavour))); } } } From 53109b28e6e088f1c2d8d5c780864011ba8a9c42 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:31:43 +1100 Subject: [PATCH 130/409] chore: fix dash, drawers, notifs and osd linter warnings --- modules/dashboard/Wrapper.qml | 4 +- modules/drawers/Drawers.qml | 5 +- modules/drawers/Exclusions.qml | 3 +- modules/drawers/Interactions.qml | 8 +- modules/drawers/Panels.qml | 7 +- modules/notifications/Content.qml | 177 +++++++++++++++--------------- modules/notifications/Wrapper.qml | 9 +- modules/osd/Content.qml | 2 +- modules/osd/Wrapper.qml | 2 +- 9 files changed, 113 insertions(+), 104 deletions(-) diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index ce6330a44..3413f6e5a 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -12,7 +12,7 @@ Item { id: root required property DrawerVisibilities visibilities - readonly property bool needsKeyboard: content.item?.needsKeyboard ?? false + readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false readonly property DashboardState dashState: DashboardState { reloadableId: "dashboardState" } @@ -28,7 +28,7 @@ Item { } } - readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 + readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 visible: height > 0 implicitHeight: 0 diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index ffa4f8d2c..8d99935b8 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -10,6 +10,7 @@ import Quickshell import Quickshell.Wayland import Quickshell.Hyprland import QtQuick +import QtQuick.Controls import QtQuick.Effects Variants { @@ -35,7 +36,7 @@ Variants { return 0; const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject?.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject?.windows > 0) + if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) return 0; const thresholds = []; @@ -90,7 +91,7 @@ Variants { HyprlandFocusGrab { id: focusGrab - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && panels.popouts.current?.depth > 1) + active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) windows: [win] onCleared: { visibilities.launcher = false; diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index e4015c89a..0ec1f8ef5 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import qs.components.containers import qs.config +import qs.modules.bar as Bar import Quickshell import QtQuick @@ -9,7 +10,7 @@ Scope { id: root required property ShellScreen screen - required property Item bar + required property Bar.BarWrapper bar ExclusionZone { anchors.left: true diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 29461e803..780990447 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -1,9 +1,11 @@ import qs.components import qs.components.controls import qs.config +import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts import Quickshell import QtQuick +import QtQuick.Controls CustomMouseArea { id: root @@ -12,7 +14,7 @@ CustomMouseArea { required property BarPopouts.Wrapper popouts required property DrawerVisibilities visibilities required property Panels panels - required property Item bar + required property Bar.BarWrapper bar property point dragStart property bool dashboardShortcutActive @@ -69,7 +71,7 @@ CustomMouseArea { if (!utilitiesShortcutActive) visibilities.utilities = false; - if (!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) { + if (!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) { popouts.hasCurrent = false; bar.closeTray(); } @@ -202,7 +204,7 @@ CustomMouseArea { // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); - } else if ((!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { + } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { popouts.hasCurrent = false; bar.closeTray(); } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index deb7af163..98313ae10 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -5,6 +5,7 @@ import qs.modules.notifications as Notifications import qs.modules.session as Session import qs.modules.launcher as Launcher import qs.modules.dashboard as Dashboard +import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities as Utilities import qs.modules.utilities.toasts as Toasts @@ -17,7 +18,7 @@ Item { required property ShellScreen screen required property DrawerVisibilities visibilities - required property Item bar + required property Bar.BarWrapper bar readonly property alias osd: osd readonly property alias notifications: notifications @@ -49,7 +50,9 @@ Item { id: notifications visibilities: root.visibilities - panels: root + sidebarPanel: sidebar + osdPanel: osd + sessionPanel: session anchors.top: parent.top anchors.right: parent.right diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 46075a20e..96f09b28b 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -11,7 +11,8 @@ Item { id: root required property DrawerVisibilities visibilities - required property Item panels + required property Item osdPanel + required property Item sessionPanel readonly property int padding: Appearance.padding.large anchors.top: parent.top @@ -26,23 +27,21 @@ Item { let height = (count - 1) * Appearance.spacing.smaller; for (let i = 0; i < count; i++) - height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; + height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; - if (visibilities && panels) { - if (visibilities.osd) { - const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; - } + if (visibilities.osd) { + const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; + } - if (visibilities.session) { - const h = panels.session.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; - } + if (visibilities.session) { + const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; } - return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { @@ -63,79 +62,9 @@ Item { orientation: Qt.Vertical spacing: 0 - cacheBuffer: QsWindow.window?.screen.height ?? 0 - - delegate: Item { - id: wrapper - - required property NotifData modelData - required property int index - readonly property alias nonAnimHeight: notif.nonAnimHeight - property int idx - - onIndexChanged: { - if (index !== -1) - idx = index; - } - - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) - - ListView.onRemove: removeAnim.start() + cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0 - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: true - } - PropertyAction { - target: wrapper - property: "enabled" - value: false - } - PropertyAction { - target: wrapper - property: "implicitHeight" - value: 0 - } - PropertyAction { - target: wrapper - property: "z" - value: 1 - } - Anim { - target: notif - property: "x" - to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized - } - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: false - } - } - - ClippingRectangle { - anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller - - color: "transparent" - radius: notif.radius - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight - - Notification { - id: notif - - modelData: wrapper.modelData - } - } - } + delegate: NotifWrapper {} move: Transition { Anim { @@ -160,7 +89,7 @@ Item { let height = 0; for (let i = 0; i < count; i++) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return i; @@ -181,7 +110,7 @@ Item { let height = 0; for (let i = count - 1; i >= 0; i--) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; if (height - Appearance.spacing.smaller >= scrollY) return count - i - 1; @@ -197,6 +126,78 @@ Item { Anim {} } + component NotifWrapper: Item { + id: wrapper + + required property NotifData modelData + required property int index + readonly property alias nonAnimHeight: notif.nonAnimHeight + property int idx + + onIndexChanged: { + if (index !== -1) + idx = index; + } + + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) + + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: true + } + PropertyAction { + target: wrapper + property: "enabled" + value: false + } + PropertyAction { + target: wrapper + property: "implicitHeight" + value: 0 + } + PropertyAction { + target: wrapper + property: "z" + value: 1 + } + Anim { + target: notif + property: "x" + to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 + duration: Appearance.anim.durations.normal + easing.bezierCurve: Appearance.anim.curves.emphasized + } + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: false + } + } + + ClippingRectangle { + anchors.top: parent.top + anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller + + color: "transparent" + radius: notif.radius + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight + + Notification { + id: notif + + modelData: wrapper.modelData + } + } + } + component Anim: NumberAnimation { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 61acc56e1..accc9d71d 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -5,11 +5,13 @@ import QtQuick Item { id: root - required property var visibilities - required property Item panels + required property DrawerVisibilities visibilities + required property Item sidebarPanel + property alias osdPanel: content.osdPanel + property alias sessionPanel: content.sessionPanel visible: height > 0 - implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) + implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) implicitHeight: content.implicitHeight states: State { @@ -34,6 +36,5 @@ Item { id: content visibilities: root.visibilities - panels: root.panels } } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 53c50d7f1..bcc89fdf8 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -12,7 +12,7 @@ Item { id: root required property Brightness.Monitor monitor - required property var visibilities + required property DrawerVisibilities visibilities required property real volume required property bool muted diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index e674d638e..5b192772b 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -10,7 +10,7 @@ Item { id: root required property ShellScreen screen - required property var visibilities + required property DrawerVisibilities visibilities property bool hovered readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) From 8090f21499a0abcdee45591b5869db1b9f1ade2c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:38:48 +1100 Subject: [PATCH 131/409] chore: fix sidebar linter warnings --- modules/sidebar/Content.qml | 2 +- modules/sidebar/Notif.qml | 50 +++--- modules/sidebar/NotifDock.qml | 11 +- modules/sidebar/NotifDockList.qml | 204 +++++++++++------------ modules/sidebar/NotifGroup.qml | 2 +- modules/sidebar/NotifGroupList.qml | 254 +++++++++++++++-------------- modules/sidebar/Wrapper.qml | 2 +- 7 files changed, 266 insertions(+), 259 deletions(-) diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index 1b7feed66..e09dd6a9a 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -8,7 +8,7 @@ Item { id: root required property Props props - required property var visibilities + required property DrawerVisibilities visibilities ColumnLayout { id: layout diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index bfe7dd556..01593f2b5 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -13,9 +13,9 @@ StyledRect { required property NotifData modelData required property Props props required property bool expanded - required property var visibilities + required property DrawerVisibilities visibilities - readonly property StyledText body: expandedContent.item?.body ?? null + readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight @@ -118,36 +118,38 @@ StyledRect { anchors.right: parent.right anchors.topMargin: Appearance.spacing.small / 2 - sourceComponent: ColumnLayout { - readonly property alias body: body + sourceComponent: ExpandedBody {} + } - spacing: Appearance.spacing.smaller + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } - StyledText { - id: body + component ExpandedBody: ColumnLayout { + readonly property alias body: bodyText - Layout.fillWidth: true - textFormat: Text.MarkdownText - text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") - color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline - wrapMode: Text.WordWrap + spacing: Appearance.spacing.smaller - onLinkActivated: link => { - Quickshell.execDetached(["app2unit", "-O", "--", link]); - root.visibilities.sidebar = false; - } - } + StyledText { + id: bodyText - NotifActionList { - notif: root.modelData + Layout.fillWidth: true + textFormat: Text.MarkdownText + text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") + color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + wrapMode: Text.WordWrap + + onLinkActivated: link => { + Quickshell.execDetached(["app2unit", "-O", "--", link]); + root.visibilities.sidebar = false; } } - } - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + NotifActionList { + notif: root.modelData } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index c38bddcea..daa1dfda1 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -15,7 +15,7 @@ Item { id: root required property Props props - required property var visibilities + required property DrawerVisibilities visibilities readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent @@ -156,13 +156,14 @@ Item { let next = null; for (let i = 0; i < notifList.repeater.count; i++) { next = notifList.repeater.itemAt(i); - if (!next?.closed) + if (!next?.closed) // qmllint disable missing-property break; } - if (next) - next.closeAll(); - else + if (next) { + next.closeAll(); // qmllint disable missing-property + } else { stop(); + } } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index b927e91a7..8ab76a578 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -11,7 +11,7 @@ Item { required property Props props required property Flickable container - required property var visibilities + required property DrawerVisibilities visibilities readonly property alias repeater: repeater readonly property int spacing: Appearance.spacing.small @@ -39,128 +39,130 @@ Item { onValuesChanged: root.flagChanged() } - MouseArea { - id: notif + delegate: NotifGroupDelegate {} + } - required property int index - required property string modelData + component NotifGroupDelegate: MouseArea { + id: notif - readonly property bool closed: notifInner.notifCount === 0 - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - property int startY + required property int index + required property string modelData - function closeAll(): void { - for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) - n.close(); - } + readonly property bool closed: notifInner.notifCount === 0 + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + property int startY - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (!item.closed) - y += item.nonAnimHeight + root.spacing; - } - return y; + function closeAll(): void { + for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) + n.close(); + } + + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i) as NotifGroupDelegate; + if (item && !item.closed) + y += item.nonAnimHeight + root.spacing; } + return y; + } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); } + } - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + implicitHeight: notifInner.implicitHeight - hoverEnabled: true - cursorShape: pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: true - enabled: !closed + hoverEnabled: true + cursorShape: pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: true + enabled: !closed - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - notifInner.toggleExpand(!notifInner.expanded); - else if (event.button === Qt.MiddleButton) - closeAll(); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + notifInner.toggleExpand(!notifInner.expanded); + else if (event.button === Qt.MiddleButton) + closeAll(); + } + onPositionChanged: event => { + if (pressed) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + notifInner.toggleExpand(diffY > 0); } - onPositionChanged: event => { - if (pressed) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - notifInner.toggleExpand(diffY > 0); - } + } + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + closeAll(); + } + + ParallelAnimation { + running: true + + Anim { + target: notif + property: "opacity" + from: 0 + to: 1 } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - closeAll(); + Anim { + target: notif + property: "scale" + from: 0 + to: 1 + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } + } - ParallelAnimation { - running: true - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: notif - property: "scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } + ParallelAnimation { + running: notif.closed - ParallelAnimation { - running: notif.closed - - Anim { - target: notif - property: "opacity" - to: 0 - } - Anim { - target: notif - property: "scale" - to: 0.6 - } + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "scale" + to: 0.6 } + } - NotifGroup { - id: notifInner + NotifGroup { + id: notifInner - modelData: notif.modelData - props: root.props - container: root.container - visibilities: root.visibilities - } + modelData: notif.modelData + props: root.props + container: root.container + visibilities: root.visibilities + } - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } + } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 6a92ce05f..da3f2b80d 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -16,7 +16,7 @@ StyledRect { required property string modelData required property Props props required property Flickable container - required property var visibilities + required property DrawerVisibilities visibilities readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) readonly property var groupProps: { diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 2d9ba38b5..a01cfb0a4 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -14,13 +14,13 @@ Item { required property list notifs required property bool expanded required property Flickable container - required property var visibilities + required property DrawerVisibilities visibilities readonly property real nonAnimHeight: { let h = -root.spacing; for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i); - if (!item.modelData.closed && !item.previewHidden) + const item = repeater.itemAt(i) as NotifDelegate; + if (item && !item.modelData.closed && !item.previewHidden) h += item.nonAnimHeight + root.spacing; } return h; @@ -59,155 +59,157 @@ Item { onValuesChanged: root.flagChanged() } - MouseArea { - id: notif + delegate: NotifDelegate {} + } + + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } - required property int index - required property NotifData modelData + component NotifDelegate: MouseArea { + id: notif - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - readonly property bool previewHidden: { - if (root.expanded) - return false; + required property int index + required property NotifData modelData - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i].closed) - extraHidden++; + readonly property alias nonAnimHeight: notifInner.nonAnimHeight + readonly property bool previewHidden: { + if (root.expanded) + return false; - return index >= Config.notifs.groupPreviewNum + extraHidden; - } - property int startY - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (!item.modelData.closed && !item.previewHidden) - y += item.nonAnimHeight + root.spacing; - } - return y; + let extraHidden = 0; + for (let i = 0; i < index; i++) + if (root.notifs[i].closed) + extraHidden++; + + return index >= Config.notifs.groupPreviewNum + extraHidden; + } + property int startY + + y: { + root.flag; // Force update + let y = 0; + for (let i = 0; i < index; i++) { + const item = repeater.itemAt(i) as NotifDelegate; + if (item && !item.modelData.closed && !item.previewHidden) + y += item.nonAnimHeight + root.spacing; } + return y; + } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); } + } - opacity: previewHidden ? 0 : 1 - scale: previewHidden ? 0.7 : 1 + opacity: previewHidden ? 0 : 1 + scale: previewHidden ? 0.7 : 1 - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + implicitWidth: root.width + implicitHeight: notifInner.implicitHeight - hoverEnabled: true - cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: !root.expanded - enabled: !modelData.closed + hoverEnabled: true + cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: !root.expanded + enabled: !modelData.closed - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - root.requestToggleExpand(!root.expanded); - else if (event.button === Qt.MiddleButton) - modelData.close(); - } - onPositionChanged: event => { - if (pressed && !root.expanded) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - root.requestToggleExpand(diffY > 0); - } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - modelData.close(); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + root.requestToggleExpand(!root.expanded); + else if (event.button === Qt.MiddleButton) + modelData.close(); + } + onPositionChanged: event => { + if (pressed && !root.expanded) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + root.requestToggleExpand(diffY > 0); } + } + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + modelData.close(); + } - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - ParallelAnimation { - Component.onCompleted: running = !notif.previewHidden - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 - } - } + Component.onCompleted: modelData.lock(this) + Component.onDestruction: modelData.unlock(this) + + ParallelAnimation { + Component.onCompleted: running = !notif.previewHidden - ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) - - Anim { - target: notif - property: "opacity" - to: 0 - } - Anim { - target: notif - property: "x" - to: notif.x >= 0 ? notif.width : -notif.width - } + Anim { + target: notif + property: "opacity" + from: 0 + to: 1 + } + Anim { + target: notif + property: "scale" + from: 0.7 + to: 1 } + } - Notif { - id: notifInner + ParallelAnimation { + running: notif.modelData.closed + onFinished: notif.modelData.unlock(notif) - anchors.fill: parent - modelData: notif.modelData - props: root.props - expanded: root.expanded - visibilities: root.visibilities + Anim { + target: notif + property: "opacity" + to: 0 } - - Behavior on opacity { - Anim {} + Anim { + target: notif + property: "x" + to: notif.x >= 0 ? notif.width : -notif.width } + } - Behavior on scale { - Anim {} - } + Notif { + id: notifInner - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } + anchors.fill: parent + modelData: notif.modelData + props: root.props + expanded: root.expanded + visibilities: root.visibilities + } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim {} + } + + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - } - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 9303c6b94..d38de3e63 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -7,7 +7,7 @@ import QtQuick Item { id: root - required property var visibilities + required property DrawerVisibilities visibilities required property var panels readonly property Props props: Props {} From 407324208f83753599bfe3ddcd1819f7d151ced1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:40:07 +1100 Subject: [PATCH 132/409] chore: fix utililities linter warnings --- modules/utilities/Content.qml | 6 ++++-- modules/utilities/Wrapper.qml | 2 +- modules/utilities/cards/Record.qml | 2 +- modules/utilities/cards/RecordingList.qml | 2 +- modules/utilities/cards/Toggles.qml | 11 ++++++----- 5 files changed, 13 insertions(+), 10 deletions(-) diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 902656de5..507debc0c 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -1,5 +1,7 @@ import "cards" +import qs.components import qs.config +import qs.modules.bar.popouts as BarPopouts import QtQuick import QtQuick.Layouts @@ -7,8 +9,8 @@ Item { id: root required property var props - required property var visibilities - required property Item popouts + required property DrawerVisibilities visibilities + required property BarPopouts.Wrapper popouts implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index d3371dde9..c8660a2f3 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -8,7 +8,7 @@ import QtQuick Item { id: root - required property var visibilities + required property DrawerVisibilities visibilities required property Item sidebar required property Item popouts diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 407b88ae3..b8653159c 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -11,7 +11,7 @@ StyledRect { id: root required property var props - required property var visibilities + required property DrawerVisibilities visibilities Layout.fillWidth: true implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 355ee0194..af006cf97 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -16,7 +16,7 @@ ColumnLayout { id: root required property var props - required property var visibilities + required property DrawerVisibilities visibilities spacing: 0 diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 2ca93db6d..4f17073d9 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -4,6 +4,7 @@ import qs.components import qs.components.controls import qs.services import qs.config +import qs.modules.bar.popouts as BarPopouts import Quickshell.Bluetooth import QtQuick import QtQuick.Layouts @@ -11,8 +12,8 @@ import QtQuick.Layouts StyledRect { id: root - required property var visibilities - required property Item popouts + required property DrawerVisibilities visibilities + required property BarPopouts.Wrapper popouts readonly property var quickToggles: { const seenIds = new Set(); @@ -54,17 +55,17 @@ StyledRect { font.pointSize: Appearance.font.size.normal } - ToggleRow { + QuickToggleRow { rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles } - ToggleRow { + QuickToggleRow { visible: root.needExtraRow rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] } } - component ToggleRow: RowLayout { + component QuickToggleRow: RowLayout { property var rowModel: [] Layout.fillWidth: true From beaefc6f36384311a9e93c999492d7cbb9648d59 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:45:23 +1100 Subject: [PATCH 133/409] chore: fix service linter warnings --- services/Audio.qml | 2 +- services/Brightness.qml | 14 +++++++------- services/Colours.qml | 1 + services/GameMode.qml | 2 +- services/NotifData.qml | 3 +++ services/Visibilities.qml | 1 + services/Wallpapers.qml | 1 + 7 files changed, 15 insertions(+), 9 deletions(-) diff --git a/services/Audio.qml b/services/Audio.qml index d3e73ab6f..515e93a40 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -92,7 +92,7 @@ Singleton { if (!stream) return qsTr("Unknown"); // Try application name first, then description, then name - return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); + return stream.properties["application.name"] || stream.description || stream.name || qsTr("Unknown Application"); } onSinkChanged: { diff --git a/services/Brightness.qml b/services/Brightness.qml index 907c0b092..4e53ec097 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -17,34 +17,34 @@ Singleton { map[m.connector] = m; return map; } - readonly property list monitors: variants.instances + readonly property list monitors: variants.instances // qmllint disable incompatible-type property bool appleDisplayPresent: false function getMonitorForScreen(screen: ShellScreen): var { - return monitors.find(m => m.modelData === screen); + return monitors.find(m => m.modelData === screen); // qmllint disable missing-property } function getMonitor(query: string): var { if (query === "active") { - return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); + return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property } if (query.startsWith("model:")) { const model = query.slice(6); - return monitors.find(m => m.modelData.model === model); + return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property } if (query.startsWith("serial:")) { const serial = query.slice(7); - return monitors.find(m => m.modelData.serialNumber === serial); + return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property } if (query.startsWith("id:")) { const id = parseInt(query.slice(3), 10); - return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); + return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property } - return monitors.find(m => m.modelData.name === query); + return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property } function increaseBrightness(): void { diff --git a/services/Colours.qml b/services/Colours.qml index cd86c8fbf..09ec669a7 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -1,6 +1,7 @@ pragma Singleton pragma ComponentBehavior: Bound +import qs.services import qs.config import qs.utils import Caelestia diff --git a/services/GameMode.qml b/services/GameMode.qml index 6e9d9604b..ee0e6a99e 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -40,7 +40,7 @@ Singleton { PersistentProperties { id: props - property bool enabled: Hypr.options["animations:enabled"] === 0 + property bool enabled: Hypr.options["animations:enabled"] === 0 // qmllint disable missing-property reloadableId: "gameMode" } diff --git a/services/NotifData.qml b/services/NotifData.qml index d1648fa75..9806c96ec 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import qs.services import qs.config import qs.utils import Caelestia @@ -139,7 +140,9 @@ QtObject { } function onActionsChanged(): void { + // qmllint disable unresolved-type notif.actions = notif.notification.actions.map(a => ({ + // qmllint enable unresolved-type identifier: a.identifier, text: a.text, invoke: () => a.invoke() diff --git a/services/Visibilities.qml b/services/Visibilities.qml index f26e43286..4305f5795 100644 --- a/services/Visibilities.qml +++ b/services/Visibilities.qml @@ -1,6 +1,7 @@ pragma Singleton import qs.components +import qs.services import Quickshell Singleton { diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index c1f3c1840..18310eaf8 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -1,5 +1,6 @@ pragma Singleton +import qs.services import qs.config import qs.utils import Caelestia.Models From b64421d614e9a94135111e7c8e58f40c0169472b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 21 Mar 2026 16:47:38 +1100 Subject: [PATCH 134/409] chore: misc fixes --- services/Nmcli.qml | 3 +++ utils/Paths.qml | 1 + 2 files changed, 4 insertions(+) diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 18fb02c8e..3275a07c8 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -30,6 +30,9 @@ Singleton { readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property list activeProcesses: [] + readonly property alias connectionCheckTimer: connectionCheckTimer + readonly property alias immediateCheckTimer: immediateCheckTimer + // Constants readonly property string deviceTypeWifi: "wifi" readonly property string deviceTypeEthernet: "ethernet" diff --git a/utils/Paths.qml b/utils/Paths.qml index bc89770ab..3a0e4ee2c 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -3,6 +3,7 @@ pragma Singleton import qs.config import Caelestia import Quickshell +import QtQuick Singleton { id: root From be3da5a1144d2e1b4d5100f8b538a9260db2bfb0 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:12:57 +1100 Subject: [PATCH 135/409] fix: control center Tbh idk what the issue was, but the entire implementation is extremely scuffed and we need to remake it completely --- modules/controlcenter/Panes.qml | 8 ++ .../appearance/AppearancePane.qml | 8 ++ .../appearance/sections/AnimationsSection.qml | 9 +- .../appearance/sections/BackgroundSection.qml | 97 ++++++++++--------- .../appearance/sections/BorderSection.qml | 15 +-- .../sections/ColorSchemeSection.qml | 20 ++-- .../sections/ColorVariantSection.qml | 16 +-- .../appearance/sections/FontsSection.qml | 43 ++++---- .../appearance/sections/ScalesSection.qml | 21 ++-- .../appearance/sections/ThemeModeSection.qml | 4 + .../sections/TransparencySection.qml | 21 ++-- modules/controlcenter/audio/AudioPane.qml | 70 ++++++------- modules/controlcenter/bluetooth/BtPane.qml | 4 + .../controlcenter/bluetooth/DeviceList.qml | 1 + .../components/ConnectedButtonGroup.qml | 18 ++-- .../components/DeviceDetails.qml | 4 + .../controlcenter/components/DeviceList.qml | 2 + .../components/ReadonlySlider.qml | 3 + .../controlcenter/components/SliderInput.qml | 1 + .../components/SplitPaneLayout.qml | 1 + .../components/SplitPaneWithDetails.qml | 7 ++ .../components/WallpaperGrid.qml | 48 ++++----- .../controlcenter/dashboard/DashboardPane.qml | 4 + .../dashboard/GeneralSection.qml | 2 + .../dashboard/PerformanceSection.qml | 2 + .../controlcenter/launcher/LauncherPane.qml | 30 +++--- modules/controlcenter/launcher/Settings.qml | 2 + .../controlcenter/network/EthernetDetails.qml | 2 + .../controlcenter/network/EthernetList.qml | 33 ++++--- .../controlcenter/network/EthernetPane.qml | 3 + .../network/EthernetSettings.qml | 2 + .../controlcenter/network/NetworkSettings.qml | 1 + .../controlcenter/network/NetworkingPane.qml | 4 + modules/controlcenter/network/VpnDetails.qml | 2 + modules/controlcenter/network/VpnSettings.qml | 3 + .../controlcenter/network/WirelessDetails.qml | 15 +-- .../controlcenter/network/WirelessList.qml | 45 ++++----- .../controlcenter/network/WirelessPane.qml | 3 + .../network/WirelessPasswordDialog.qml | 17 ++-- .../network/WirelessSettings.qml | 1 + modules/controlcenter/taskbar/TaskbarPane.qml | 1 + 41 files changed, 341 insertions(+), 252 deletions(-) diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 3c0f4df9d..cd7147069 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -1,6 +1,14 @@ pragma ComponentBehavior: Bound +import "bluetooth" +import "network" +import "audio" +import "appearance" +import "taskbar" +import "launcher" +import "dashboard" import qs.components +import qs.services import qs.config import qs.modules.controlcenter import Quickshell.Widgets diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index cd9474050..2d22425ee 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -3,10 +3,18 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "./sections" +import "../../launcher/services" import qs.components import qs.components.controls +import qs.components.effects import qs.components.containers +import qs.components.images +import qs.services import qs.config +import qs.utils +import Caelestia.Models +import Quickshell +import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index f6f9b7a47..0cba5cecd 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -1,8 +1,11 @@ pragma ComponentBehavior: Bound +import ".." import "../../components" import qs.components import qs.components.controls +import qs.components.containers +import qs.services import qs.config import QtQuick import QtQuick.Layouts @@ -22,7 +25,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Animation duration scale") - value: root.rootPane.animDurationsScale + value: rootPane.animDurationsScale from: 0.1 to: 5.0 decimals: 1 @@ -33,8 +36,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.animDurationsScale = newValue; - root.rootPane.saveConfig(); + rootPane.animDurationsScale = newValue; + rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 0488daac7..7f528e996 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -1,8 +1,11 @@ pragma ComponentBehavior: Bound +import ".." import "../../components" import qs.components import qs.components.controls +import qs.components.containers +import qs.services import qs.config import QtQuick import QtQuick.Layouts @@ -17,19 +20,19 @@ CollapsibleSection { SwitchRow { label: qsTr("Background enabled") - checked: root.rootPane.backgroundEnabled + checked: rootPane.backgroundEnabled onToggled: checked => { - root.rootPane.backgroundEnabled = checked; - root.rootPane.saveConfig(); + rootPane.backgroundEnabled = checked; + rootPane.saveConfig(); } } SwitchRow { label: qsTr("Wallpaper enabled") - checked: root.rootPane.wallpaperEnabled + checked: rootPane.wallpaperEnabled onToggled: checked => { - root.rootPane.wallpaperEnabled = checked; - root.rootPane.saveConfig(); + rootPane.wallpaperEnabled = checked; + rootPane.saveConfig(); } } @@ -42,23 +45,23 @@ CollapsibleSection { SwitchRow { label: qsTr("Desktop Clock enabled") - checked: root.rootPane.desktopClockEnabled + checked: rootPane.desktopClockEnabled onToggled: checked => { - root.rootPane.desktopClockEnabled = checked; - root.rootPane.saveConfig(); + rootPane.desktopClockEnabled = checked; + rootPane.saveConfig(); } } SectionContainer { id: posContainer - readonly property var pos: (root.rootPane.desktopClockPosition || "top-left").split('-') + readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') readonly property string currentV: pos[0] readonly property string currentH: pos[1] function updateClockPos(v, h) { - root.rootPane.desktopClockPosition = v + "-" + h; - root.rootPane.saveConfig(); + rootPane.desktopClockPosition = v + "-" + h; + rootPane.saveConfig(); } contentSpacing: Appearance.spacing.small @@ -72,7 +75,7 @@ CollapsibleSection { SplitButtonRow { label: qsTr("Vertical Position") - enabled: root.rootPane.desktopClockEnabled + enabled: rootPane.desktopClockEnabled menuItems: [ MenuItem { @@ -108,7 +111,7 @@ CollapsibleSection { SplitButtonRow { label: qsTr("Horizontal Position") - enabled: root.rootPane.desktopClockEnabled + enabled: rootPane.desktopClockEnabled expandedZ: 99 menuItems: [ @@ -145,10 +148,10 @@ CollapsibleSection { SwitchRow { label: qsTr("Invert colors") - checked: root.rootPane.desktopClockInvertColors + checked: rootPane.desktopClockInvertColors onToggled: checked => { - root.rootPane.desktopClockInvertColors = checked; - root.rootPane.saveConfig(); + rootPane.desktopClockInvertColors = checked; + rootPane.saveConfig(); } } @@ -163,10 +166,10 @@ CollapsibleSection { SwitchRow { label: qsTr("Enabled") - checked: root.rootPane.desktopClockShadowEnabled + checked: rootPane.desktopClockShadowEnabled onToggled: checked => { - root.rootPane.desktopClockShadowEnabled = checked; - root.rootPane.saveConfig(); + rootPane.desktopClockShadowEnabled = checked; + rootPane.saveConfig(); } } @@ -177,7 +180,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Opacity") - value: root.rootPane.desktopClockShadowOpacity * 100 + value: rootPane.desktopClockShadowOpacity * 100 from: 0 to: 100 suffix: "%" @@ -189,8 +192,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - root.rootPane.desktopClockShadowOpacity = newValue / 100; - root.rootPane.saveConfig(); + rootPane.desktopClockShadowOpacity = newValue / 100; + rootPane.saveConfig(); } } } @@ -202,7 +205,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Blur") - value: root.rootPane.desktopClockShadowBlur * 100 + value: rootPane.desktopClockShadowBlur * 100 from: 0 to: 100 suffix: "%" @@ -214,8 +217,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - root.rootPane.desktopClockShadowBlur = newValue / 100; - root.rootPane.saveConfig(); + rootPane.desktopClockShadowBlur = newValue / 100; + rootPane.saveConfig(); } } } @@ -232,19 +235,19 @@ CollapsibleSection { SwitchRow { label: qsTr("Enabled") - checked: root.rootPane.desktopClockBackgroundEnabled + checked: rootPane.desktopClockBackgroundEnabled onToggled: checked => { - root.rootPane.desktopClockBackgroundEnabled = checked; - root.rootPane.saveConfig(); + rootPane.desktopClockBackgroundEnabled = checked; + rootPane.saveConfig(); } } SwitchRow { label: qsTr("Blur enabled") - checked: root.rootPane.desktopClockBackgroundBlur + checked: rootPane.desktopClockBackgroundBlur onToggled: checked => { - root.rootPane.desktopClockBackgroundBlur = checked; - root.rootPane.saveConfig(); + rootPane.desktopClockBackgroundBlur = checked; + rootPane.saveConfig(); } } @@ -255,7 +258,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Opacity") - value: root.rootPane.desktopClockBackgroundOpacity * 100 + value: rootPane.desktopClockBackgroundOpacity * 100 from: 0 to: 100 suffix: "%" @@ -267,8 +270,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - root.rootPane.desktopClockBackgroundOpacity = newValue / 100; - root.rootPane.saveConfig(); + rootPane.desktopClockBackgroundOpacity = newValue / 100; + rootPane.saveConfig(); } } } @@ -283,19 +286,19 @@ CollapsibleSection { SwitchRow { label: qsTr("Visualiser enabled") - checked: root.rootPane.visualiserEnabled + checked: rootPane.visualiserEnabled onToggled: checked => { - root.rootPane.visualiserEnabled = checked; - root.rootPane.saveConfig(); + rootPane.visualiserEnabled = checked; + rootPane.saveConfig(); } } SwitchRow { label: qsTr("Visualiser auto hide") - checked: root.rootPane.visualiserAutoHide + checked: rootPane.visualiserAutoHide onToggled: checked => { - root.rootPane.visualiserAutoHide = checked; - root.rootPane.saveConfig(); + rootPane.visualiserAutoHide = checked; + rootPane.saveConfig(); } } @@ -306,7 +309,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Visualiser rounding") - value: root.rootPane.visualiserRounding + value: rootPane.visualiserRounding from: 0 to: 10 stepSize: 1 @@ -318,8 +321,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - root.rootPane.visualiserRounding = Math.round(newValue); - root.rootPane.saveConfig(); + rootPane.visualiserRounding = Math.round(newValue); + rootPane.saveConfig(); } } } @@ -331,7 +334,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Visualiser spacing") - value: root.rootPane.visualiserSpacing + value: rootPane.visualiserSpacing from: 0 to: 2 validator: DoubleValidator { @@ -340,8 +343,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.visualiserSpacing = newValue; - root.rootPane.saveConfig(); + rootPane.visualiserSpacing = newValue; + rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index 167c3f144..a259f934e 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -1,8 +1,11 @@ pragma ComponentBehavior: Bound +import ".." import "../../components" import qs.components import qs.components.controls +import qs.components.containers +import qs.services import qs.config import QtQuick import QtQuick.Layouts @@ -22,7 +25,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Border rounding") - value: root.rootPane.borderRounding + value: rootPane.borderRounding from: 0.1 to: 100 decimals: 1 @@ -33,8 +36,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.borderRounding = newValue; - root.rootPane.saveConfig(); + rootPane.borderRounding = newValue; + rootPane.saveConfig(); } } } @@ -46,7 +49,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Border thickness") - value: root.rootPane.borderThickness + value: rootPane.borderThickness from: 0 to: 100 decimals: 1 @@ -57,8 +60,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.borderThickness = newValue; - root.rootPane.saveConfig(); + rootPane.borderThickness = newValue; + rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 98cbe9061..4b4559ae6 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -1,8 +1,10 @@ pragma ComponentBehavior: Bound +import ".." import "../../../launcher/services" import qs.components import qs.components.controls +import qs.components.containers import qs.services import qs.config import Quickshell @@ -22,8 +24,6 @@ CollapsibleSection { model: Schemes.list delegate: StyledRect { - id: schemeDelegate - required property var modelData Layout.fillWidth: true @@ -38,8 +38,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - const name = schemeDelegate.modelData.name; - const flavour = schemeDelegate.modelData.flavour; + const name = modelData.name; + const flavour = modelData.flavour; const schemeKey = `${name} ${flavour}`; Schemes.currentScheme = schemeKey; @@ -74,9 +74,9 @@ CollapsibleSection { Layout.alignment: Qt.AlignVCenter border.width: 1 - border.color: Qt.alpha(`#${schemeDelegate.modelData.colours?.outline}`, 0.5) + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - color: `#${schemeDelegate.modelData.colours?.surface}` + color: `#${modelData.colours?.surface}` radius: Appearance.rounding.full implicitWidth: iconPlaceholder.implicitWidth implicitHeight: iconPlaceholder.implicitWidth @@ -103,7 +103,7 @@ CollapsibleSection { anchors.right: parent.right implicitWidth: preview.implicitWidth - color: `#${schemeDelegate.modelData.colours?.primary}` + color: `#${modelData.colours?.primary}` radius: Appearance.rounding.full } } @@ -114,12 +114,12 @@ CollapsibleSection { spacing: 0 StyledText { - text: schemeDelegate.modelData.flavour ?? "" + text: modelData.flavour ?? "" font.pointSize: Appearance.font.size.normal } StyledText { - text: schemeDelegate.modelData.name ?? "" + text: modelData.name ?? "" font.pointSize: Appearance.font.size.small color: Colours.palette.m3outline @@ -131,7 +131,7 @@ CollapsibleSection { Loader { asynchronous: true - active: schemeDelegate.isCurrent + active: isCurrent sourceComponent: MaterialIcon { text: "check" diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index afbf9b29f..3de9e4b3c 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -1,8 +1,10 @@ pragma ComponentBehavior: Bound +import ".." import "../../../launcher/services" import qs.components import qs.components.controls +import qs.components.containers import qs.services import qs.config import Quickshell @@ -22,8 +24,6 @@ CollapsibleSection { model: M3Variants.list delegate: StyledRect { - id: variantDelegate - required property var modelData Layout.fillWidth: true @@ -35,7 +35,7 @@ CollapsibleSection { StateLayer { function onClicked(): void { - const variant = variantDelegate.modelData.variant; + const variant = modelData.variant; Schemes.currentVariant = variant; Quickshell.execDetached(["caelestia", "scheme", "set", "-v", variant]); @@ -66,19 +66,19 @@ CollapsibleSection { spacing: Appearance.spacing.normal MaterialIcon { - text: variantDelegate.modelData.icon + text: modelData.icon font.pointSize: Appearance.font.size.large - fill: variantDelegate.modelData.variant === Schemes.currentVariant ? 1 : 0 + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 } StyledText { Layout.fillWidth: true - text: variantDelegate.modelData.name - font.weight: variantDelegate.modelData.variant === Schemes.currentVariant ? 500 : 400 + text: modelData.name + font.weight: modelData.variant === Schemes.currentVariant ? 500 : 400 } MaterialIcon { - visible: variantDelegate.modelData.variant === Schemes.currentVariant + visible: modelData.variant === Schemes.currentVariant text: "check" color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index fb44f1a68..8c288608e 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import ".." import "../../components" import qs.components import qs.components.controls @@ -45,11 +46,9 @@ CollapsibleSection { } delegate: StyledRect { - id: sansDelegate - required property string modelData required property int index - readonly property bool isCurrent: modelData === root.rootPane.fontFamilySans + readonly property bool isCurrent: modelData === rootPane.fontFamilySans width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -59,8 +58,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - root.rootPane.fontFamilySans = sansDelegate.modelData; - root.rootPane.saveConfig(); + rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); } } @@ -75,7 +74,7 @@ CollapsibleSection { spacing: Appearance.spacing.normal StyledText { - text: sansDelegate.modelData + text: modelData font.pointSize: Appearance.font.size.normal } @@ -85,7 +84,7 @@ CollapsibleSection { Loader { asynchronous: true - active: sansDelegate.isCurrent + active: isCurrent sourceComponent: MaterialIcon { text: "check" @@ -129,11 +128,9 @@ CollapsibleSection { } delegate: StyledRect { - id: monoDelegate - required property string modelData required property int index - readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMono + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -143,8 +140,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - root.rootPane.fontFamilyMono = monoDelegate.modelData; - root.rootPane.saveConfig(); + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); } } @@ -159,7 +156,7 @@ CollapsibleSection { spacing: Appearance.spacing.normal StyledText { - text: monoDelegate.modelData + text: modelData font.pointSize: Appearance.font.size.normal } @@ -169,7 +166,7 @@ CollapsibleSection { Loader { asynchronous: true - active: monoDelegate.isCurrent + active: isCurrent sourceComponent: MaterialIcon { text: "check" @@ -215,11 +212,9 @@ CollapsibleSection { } delegate: StyledRect { - id: materialDelegate - required property string modelData required property int index - readonly property bool isCurrent: modelData === root.rootPane.fontFamilyMaterial + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) @@ -229,8 +224,8 @@ CollapsibleSection { StateLayer { function onClicked(): void { - root.rootPane.fontFamilyMaterial = materialDelegate.modelData; - root.rootPane.saveConfig(); + rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); } } @@ -245,7 +240,7 @@ CollapsibleSection { spacing: Appearance.spacing.normal StyledText { - text: materialDelegate.modelData + text: modelData font.pointSize: Appearance.font.size.normal } @@ -255,7 +250,7 @@ CollapsibleSection { Loader { asynchronous: true - active: materialDelegate.isCurrent + active: isCurrent sourceComponent: MaterialIcon { text: "check" @@ -278,7 +273,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Font size scale") - value: root.rootPane.fontSizeScale + value: rootPane.fontSizeScale from: 0.7 to: 1.5 decimals: 2 @@ -289,8 +284,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.fontSizeScale = newValue; - root.rootPane.saveConfig(); + rootPane.fontSizeScale = newValue; + rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index b6f6a409b..b0e6e38b8 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -1,8 +1,11 @@ pragma ComponentBehavior: Bound +import ".." import "../../components" import qs.components import qs.components.controls +import qs.components.containers +import qs.services import qs.config import QtQuick import QtQuick.Layouts @@ -22,7 +25,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Padding scale") - value: root.rootPane.paddingScale + value: rootPane.paddingScale from: 0.5 to: 2.0 decimals: 1 @@ -33,8 +36,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.paddingScale = newValue; - root.rootPane.saveConfig(); + rootPane.paddingScale = newValue; + rootPane.saveConfig(); } } } @@ -46,7 +49,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Rounding scale") - value: root.rootPane.roundingScale + value: rootPane.roundingScale from: 0.1 to: 5.0 decimals: 1 @@ -57,8 +60,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.roundingScale = newValue; - root.rootPane.saveConfig(); + rootPane.roundingScale = newValue; + rootPane.saveConfig(); } } } @@ -70,7 +73,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Spacing scale") - value: root.rootPane.spacingScale + value: rootPane.spacingScale from: 0.1 to: 2.0 decimals: 1 @@ -81,8 +84,8 @@ CollapsibleSection { } onValueModified: newValue => { - root.rootPane.spacingScale = newValue; - root.rootPane.saveConfig(); + rootPane.spacingScale = newValue; + rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index 67084a7f9..04eed9113 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -1,7 +1,11 @@ pragma ComponentBehavior: Bound +import ".." +import qs.components import qs.components.controls +import qs.components.containers import qs.services +import qs.config import QtQuick CollapsibleSection { diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 7a29ec4bd..9a48629c1 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -1,8 +1,11 @@ pragma ComponentBehavior: Bound +import ".." import "../../components" import qs.components import qs.components.controls +import qs.components.containers +import qs.services import qs.config import QtQuick import QtQuick.Layouts @@ -17,10 +20,10 @@ CollapsibleSection { SwitchRow { label: qsTr("Transparency enabled") - checked: root.rootPane.transparencyEnabled + checked: rootPane.transparencyEnabled onToggled: checked => { - root.rootPane.transparencyEnabled = checked; - root.rootPane.saveConfig(); + rootPane.transparencyEnabled = checked; + rootPane.saveConfig(); } } @@ -31,7 +34,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Transparency base") - value: root.rootPane.transparencyBase * 100 + value: rootPane.transparencyBase * 100 from: 0 to: 100 suffix: "%" @@ -43,8 +46,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - root.rootPane.transparencyBase = newValue / 100; - root.rootPane.saveConfig(); + rootPane.transparencyBase = newValue / 100; + rootPane.saveConfig(); } } } @@ -56,7 +59,7 @@ CollapsibleSection { Layout.fillWidth: true label: qsTr("Transparency layers") - value: root.rootPane.transparencyLayers * 100 + value: rootPane.transparencyLayers * 100 from: 0 to: 100 suffix: "%" @@ -68,8 +71,8 @@ CollapsibleSection { parseValueFunction: text => parseInt(text) onValueModified: newValue => { - root.rootPane.transparencyLayers = newValue / 100; - root.rootPane.saveConfig(); + rootPane.transparencyLayers = newValue / 100; + rootPane.saveConfig(); } } } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 28f82fc99..159c862e2 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -4,9 +4,11 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.effects import qs.components.containers import qs.services import qs.config +import Quickshell.Widgets import QtQuick import QtQuick.Layouts @@ -86,18 +88,16 @@ Item { model: Audio.sinks delegate: StyledRect { - id: outputDeviceDelegate - required property var modelData Layout.fillWidth: true - color: Audio.sink?.id === outputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { function onClicked(): void { - Audio.setAudioSink(outputDeviceDelegate.modelData); + Audio.setAudioSink(modelData); } } @@ -112,9 +112,9 @@ Item { spacing: Appearance.spacing.normal MaterialIcon { - text: Audio.sink?.id === outputDeviceDelegate.modelData.id ? "speaker" : "speaker_group" + text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" font.pointSize: Appearance.font.size.large - fill: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 1 : 0 + fill: Audio.sink?.id === modelData.id ? 1 : 0 } StyledText { @@ -122,8 +122,8 @@ Item { elide: Text.ElideRight maximumLineCount: 1 - text: outputDeviceDelegate.modelData.description || qsTr("Unknown") - font.weight: Audio.sink?.id === outputDeviceDelegate.modelData.id ? 500 : 400 + text: modelData.description || qsTr("Unknown") + font.weight: Audio.sink?.id === modelData.id ? 500 : 400 } } @@ -166,18 +166,16 @@ Item { model: Audio.sources delegate: StyledRect { - id: inputDeviceDelegate - required property var modelData Layout.fillWidth: true - color: Audio.source?.id === inputDeviceDelegate.modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal StateLayer { function onClicked(): void { - Audio.setAudioSource(inputDeviceDelegate.modelData); + Audio.setAudioSource(modelData); } } @@ -194,7 +192,7 @@ Item { MaterialIcon { text: "mic" font.pointSize: Appearance.font.size.large - fill: Audio.source?.id === inputDeviceDelegate.modelData.id ? 1 : 0 + fill: Audio.source?.id === modelData.id ? 1 : 0 } StyledText { @@ -202,8 +200,8 @@ Item { elide: Text.ElideRight maximumLineCount: 1 - text: inputDeviceDelegate.modelData.description || qsTr("Unknown") - font.weight: Audio.source?.id === inputDeviceDelegate.modelData.id ? 500 : 400 + text: modelData.description || qsTr("Unknown") + font.weight: Audio.source?.id === modelData.id ? 500 : 400 } } @@ -493,8 +491,6 @@ Item { Layout.fillWidth: true delegate: ColumnLayout { - id: streamDelegate - required property var modelData required property int index @@ -515,7 +511,7 @@ Item { Layout.fillWidth: true elide: Text.ElideRight maximumLineCount: 1 - text: Audio.getStreamName(streamDelegate.modelData) + text: Audio.getStreamName(modelData) font.pointSize: Appearance.font.size.normal font.weight: 500 } @@ -528,27 +524,27 @@ Item { bottom: 0 top: 100 } - enabled: !Audio.getStreamMuted(streamDelegate.modelData) + enabled: !Audio.getStreamMuted(modelData) Component.onCompleted: { - text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString(); + text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } Connections { function onAudioChanged() { - if (!streamVolumeInput.hasFocus && streamDelegate.modelData?.audio) { - streamVolumeInput.text = Math.round(streamDelegate.modelData.audio.volume * 100).toString(); + if (!streamVolumeInput.hasFocus && modelData?.audio) { + streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); } } - target: streamDelegate.modelData + target: modelData } onTextEdited: text => { if (hasFocus) { const val = parseInt(text); if (!isNaN(val) && val >= 0 && val <= 100) { - Audio.setStreamVolume(streamDelegate.modelData, val / 100); + Audio.setStreamVolume(modelData, val / 100); } } } @@ -556,7 +552,7 @@ Item { onEditingFinished: { const val = parseInt(text); if (isNaN(val) || val < 0 || val > 100) { - text = Math.round(Audio.getStreamVolume(streamDelegate.modelData) * 100).toString(); + text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } } } @@ -565,7 +561,7 @@ Item { text: "%" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.normal - opacity: Audio.getStreamMuted(streamDelegate.modelData) ? 0.5 : 1 + opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 } StyledRect { @@ -573,11 +569,11 @@ Item { implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { function onClicked(): void { - Audio.setStreamMuted(streamDelegate.modelData, !Audio.getStreamMuted(streamDelegate.modelData)); + Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); } } @@ -585,23 +581,21 @@ Item { id: streamMuteIcon anchors.centerIn: parent - text: Audio.getStreamMuted(streamDelegate.modelData) ? "volume_off" : "volume_up" - color: Audio.getStreamMuted(streamDelegate.modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + text: Audio.getStreamMuted(modelData) ? "volume_off" : "volume_up" + color: Audio.getStreamMuted(modelData) ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer } } } StyledSlider { - id: streamSlider - Layout.fillWidth: true implicitHeight: Appearance.padding.normal * 3 - value: Audio.getStreamVolume(streamDelegate.modelData) - enabled: !Audio.getStreamMuted(streamDelegate.modelData) + value: Audio.getStreamVolume(modelData) + enabled: !Audio.getStreamMuted(modelData) opacity: enabled ? 1 : 0.5 onMoved: { - Audio.setStreamVolume(streamDelegate.modelData, value); + Audio.setStreamVolume(modelData, value); if (!streamVolumeInput.hasFocus) { streamVolumeInput.text = Math.round(value * 100).toString(); } @@ -609,12 +603,12 @@ Item { Connections { function onAudioChanged() { - if (streamDelegate.modelData?.audio) { - streamSlider.value = streamDelegate.modelData.audio.volume; + if (modelData?.audio) { + value = modelData.audio.volume; } } - target: streamDelegate.modelData + target: modelData } } } diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 84913b7ad..1b9a7fcfa 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -3,8 +3,12 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import qs.components import qs.components.controls import qs.components.containers +import qs.config +import Quickshell.Widgets +import Quickshell.Bluetooth import QtQuick SplitPaneWithDetails { diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index be1b0258e..a943f2806 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -4,6 +4,7 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.containers import qs.services import qs.config import qs.utils diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index c4398ebc2..4115003f2 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -1,7 +1,7 @@ -pragma ComponentBehavior: Bound - +import ".." import qs.components import qs.components.controls +import qs.components.effects import qs.services import qs.config import QtQuick @@ -68,10 +68,10 @@ StyledRect { // Create binding in Component.onCompleted Component.onCompleted: { - if (button.modelData.state !== undefined && button.modelData.state) { - _checked = button.modelData.state; - } else if (root.rootItem && button.modelData.propertyName) { - const propName = button.modelData.propertyName; + if (modelData.state !== undefined && modelData.state) { + _checked = modelData.state; + } else if (root.rootItem && modelData.propertyName) { + const propName = modelData.propertyName; const rootItem = root.rootItem; _checked = Qt.binding(function () { return rootItem[propName] ?? false; @@ -90,9 +90,9 @@ StyledRect { Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) onClicked: { - if (button.modelData.onToggled && root.rootItem && button.modelData.propertyName) { - const currentValue = root.rootItem[button.modelData.propertyName] ?? false; - button.modelData.onToggled(!currentValue); + if (modelData.onToggled && root.rootItem && modelData.propertyName) { + const currentValue = root.rootItem[modelData.propertyName] ?? false; + modelData.onToggled(!currentValue); } } diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index 072a20a31..8e5cdb2ce 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -1,6 +1,10 @@ pragma ComponentBehavior: Bound import ".." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index fe09afdef..7c5292da1 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -2,9 +2,11 @@ pragma ComponentBehavior: Bound import ".." import qs.components +import qs.components.controls import qs.components.containers import qs.services import qs.config +import Quickshell import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index 45774749f..169d63653 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -1,4 +1,7 @@ +import ".." +import "../components" import qs.components +import qs.components.controls import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 95eb6de1f..1aed5cbc3 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.components.controls +import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index d656c38b6..5c1a8be68 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import qs.components import qs.components.effects import qs.config import Quickshell.Widgets diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index 4636e28e8..79b23abc0 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -1,6 +1,13 @@ pragma ComponentBehavior: Bound +import ".." +import qs.components +import qs.components.effects +import qs.components.containers +import qs.config +import Quickshell.Widgets import QtQuick +import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 851140045..588d51d1f 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -3,9 +3,11 @@ pragma ComponentBehavior: Bound import ".." import qs.components import qs.components.controls +import qs.components.effects import qs.components.images import qs.services import qs.config +import Caelestia.Models import QtQuick GridView { @@ -28,8 +30,6 @@ GridView { } delegate: Item { - id: wpDelegate - required property var modelData required property int index readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent @@ -41,27 +41,27 @@ GridView { StateLayer { function onClicked(): void { - Wallpapers.setWallpaper(wpDelegate.modelData.path); + Wallpapers.setWallpaper(modelData.path); } anchors.fill: parent - anchors.leftMargin: wpDelegate.itemMargin - anchors.rightMargin: wpDelegate.itemMargin - anchors.topMargin: wpDelegate.itemMargin - anchors.bottomMargin: wpDelegate.itemMargin - radius: wpDelegate.itemRadius + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + radius: itemRadius } StyledClippingRect { id: image anchors.fill: parent - anchors.leftMargin: wpDelegate.itemMargin - anchors.rightMargin: wpDelegate.itemMargin - anchors.topMargin: wpDelegate.itemMargin - anchors.bottomMargin: wpDelegate.itemMargin + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin color: Colours.tPalette.m3surfaceContainer - radius: wpDelegate.itemRadius + radius: itemRadius antialiasing: true layer.enabled: true layer.smooth: true @@ -69,7 +69,7 @@ GridView { CachingImage { id: cachingImage - path: wpDelegate.modelData.path + path: modelData.path anchors.fill: parent fillMode: Image.PreserveAspectCrop cache: true @@ -93,7 +93,7 @@ GridView { id: fallbackImage anchors.fill: parent - source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? wpDelegate.modelData.path : "" + source: fallbackTimer.triggered && cachingImage.status !== Image.Ready ? modelData.path : "" asynchronous: true fillMode: Image.PreserveAspectCrop cache: true @@ -169,13 +169,13 @@ GridView { Rectangle { anchors.fill: parent - anchors.leftMargin: wpDelegate.itemMargin - anchors.rightMargin: wpDelegate.itemMargin - anchors.topMargin: wpDelegate.itemMargin - anchors.bottomMargin: wpDelegate.itemMargin + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin color: "transparent" - radius: wpDelegate.itemRadius + border.width - border.width: wpDelegate.isCurrent ? 2 : 0 + radius: itemRadius + border.width + border.width: isCurrent ? 2 : 0 border.color: Colours.palette.m3primary antialiasing: true smooth: true @@ -192,7 +192,7 @@ GridView { anchors.top: parent.top anchors.margins: Appearance.padding.small - visible: wpDelegate.isCurrent + visible: isCurrent text: "check_circle" color: Colours.palette.m3primary font.pointSize: Appearance.font.size.large @@ -209,10 +209,10 @@ GridView { anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 anchors.bottomMargin: Appearance.padding.normal - text: wpDelegate.modelData.name + text: modelData.name font.pointSize: Appearance.font.size.smaller font.weight: 500 - color: wpDelegate.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface + color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface elide: Text.ElideMiddle maximumLineCount: 1 horizontalAlignment: Text.AlignHCenter diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 285ad55f6..d7186a790 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -1,11 +1,15 @@ pragma ComponentBehavior: Bound import ".." +import "../components" import qs.components import qs.components.controls import qs.components.effects import qs.components.containers +import qs.services import qs.config +import qs.utils +import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 3db044ee4..95e7531ed 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -1,6 +1,8 @@ +import ".." import "../components" import qs.components import qs.components.controls +import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index faaa05c21..ac84752b6 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -1,8 +1,10 @@ +import ".." import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower import qs.components +import qs.components.controls import qs.config import qs.services diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 7f82457ca..6de344691 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -2,8 +2,10 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import "../../launcher/services" import qs.components import qs.components.controls +import qs.components.effects import qs.components.containers import qs.services import qs.config @@ -102,7 +104,7 @@ Item { anchors.fill: parent onSelectedAppChanged: { - session.launcher.active = selectedApp; + root.session.launcher.active = root.selectedApp; updateToggleState(); } @@ -117,7 +119,7 @@ Item { Connections { function onActiveChanged() { root.selectedApp = root.session.launcher.active; - root.updateToggleState(); + updateToggleState(); } target: root.session.launcher @@ -133,7 +135,7 @@ Item { Connections { function onAppsChanged() { - root.updateFilteredApps(); + updateFilteredApps(); } target: allAppsDb @@ -299,12 +301,10 @@ Item { clip: true StyledScrollBar.vertical: StyledScrollBar { - flickable: appsListView + flickable: parent } delegate: StyledRect { - id: appDelegate - required property var modelData readonly property bool isSelected: root.selectedApp === modelData @@ -330,7 +330,7 @@ Item { StateLayer { function onClicked(): void { - root.session.launcher.active = appDelegate.modelData; + root.session.launcher.active = modelData; } } @@ -347,20 +347,20 @@ Item { Layout.alignment: Qt.AlignVCenter implicitSize: 32 source: { - const entry = appDelegate.modelData.entry; + const entry = modelData.entry; return entry ? Quickshell.iconPath(entry.icon, "image-missing") : "image-missing"; } } StyledText { Layout.fillWidth: true - text: appDelegate.modelData.name || appDelegate.modelData.entry?.name || qsTr("Unknown") + text: modelData.name || modelData.entry?.name || qsTr("Unknown") font.pointSize: Appearance.font.size.normal } Loader { - readonly property bool isHidden: appDelegate.modelData ? Strings.testRegexList(Config.launcher.hiddenApps, appDelegate.modelData.id) : false - readonly property bool isFav: appDelegate.modelData ? Strings.testRegexList(Config.launcher.favouriteApps, appDelegate.modelData.id) : false + readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false + readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false Layout.alignment: Qt.AlignVCenter asynchronous: true @@ -515,7 +515,7 @@ Item { ColumnLayout { id: appDetailsLayout - readonly property var displayedApp: parent?.displayedApp ?? null // qmllint disable missing-property + readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null anchors.fill: parent spacing: Appearance.spacing.normal @@ -524,7 +524,7 @@ Item { Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.topMargin: Appearance.padding.large * 2 - visible: appDetailsLayout.displayedApp === null + visible: displayedApp === null icon: "apps" title: qsTr("Launcher Applications") } @@ -534,7 +534,7 @@ Item { Layout.leftMargin: Appearance.padding.large * 2 Layout.rightMargin: Appearance.padding.large * 2 Layout.topMargin: Appearance.padding.large * 2 - visible: appDetailsLayout.displayedApp !== null + visible: displayedApp !== null implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight @@ -564,7 +564,7 @@ Item { id: appTitleText Layout.alignment: Qt.AlignHCenter - text: appDetailsLayout.displayedApp.displayedApp ? (appDetailsLayout.displayedApp.displayedApp.displayedApp.name || appDetailsLayout.displayedApp.displayedApp.displayedApp.entry?.name || qsTr("Application Details")) : "" + text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" font.pointSize: Appearance.font.size.large font.bold: true } diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index a2ed2b121..5eaf6e0e0 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -4,6 +4,8 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.effects +import qs.services import qs.config import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 9e702f979..4e60b3d48 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -4,6 +4,8 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.effects +import qs.components.containers import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index e9f241682..87e00015d 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -4,6 +4,7 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.containers import qs.services import qs.config import QtQuick @@ -70,7 +71,7 @@ DeviceList { id: stateLayer function onClicked(): void { - root.session.ethernet.active = ethernetItem.modelData; + root.session.ethernet.active = modelData; } } @@ -87,12 +88,12 @@ DeviceList { implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: ethernetItem.modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { anchors.fill: parent radius: parent.radius - color: Qt.alpha(ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) + color: Qt.alpha(modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface, stateLayer.pressed ? 0.1 : stateLayer.containsMouse ? 0.08 : 0) } MaterialIcon { @@ -101,8 +102,8 @@ DeviceList { anchors.centerIn: parent text: "cable" font.pointSize: Appearance.font.size.large - fill: ethernetItem.modelData.connected ? 1 : 0 - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface Behavior on fill { Anim {} @@ -117,7 +118,7 @@ DeviceList { StyledText { Layout.fillWidth: true - text: ethernetItem.modelData.interface || qsTr("Unknown") + text: modelData.interface || qsTr("Unknown") elide: Text.ElideRight } @@ -127,10 +128,10 @@ DeviceList { StyledText { Layout.fillWidth: true - text: ethernetItem.modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: ethernetItem.modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline font.pointSize: Appearance.font.size.small - font.weight: ethernetItem.modelData.connected ? 500 : 400 + font.weight: modelData.connected ? 500 : 400 elide: Text.ElideRight } } @@ -143,18 +144,18 @@ DeviceList { implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, ethernetItem.modelData.connected ? 1 : 0) + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { function onClicked(): void { - if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { - Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); + if (modelData.connected && modelData.connection) { + Nmcli.disconnectEthernet(modelData.connection, () => {}); } else { - Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); } } - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } MaterialIcon { @@ -162,8 +163,8 @@ DeviceList { anchors.centerIn: parent animate: true - text: ethernetItem.modelData.connected ? "link_off" : "link" - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + text: modelData.connected ? "link_off" : "link" + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } } diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 63cdb16f3..59d82bb08 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -2,7 +2,10 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import qs.components import qs.components.containers +import qs.config +import Quickshell.Widgets import QtQuick SplitPaneWithDetails { diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 50f8fd335..90bfcf46a 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -3,6 +3,8 @@ pragma ComponentBehavior: Bound import ".." import "../components" import qs.components +import qs.components.controls +import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 63e62058c..bda7cb18a 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -5,6 +5,7 @@ import "../components" import qs.components import qs.components.controls import qs.components.containers +import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 234c888fe..0a6b20e27 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -5,9 +5,13 @@ import "../components" import "." import qs.components import qs.components.controls +import qs.components.effects import qs.components.containers import qs.services import qs.config +import qs.utils +import Quickshell +import Quickshell.Widgets import QtQuick import QtQuick.Layouts diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 88c7b08c9..23e4010b4 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -5,8 +5,10 @@ import "../components" import qs.components import qs.components.controls import qs.components.effects +import qs.components.containers import qs.services import qs.config +import qs.utils import QtQuick import QtQuick.Controls import QtQuick.Layouts diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 2204479be..49d801d9a 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -4,10 +4,13 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.containers +import qs.components.effects import qs.services import qs.config import Quickshell import QtQuick +import QtQuick.Controls import QtQuick.Layouts ColumnLayout { diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 67d7f065c..beaaff3f0 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -2,8 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import "." import qs.components import qs.components.controls +import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils @@ -170,11 +173,11 @@ DeviceDetails { Connections { function onActiveChanged() { - root.updateDeviceDetails(); + updateDeviceDetails(); } function onWirelessDeviceDetailsChanged() { - if (root.network && root.network.ssid) { - const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid); + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { connectionUpdateTimer.stop(); } @@ -189,10 +192,10 @@ DeviceDetails { interval: 500 repeat: true - running: root.network && root.network.ssid + running: network && network.ssid onTriggered: { - if (root.network) { - const isActive = root.network.active || (Nmcli.active && Nmcli.active.ssid === root.network.ssid); + if (network) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); if (isActive) { if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { Nmcli.getWirelessDeviceDetails("", () => {}); diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 15fa293fa..1713022eb 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -2,8 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import "." import qs.components import qs.components.controls +import qs.components.containers +import qs.components.effects import qs.services import qs.config import qs.utils @@ -104,20 +107,18 @@ DeviceList { delegate: Component { StyledRect { - id: networkDelegate - required property var modelData width: ListView.view ? ListView.view.width : undefined - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === networkDelegate.modelData ? Colours.tPalette.m3surfaceContainer.a : 0) + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal StateLayer { function onClicked(): void { - root.session.network.active = networkDelegate.modelData; - if (networkDelegate.modelData && networkDelegate.modelData.ssid) { - root.checkSavedProfileForNetwork(networkDelegate.modelData.ssid); + root.session.network.active = modelData; + if (modelData && modelData.ssid) { + root.checkSavedProfileForNetwork(modelData.ssid); } } } @@ -137,16 +138,16 @@ DeviceList { implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 radius: Appearance.rounding.normal - color: networkDelegate.modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { id: icon anchors.centerIn: parent - text: Icons.getNetworkIcon(networkDelegate.modelData.strength, networkDelegate.modelData.isSecure) + text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) font.pointSize: Appearance.font.size.large - fill: networkDelegate.modelData.active ? 1 : 0 - color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + fill: modelData.active ? 1 : 0 + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } @@ -160,7 +161,7 @@ DeviceList { elide: Text.ElideRight maximumLineCount: 1 - text: networkDelegate.modelData.ssid || qsTr("Unknown") + text: modelData.ssid || qsTr("Unknown") } RowLayout { @@ -170,18 +171,18 @@ DeviceList { StyledText { Layout.fillWidth: true text: { - if (networkDelegate.modelData.active) + if (modelData.active) return qsTr("Connected"); - if (networkDelegate.modelData.isSecure && networkDelegate.modelData.security && networkDelegate.modelData.security.length > 0) { - return networkDelegate.modelData.security; + if (modelData.isSecure && modelData.security && modelData.security.length > 0) { + return modelData.security; } - if (networkDelegate.modelData.isSecure) + if (modelData.isSecure) return qsTr("Secured"); return qsTr("Open"); } - color: networkDelegate.modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline font.pointSize: Appearance.font.size.small - font.weight: networkDelegate.modelData.active ? 500 : 400 + font.weight: modelData.active ? 500 : 400 elide: Text.ElideRight } } @@ -192,14 +193,14 @@ DeviceList { implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, networkDelegate.modelData.active ? 1 : 0) + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) StateLayer { function onClicked(): void { - if (networkDelegate.modelData.active) { + if (modelData.active) { Nmcli.disconnectFromNetwork(); } else { - NetworkConnection.handleConnect(networkDelegate.modelData, root.session, null); + NetworkConnection.handleConnect(modelData, root.session, null); } } } @@ -208,8 +209,8 @@ DeviceList { id: connectIcon anchors.centerIn: parent - text: networkDelegate.modelData.active ? "link_off" : "link" - color: networkDelegate.modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + text: modelData.active ? "link_off" : "link" + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } } } diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index e928d0617..8150af9cf 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -2,7 +2,10 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import qs.components import qs.components.containers +import qs.config +import Quickshell.Widgets import QtQuick SplitPaneWithDetails { diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 9049ca0d5..6db01bc06 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -1,8 +1,11 @@ pragma ComponentBehavior: Bound import ".." +import "." import qs.components import qs.components.controls +import qs.components.effects +import qs.components.containers import qs.services import qs.config import qs.utils @@ -71,7 +74,9 @@ Item { enabled: session.network.showPasswordDialog && !isClosing focus: enabled - Keys.onEscapePressed: closeDialog() + Keys.onEscapePressed: { + closeDialog(); + } Rectangle { anchors.fill: parent @@ -84,7 +89,7 @@ Item { MouseArea { anchors.fill: parent - onClicked: root.closeDialog() + onClicked: closeDialog() } } @@ -130,7 +135,7 @@ Item { } } - Keys.onEscapePressed: root.closeDialog() + Keys.onEscapePressed: closeDialog() ColumnLayout { id: content @@ -463,7 +468,7 @@ Item { triggeredOnStart: false onTriggered: { repeatCount++; - root.checkConnectionStatus(); + checkConnectionStatus(); } onRunningChanged: { @@ -484,7 +489,7 @@ Item { connectionMonitor.stop(); connectButton.connecting = false; connectButton.text = qsTr("Connect"); - root.closeDialog(); + closeDialog(); } } } @@ -493,7 +498,7 @@ Item { Connections { function onActiveChanged() { if (root.visible) { - root.checkConnectionStatus(); + checkConnectionStatus(); } } function onConnectionFailed(ssid: string) { diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index 1753dc125..b4eb391d4 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -4,6 +4,7 @@ import ".." import "../components" import qs.components import qs.components.controls +import qs.components.effects import qs.services import qs.config import QtQuick diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 4cae7dff6..7fcdec9e5 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -9,6 +9,7 @@ import qs.components.containers import qs.services import qs.config import qs.utils +import Quickshell import Quickshell.Widgets import QtQuick import QtQuick.Layouts From 2a54c089a4bcf68bb3409ca8c421e5fb12a72e3a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:31:52 +1100 Subject: [PATCH 136/409] chore: fix rest of linter warnings --- components/containers/StyledWindow.qml | 2 ++ components/controls/SpinBoxRow.qml | 2 +- components/controls/SwitchRow.qml | 2 +- components/misc/CustomShortcut.qml | 2 ++ config/Appearance.qml | 12 +++++------ config/Config.qml | 8 ++++---- modules/Shortcuts.qml | 16 +++++++++++++++ modules/areapicker/AreaPicker.qml | 8 ++++++++ modules/bar/BarWrapper.qml | 2 +- modules/bar/components/StatusIcons.qml | 8 ++++---- .../bar/components/workspaces/OccupiedBg.qml | 4 ++-- modules/bar/popouts/ActiveWindow.qml | 2 +- modules/bar/popouts/Bluetooth.qml | 20 +++++++++---------- modules/bar/popouts/Network.qml | 2 +- modules/bar/popouts/kblayout/KbLayout.qml | 2 +- .../bar/popouts/kblayout/KbLayoutModel.qml | 4 +++- modules/dashboard/Performance.qml | 4 ++-- modules/dashboard/dash/Media.qml | 2 +- modules/drawers/Backgrounds.qml | 16 +++++++-------- modules/drawers/Drawers.qml | 2 +- modules/lock/Lock.qml | 4 ++-- modules/lock/Media.qml | 2 +- modules/lock/Pam.qml | 2 +- modules/notifications/Notification.qml | 2 +- modules/utilities/Wrapper.qml | 3 ++- modules/utilities/cards/Toggles.qml | 4 ++-- modules/windowinfo/Preview.qml | 2 +- services/Brightness.qml | 4 ++++ services/Hypr.qml | 2 ++ services/Nmcli.qml | 6 +++--- services/NotifData.qml | 4 ++++ services/Notifs.qml | 2 ++ services/Players.qml | 8 ++++++++ services/Recorder.qml | 4 ++-- services/VPN.qml | 6 ++++-- 35 files changed, 114 insertions(+), 61 deletions(-) diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml index 8c6e39fc8..0adca1fe6 100644 --- a/components/containers/StyledWindow.qml +++ b/components/containers/StyledWindow.qml @@ -1,7 +1,9 @@ import Quickshell import Quickshell.Wayland +// qmllint disable uncreatable-type PanelWindow { + // qmllint enable uncreatable-type required property string name WlrLayershell.namespace: `caelestia-${name}` diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index 2109bfa4f..c43cbc782 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -44,7 +44,7 @@ StyledRect { step: root.step value: root.value onValueModified: value => { - root.onValueModified(value); + root.onValueModified(value); // qmllint disable use-proper-function } } } diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 3c460e8b0..7e950cb01 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -40,7 +40,7 @@ StyledRect { checked: root.checked enabled: root.enabled onToggled: { - root.onToggled(checked); + root.onToggled(checked); // qmllint disable use-proper-function } } } diff --git a/components/misc/CustomShortcut.qml b/components/misc/CustomShortcut.qml index aa35ed8f5..9bbcf1f76 100644 --- a/components/misc/CustomShortcut.qml +++ b/components/misc/CustomShortcut.qml @@ -1,5 +1,7 @@ import Quickshell.Hyprland +// qmllint disable unresolved-type GlobalShortcut { + // qmllint enable unresolved-type appid: "caelestia" } diff --git a/config/Appearance.qml b/config/Appearance.qml index 241c21a78..489620a07 100644 --- a/config/Appearance.qml +++ b/config/Appearance.qml @@ -5,10 +5,10 @@ import Quickshell Singleton { // Literally just here to shorten accessing stuff :woe: // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx` - readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding - readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing - readonly property AppearanceConfig.Padding padding: Config.appearance.padding - readonly property AppearanceConfig.FontStuff font: Config.appearance.font - readonly property AppearanceConfig.Anim anim: Config.appearance.anim - readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency + readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding // qmllint disable missing-property + readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing // qmllint disable missing-property + readonly property AppearanceConfig.Padding padding: Config.appearance.padding // qmllint disable missing-property + readonly property AppearanceConfig.FontStuff font: Config.appearance.font // qmllint disable missing-property + readonly property AppearanceConfig.Anim anim: Config.appearance.anim // qmllint disable missing-property + readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency // qmllint disable missing-property } diff --git a/config/Config.qml b/config/Config.qml index a8eb0a4b7..7ec9bbe06 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -436,9 +436,9 @@ Singleton { JSON.parse(text()); const elapsed = timer.elapsedMs(); // Only show toast for external changes (not our own saves) and when elapsed time is meaningful - if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) { - Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); - } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) { + if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) { // qmllint disable unresolved-type + Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); // qmllint disable unresolved-type + } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) { // qmllint disable unresolved-type Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); } } catch (e) { @@ -451,7 +451,7 @@ Singleton { } onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) - JsonAdapter { + JsonAdapter { // qmllint disable unresolved-type id: adapter property AppearanceConfig appearance: AppearanceConfig {} diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index adf660ff1..64509dacb 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -11,13 +11,17 @@ Scope { property bool launcherInterrupted readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "controlCenter" description: "Open control center" onPressed: WindowFactory.create() } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "showall" description: "Toggle launcher, dashboard and osd" onPressed: { @@ -28,7 +32,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "dashboard" description: "Toggle dashboard" onPressed: { @@ -39,7 +45,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "session" description: "Toggle session menu" onPressed: { @@ -50,7 +58,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "launcher" description: "Toggle launcher" onPressed: root.launcherInterrupted = false @@ -63,13 +73,17 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "launcherInterrupt" description: "Interrupt launcher keybind" onPressed: root.launcherInterrupted = true } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "sidebar" description: "Toggle sidebar" onPressed: { @@ -80,7 +94,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "utilities" description: "Toggle utilities" onPressed: { diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml index fc0eaab08..f9b046bee 100644 --- a/modules/areapicker/AreaPicker.qml +++ b/modules/areapicker/AreaPicker.qml @@ -79,7 +79,9 @@ Scope { target: "picker" } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshot" description: "Open screenshot tool" onPressed: { @@ -90,7 +92,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshotFreeze" description: "Open screenshot tool (freeze mode)" onPressed: { @@ -101,7 +105,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshotClip" description: "Open screenshot tool (clipboard)" onPressed: { @@ -112,7 +118,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshotFreezeClip" description: "Open screenshot tool (freeze mode, clipboard)" onPressed: { diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 3a6bdfee7..8d5f9a738 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -82,7 +82,7 @@ Item { width: root.contentWidth screen: root.screen visibilities: root.visibilities - popouts: root.popouts + popouts: root.popouts // qmllint disable incompatible-type } } } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index b1cc133fb..b968d6877 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -178,9 +178,9 @@ StyledRect { MaterialIcon { animate: true text: { - if (!Bluetooth.defaultAdapter?.enabled) + if (!Bluetooth.defaultAdapter?.enabled) // qmllint disable unresolved-type return "bluetooth_disabled"; - if (Bluetooth.devices.values.some(d => d.connected)) + if (Bluetooth.devices.values.some(d => d.connected)) // qmllint disable unresolved-type return "bluetooth_connected"; return "bluetooth"; } @@ -190,7 +190,7 @@ StyledRect { // Connected bluetooth devices Repeater { model: ScriptModel { - values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) + values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) // qmllint disable unresolved-type } MaterialIcon { @@ -204,7 +204,7 @@ StyledRect { fill: 1 SequentialAnimation on opacity { - running: device.modelData?.state !== BluetoothDeviceState.Connected + running: device.modelData?.state !== BluetoothDeviceState.Connected // qmllint disable unresolved-type alwaysRunToEnd: true loops: Animation.Infinite diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 56b215e67..ac4f36f38 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -52,8 +52,8 @@ Item { required property var modelData - readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null - readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null + readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null // qmllint disable incompatible-type + readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null // qmllint disable incompatible-type function getWsIdx(ws: int): int { let i = ws - 1; diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 9a1582a3f..0cb5f69bb 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -92,7 +92,7 @@ Item { ScreencopyView { id: preview - captureSource: Hypr.activeToplevel?.wayland ?? null + captureSource: Hypr.activeToplevel?.wayland ?? null // qmllint disable unresolved-type live: visible constraintSize.width: Config.bar.sizes.windowPreviewSize diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 3acace1c9..cd24dce3a 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -26,9 +26,9 @@ ColumnLayout { Toggle { label: qsTr("Enabled") - checked: Bluetooth.defaultAdapter?.enabled ?? false + checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; + const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type if (adapter) adapter.enabled = checked; } @@ -36,9 +36,9 @@ ColumnLayout { Toggle { label: qsTr("Discovering") - checked: Bluetooth.defaultAdapter?.discovering ?? false + checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; + const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type if (adapter) adapter.discovering = checked; } @@ -48,7 +48,7 @@ ColumnLayout { Layout.topMargin: Appearance.spacing.small Layout.rightMargin: Appearance.padding.small text: { - const devices = Bluetooth.devices.values; + const devices = Bluetooth.devices.values; // qmllint disable unresolved-type let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); const connected = devices.filter(d => d.connected).length; if (connected > 0) @@ -61,14 +61,14 @@ ColumnLayout { Repeater { model: ScriptModel { - values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type } RowLayout { id: device required property BluetoothDevice modelData - readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting + readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type Layout.fillWidth: true Layout.rightMargin: Appearance.padding.small @@ -108,7 +108,7 @@ ColumnLayout { implicitHeight: connectIcon.implicitHeight + Appearance.padding.small radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) + color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type CircularIndicator { anchors.fill: parent @@ -120,7 +120,7 @@ ColumnLayout { device.modelData.connected = !device.modelData.connected; } - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type disabled: device.loading } @@ -130,7 +130,7 @@ ColumnLayout { anchors.centerIn: parent animate: true text: device.modelData.connected ? "link_off" : "link" - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type opacity: device.loading ? 0 : 1 diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index fc2f1d408..4c61636fd 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -45,7 +45,7 @@ ColumnLayout { Layout.preferredHeight: visible ? implicitHeight : 0 Layout.topMargin: visible ? Appearance.spacing.small : 0 Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 networks available").arg(Nmcli.networks.length) + text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property color: Colours.palette.m3onSurfaceVariant font.pointSize: Appearance.font.size.small } diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 68a7a7947..3e5819027 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -129,7 +129,7 @@ ColumnLayout { Layout.rightMargin: Appearance.padding.small Layout.topMargin: Appearance.spacing.small - height: 1 + implicitHeight: 1 color: Colours.palette.m3onSurfaceVariant opacity: 0.35 } diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 36032e290..d5515f1aa 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -7,6 +7,8 @@ import Quickshell.Io import qs.config import Caelestia +// TODO: handle this better later + Item { id: model @@ -139,7 +141,7 @@ Item { stdout: StdioCollector { onStreamFinished: model._buildXmlMap(text) } - onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) + onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property _xkbXmlEvdev.running = true } diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 2551bc952..14108cad0 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -685,10 +685,10 @@ Item { property real smoothMax: targetMax anchors.fill: parent - line1: NetworkUsage.uploadBuffer + line1: NetworkUsage.uploadBuffer // qmllint disable missing-type line1Color: Colours.palette.m3secondary line1FillAlpha: 0.15 - line2: NetworkUsage.downloadBuffer + line2: NetworkUsage.downloadBuffer // qmllint disable missing-type line2Color: Colours.palette.m3tertiary line2FillAlpha: 0.2 maxValue: smoothMax diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index cb764dc0a..4451e5d7f 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -213,7 +213,7 @@ Item { anchors.margins: Appearance.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment + speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index c99243239..8810f50e2 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -22,14 +22,14 @@ Shape { preferredRendererType: Shape.CurveRenderer Osd.Background { - wrapper: root.panels.osd + wrapper: root.panels.osd // qmllint disable incompatible-type startX: root.width - root.panels.session.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Notifications.Background { - wrapper: root.panels.notifications + wrapper: root.panels.notifications // qmllint disable incompatible-type sidebar: sidebar startX: root.width @@ -37,28 +37,28 @@ Shape { } Session.Background { - wrapper: root.panels.session + wrapper: root.panels.session // qmllint disable incompatible-type startX: root.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding } Launcher.Background { - wrapper: root.panels.launcher + wrapper: root.panels.launcher // qmllint disable incompatible-type startX: (root.width - wrapper.width) / 2 - rounding startY: root.height } Dashboard.Background { - wrapper: root.panels.dashboard + wrapper: root.panels.dashboard // qmllint disable incompatible-type startX: (root.width - wrapper.width) / 2 - rounding startY: 0 } BarPopouts.Background { - wrapper: root.panels.popouts + wrapper: root.panels.popouts // qmllint disable incompatible-type invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height startX: wrapper.x @@ -66,7 +66,7 @@ Shape { } Utilities.Background { - wrapper: root.panels.utilities + wrapper: root.panels.utilities // qmllint disable incompatible-type sidebar: sidebar startX: root.width @@ -76,7 +76,7 @@ Shape { Sidebar.Background { id: sidebar - wrapper: root.panels.sidebar + wrapper: root.panels.sidebar // qmllint disable incompatible-type panels: root.panels startX: root.width diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 8d99935b8..73877feb3 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -64,7 +64,7 @@ Variants { height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 intersection: Intersection.Xor - regions: regions.instances + regions: regions.instances // qmllint disable stale-property-read } anchors.top: true diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index a4795016c..25d8a08c8 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -25,13 +25,13 @@ Scope { lock: lock } - CustomShortcut { + CustomShortcut { // qmllint disable unresolved-type name: "lock" description: "Lock the current session" onPressed: lock.locked = true } - CustomShortcut { + CustomShortcut { // qmllint disable unresolved-type name: "unlock" description: "Unlock the current session" onPressed: lock.unlock() diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index d06d374aa..12fe964f9 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -18,7 +18,7 @@ Item { Image { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" + source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type asynchronous: true fillMode: Image.PreserveAspectCrop diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 31f9fdfa1..c7f05a7c3 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -132,7 +132,7 @@ Scope { id: availProc command: ["sh", "-c", "fprintd-list $USER"] - onExited: code => { + onExited: code => { // qmllint disable signal-handler-parameters fprint.available = code === 0; fprint.checkAvail(); } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index d53cc7cbd..faced0827 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -85,7 +85,7 @@ StyledRect { return; const actions = root.modelData.actions; - if (actions?.length === 1) + if (actions.length === 1) actions[0].invoke(); } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index c8660a2f3..329e85eb2 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.config +import qs.modules.bar.popouts as BarPopouts import Quickshell import QtQuick @@ -10,7 +11,7 @@ Item { required property DrawerVisibilities visibilities required property Item sidebar - required property Item popouts + required property BarPopouts.Wrapper popouts readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 4f17073d9..03aa406f8 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -89,9 +89,9 @@ StyledRect { roleValue: "bluetooth" delegate: Toggle { icon: "bluetooth" - checked: Bluetooth.defaultAdapter?.enabled ?? false + checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type onClicked: { - const adapter = Bluetooth.defaultAdapter; + const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type if (adapter) adapter.enabled = !adapter.enabled; } diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 40846320f..4e46af86f 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -69,7 +69,7 @@ Item { anchors.centerIn: parent - captureSource: root.client?.wayland ?? null + captureSource: root.client?.wayland ?? null // qmllint disable unresolved-type live: true constraintSize.width: root.client ? parent.height * Math.min(root.screen.width / root.screen.height, root.client?.lastIpcObject.size[0] / root.client?.lastIpcObject.size[1]) : parent.height diff --git a/services/Brightness.qml b/services/Brightness.qml index 4e53ec097..a41a0d7a5 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -92,13 +92,17 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "brightnessUp" description: "Increase brightness" onPressed: root.increaseBrightness() } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "brightnessDown" description: "Decrease brightness" onPressed: root.decreaseBrightness() diff --git a/services/Hypr.qml b/services/Hypr.qml index 52e3d28fe..9690e788b 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -207,7 +207,9 @@ Singleton { target: "hypr" } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "refreshDevices" description: "Reload devices" onPressed: extras.refreshDevices() diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 3275a07c8..212acce82 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -903,7 +903,7 @@ Singleton { } } - onExited: code => { + onExited: code => { // qmllint disable signal-handler-parameters exitCode = code; Qt.callLater(() => { @@ -1268,7 +1268,7 @@ Singleton { id: rescanProc command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: root.getNetworks() + onExited: root.getNetworks() // qmllint disable signal-handler-parameters } Process { @@ -1283,7 +1283,7 @@ Singleton { stdout: SplitParser { onRead: root.refreshOnConnectionChange() } - onExited: monitorRestartTimer.start() + onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters } Timer { diff --git a/services/NotifData.qml b/services/NotifData.qml index 9806c96ec..7ad024e7a 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -51,7 +51,9 @@ QtObject { readonly property LazyLoader dummyImageLoader: LazyLoader { active: false + // qmllint disable uncreatable-type PanelWindow { + // qmllint enable uncreatable-type implicitWidth: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image color: "transparent" @@ -216,7 +218,9 @@ QtObject { urgency = notification.urgency; resident = notification.resident; hasActionIcons = notification.hasActionIcons; + // qmllint disable unresolved-type actions = notification.actions.map(a => ({ + // qmllint enable unresolved-type identifier: a.identifier, text: a.text, invoke: () => a.invoke() diff --git a/services/Notifs.qml b/services/Notifs.qml index ed187ce51..1fa1a158b 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -105,7 +105,9 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "clearNotifs" description: "Clear all notifications" onPressed: { diff --git a/services/Players.qml b/services/Players.qml index 65954c690..5317317ff 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -41,7 +41,9 @@ Singleton { reloadableId: "players" } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaToggle" description: "Toggle media playback" onPressed: { @@ -51,7 +53,9 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaPrev" description: "Previous track" onPressed: { @@ -61,7 +65,9 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaNext" description: "Next track" onPressed: { @@ -71,7 +77,9 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaStop" description: "Stop media playback" onPressed: root.active?.stop() diff --git a/services/Recorder.qml b/services/Recorder.qml index 4c9f9fd55..ff8a7d10d 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -46,7 +46,7 @@ Singleton { running: true command: ["pidof", "gpu-screen-recorder"] - onExited: code => { + onExited: code => { // qmllint disable signal-handler-parameters props.running = code === 0; if (code === 0) { @@ -77,6 +77,6 @@ Singleton { props.elapsed++; } - target: Time + target: Time // qmllint disable incompatible-type } } diff --git a/services/VPN.qml b/services/VPN.qml index 2d08631a1..297d76fff 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -128,7 +128,9 @@ Singleton { id: statusProc command: ["ip", "link", "show"] + // qmllint disable incompatible-type environment: ({ + // qmllint enable incompatible-type LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) @@ -143,7 +145,7 @@ Singleton { Process { id: connectProc - onExited: statusCheckTimer.start() + onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters stderr: StdioCollector { onStreamFinished: { const error = text.trim(); @@ -159,7 +161,7 @@ Singleton { Process { id: disconnectProc - onExited: statusCheckTimer.start() + onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters stderr: StdioCollector { onStreamFinished: { const error = text.trim(); From 921fb3504dfe5f8749f72c21160c35889e955fa2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:35:59 +1100 Subject: [PATCH 137/409] ci: add clazy to image --- .github/workflows/update-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 3a5647d1a..9700e012e 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -23,7 +23,7 @@ jobs: run: | cat > /tmp/Dockerfile <> /etc/sudoers && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ From 9f3ef267ac0006eeb92d126d1248a3251d62293c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:37:29 +1100 Subject: [PATCH 138/409] chore: fix formatting issues --- modules/lock/Lock.qml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index 25d8a08c8..283906cbf 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -25,13 +25,17 @@ Scope { lock: lock } - CustomShortcut { // qmllint disable unresolved-type + // qmllint disable unresolved-type + CustomShortcut { + // qmllint enable unresolved-type name: "lock" description: "Lock the current session" onPressed: lock.locked = true } - CustomShortcut { // qmllint disable unresolved-type + // qmllint disable unresolved-type + CustomShortcut { + // qmllint enable unresolved-type name: "unlock" description: "Unlock the current session" onPressed: lock.unlock() From e5972e789e6669731f22afbd2f1639429116ff01 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:46:24 +1100 Subject: [PATCH 139/409] ci: add lint workflow --- .github/workflows/lint.yml | 42 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/lint.yml diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..64e2abf89 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,42 @@ +name: Lint code + +on: + push: + branches: + - main + pull_request: + +jobs: + check-format: + runs-on: ubuntu-latest + + container: + image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest + + steps: + - uses: actions/checkout@v6 + + - name: Build + run: | + cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror + cmake --build build + + - name: Lint QML + shell: fish {0} + run: | + # Generate tooling + touch .qmlls.ini + QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p . + + # Construct linter args + set -l build_dir (grep -oP "(?<=buildDir=\")(.*)(?=\")" .qmlls.ini) + set -l import_paths (grep -oP "(?<=importPaths=\")(.*)(?=\")" .qmlls.ini | string split :) + set -l args -I $build_dir + for path in $import_paths + set -a args -I $path + end + set -l qml_files (string match -vr '(build|modules/controlcenter)/.*' **.qml) + + # Lint + set -l lint_out (qmllint --import disable $args $qml_files 2>&1 | tee /dev/stderr) + test -z "$lint_out" || exit 1 From 6917fac5f27837339f7661b1440793052ef12d7a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:48:02 +1100 Subject: [PATCH 140/409] ci: fix lint workflow job name --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 64e2abf89..873b19e3f 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -7,7 +7,7 @@ on: pull_request: jobs: - check-format: + lint: runs-on: ubuntu-latest container: From e4c27fad500b3ad60ff38554ef745fbb0b97a0dc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:51:34 +1100 Subject: [PATCH 141/409] ci: specify version manually when building --- .github/workflows/lint.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 873b19e3f..992f9165a 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,7 +18,8 @@ jobs: - name: Build run: | - cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror + # Use fake version for CI build + cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror -DVERSION=v0.0.0 cmake --build build - name: Lint QML From 6ff823408dec24bcab1c0c62ce026b7506593841 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:11:45 +1100 Subject: [PATCH 142/409] ci: update image deps --- .github/workflows/update-image.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 9700e012e..23006dd0c 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -23,13 +23,13 @@ jobs: run: | cat > /tmp/Dockerfile <> /etc/sudoers && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ cd /home/builder/yay-bin && \ sudo -u builder makepkg -si --noconfirm && \ - sudo -u builder yay -S --noconfirm quickshell-git && \ + sudo -u builder yay -S --noconfirm quickshell-git libcava && \ sudo -u builder yay -Yc --noconfirm && \ pacman -Rns --noconfirm yay-bin && \ sed -i '/builder ALL=(ALL) NOPASSWD:ALL/d' /etc/sudoers && \ From 71fc17735d36f00a70368458fff2762db2568425 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:21:53 +1100 Subject: [PATCH 143/409] feat: add import checking to format script --- scripts/qml-lint-conventions.py | 79 +++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py index 77fa0b66f..f709bf3fb 100755 --- a/scripts/qml-lint-conventions.py +++ b/scripts/qml-lint-conventions.py @@ -50,12 +50,89 @@ class Section(IntEnum): } RULE_COLOURS = { + "import-order": GREEN, "section-order": YELLOW, "missing-section-separator": CYAN, "blank-after-open-brace": MAGENTA, "blank-before-close-brace": MAGENTA, } +IMPORT_RE = re.compile(r"^import\s+(\S+)") + + +def import_group(module: str) -> tuple[int, int] | None: + """Return (group, depth) for a module import, or None to skip.""" + if module.startswith('"'): + return None + depth = module.count(".") + 1 + if module == "QtQuick" or module.startswith("QtQuick."): + return (1, depth) + if module.startswith("Qt"): + return (2, depth) + if module == "Quickshell" or module.startswith("Quickshell."): + return (3, depth) + if module == "M3Shapes": + return (4, depth) + if module == "Caelestia" or module.startswith("Caelestia."): + return (5, depth) + if module == "qs.components" or module.startswith("qs.components."): + return (6, depth) + if module == "qs.services": + return (7, depth) + if module == "qs.config": + return (8, depth) + if module == "qs.utils": + return (9, depth) + if module == "qs.modules" or module.startswith("qs.modules."): + return (10, depth) + return None + + +def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]: + """Check that module imports are in the required order.""" + violations = [] + imports: list[tuple[int, str, int, int]] = [] # (lineno, module, group, depth) + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped or stripped.startswith("//") or stripped.startswith("pragma "): + continue + m = IMPORT_RE.match(stripped) + if m: + module = m.group(1) + result = import_group(module) + if result is not None: + group, depth = result + imports.append((i + 1, module, group, depth)) + continue + break # end of import block + + for j in range(1, len(imports)): + prev_lineno, prev_mod, prev_group, prev_depth = imports[j - 1] + curr_lineno, curr_mod, curr_group, curr_depth = imports[j] + + if curr_group < prev_group: + violations.append( + Violation( + rel, + curr_lineno, + "import-order", + f"'{curr_mod}' should appear before '{prev_mod}'", + ) + ) + elif curr_group == prev_group and curr_depth < prev_depth: + violations.append( + Violation( + rel, + curr_lineno, + "import-order", + f"'{curr_mod}' should appear before '{prev_mod}' (less nested first)", + ) + ) + + return violations + + # Regexes PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s") SIGNAL_RE = re.compile(r"^signal\s") @@ -139,6 +216,8 @@ def check_file(filepath: Path) -> list[Violation]: except (OSError, UnicodeDecodeError): return violations + violations.extend(check_imports(filepath, lines, rel)) + scopes: dict[str, ScopeTracker] = {} # indent -> tracker in_block_comment = False func_skip_depth = 0 # brace depth for skipping function bodies only From d19ec2c2c8e611378ec72228d7d0e427cc7cc175 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:25:01 +1100 Subject: [PATCH 144/409] ci: manually define vars for lint build Git in containerised workflows doesn't really like to work properly, so avoid using it by manually defining variables --- .github/workflows/lint.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 992f9165a..de8c0f607 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -18,8 +18,8 @@ jobs: - name: Build run: | - # Use fake version for CI build - cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror -DVERSION=v0.0.0 + # Set version and git rev for CI build so we don't call git + cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror -DVERSION= -DGIT_REVISION= cmake --build build - name: Lint QML From 6068e9f08a16580da1300acf1d869be37fbfa984 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 02:28:49 +1100 Subject: [PATCH 145/409] ci: use abs path to qmllint --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index de8c0f607..7962963ca 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -39,5 +39,5 @@ jobs: set -l qml_files (string match -vr '(build|modules/controlcenter)/.*' **.qml) # Lint - set -l lint_out (qmllint --import disable $args $qml_files 2>&1 | tee /dev/stderr) + set -l lint_out (/usr/lib/qt6/bin/qmllint --import disable $args $qml_files 2>&1 | tee /dev/stderr) test -z "$lint_out" || exit 1 From 1a92294fca7469e9d608568e74ff84d38de77ac7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:01:59 +1100 Subject: [PATCH 146/409] feat: add auto format fix --- scripts/qml-lint-conventions.py | 169 +++++++++++++++++++++++++++++--- 1 file changed, 158 insertions(+), 11 deletions(-) diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py index f709bf3fb..a191d1a11 100755 --- a/scripts/qml-lint-conventions.py +++ b/scripts/qml-lint-conventions.py @@ -88,10 +88,15 @@ def import_group(module: str) -> tuple[int, int] | None: return None -def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]: - """Check that module imports are in the required order.""" - violations = [] - imports: list[tuple[int, str, int, int]] = [] # (lineno, module, group, depth) +def parse_imports(lines: list[str]) -> tuple[int | None, int | None, list[str], list[tuple[str, int, int, str]]]: + """Parse the import block, returning (first_idx, last_idx, relative_imports, module_imports). + + module_imports entries are (line_text, group, depth, module_name). + """ + first_import = None + last_import = None + relative_imports: list[str] = [] + module_imports: list[tuple[str, int, int, str]] = [] for i, line in enumerate(lines): stripped = line.strip() @@ -99,23 +104,49 @@ def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation] continue m = IMPORT_RE.match(stripped) if m: + if first_import is None: + first_import = i + last_import = i module = m.group(1) result = import_group(module) - if result is not None: + if result is None: + relative_imports.append(line) + else: group, depth = result - imports.append((i + 1, module, group, depth)) + module_imports.append((line, group, depth, module)) continue - break # end of import block + break + + return first_import, last_import, relative_imports, module_imports + + +def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]: + """Check that module imports are in the required order.""" + violations = [] + _, _, _, module_imports = parse_imports(lines) + imports = [(i, *entry) for i, entry in enumerate(module_imports)] for j in range(1, len(imports)): - prev_lineno, prev_mod, prev_group, prev_depth = imports[j - 1] - curr_lineno, curr_mod, curr_group, curr_depth = imports[j] + _, prev_line, prev_group, prev_depth, prev_mod = imports[j - 1] + _, curr_line, curr_group, curr_depth, curr_mod = imports[j] + + # Find actual line number for the current import + lineno = 0 + count = 0 + for li, line in enumerate(lines): + stripped = line.strip() + m = IMPORT_RE.match(stripped) + if m and import_group(m.group(1)) is not None: + if count == j: + lineno = li + 1 + break + count += 1 if curr_group < prev_group: violations.append( Violation( rel, - curr_lineno, + lineno, "import-order", f"'{curr_mod}' should appear before '{prev_mod}'", ) @@ -124,7 +155,7 @@ def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation] violations.append( Violation( rel, - curr_lineno, + lineno, "import-order", f"'{curr_mod}' should appear before '{prev_mod}' (less nested first)", ) @@ -133,6 +164,117 @@ def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation] return violations +def fix_imports(lines: list[str]) -> list[str]: + """Sort imports and return the modified lines.""" + first, last, relative, module = parse_imports(lines) + if first is None: + return lines + + module.sort(key=lambda x: (x[1], x[2], x[3])) + sorted_imports = relative + [entry[0] for entry in module] + return lines[:first] + sorted_imports + lines[last + 1 :] + + +def fix_section_separators(lines: list[str]) -> list[str]: + """Insert blank lines between different sections and return modified lines.""" + insertions: list[int] = [] + scopes: dict[str, ScopeTracker] = {} + in_block_comment = False + func_skip_depth = 0 + prev_blank: dict[str, bool] = {} + + for i, line in enumerate(lines): + stripped = line.strip() + indent = get_indent(line) + + if in_block_comment: + if BLOCK_COMMENT_END.search(stripped): + in_block_comment = False + continue + if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): + in_block_comment = True + continue + + if not stripped: + for key in prev_blank: + prev_blank[key] = True + continue + + if COMMENT_LINE_RE.match(stripped): + continue + + if func_skip_depth > 0: + func_skip_depth += stripped.count("{") - stripped.count("}") + if func_skip_depth <= 0: + func_skip_depth = 0 + continue + + if stripped == "}": + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + continue + + section = classify_line(stripped) + if section is None: + continue + + if indent not in scopes: + scopes[indent] = ScopeTracker() + prev_blank[indent] = True + + tracker = scopes[indent] + had_blank = prev_blank.get(indent, True) + + if tracker.last_section is not None and section != tracker.last_section and not had_blank: + insertions.append(i) + + if tracker.last_section is None or section >= tracker.last_section: + tracker.last_section = section + tracker.last_section_line = i + 1 + + prev_blank[indent] = False + + brace_count = stripped.count("{") - stripped.count("}") + if brace_count > 0 and section == Section.FUNCTION: + func_skip_depth = brace_count + if brace_count > 0 and section == Section.BINDING: + colon_idx = stripped.index(":") + after_colon = stripped[colon_idx + 1 :].strip() + if not re.match(r"^[A-Z]", after_colon): + func_skip_depth = brace_count + if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + + result = list(lines) + for idx in reversed(insertions): + result.insert(idx, "") + return result + + +def fix_file(filepath: Path) -> bool: + """Fix auto-fixable violations. Returns True if file was modified.""" + try: + text = filepath.read_text() + except (OSError, UnicodeDecodeError): + return False + + lines = text.splitlines() + lines = fix_imports(lines) + lines = fix_section_separators(lines) + new_text = "\n".join(lines) + if text.endswith("\n"): + new_text += "\n" + if new_text != text: + filepath.write_text(new_text) + return True + return False + + # Regexes PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s") SIGNAL_RE = re.compile(r"^signal\s") @@ -352,8 +494,13 @@ def check_file(filepath: Path) -> list[Violation]: def main(): + fix_mode = "--fix" in sys.argv qml_files = sorted(p for p in REPO_ROOT.rglob("*.qml") if "build" not in p.parts) + if fix_mode: + fixed = sum(1 for f in qml_files if fix_file(f)) + print(f"{BOLD}Fixed {fixed} file(s).{RESET}\n") + print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n") all_violations: list[Violation] = [] From d0f2df9bae89fe9c8deca5ba0d492b2bd984031e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:02:19 +1100 Subject: [PATCH 147/409] chore: fix all import formats --- components/Anim.qml | 2 +- components/CAnim.qml | 2 +- components/ConnectionHeader.qml | 4 ++-- components/ConnectionInfoSection.qml | 4 ++-- components/PropertyRow.qml | 4 ++-- components/SectionContainer.qml | 4 ++-- components/SectionHeader.qml | 4 ++-- components/StateLayer.qml | 2 +- components/StyledClippingRect.qml | 2 +- components/StyledText.qml | 2 +- components/controls/CircularIndicator.qml | 6 +++--- components/controls/CircularProgress.qml | 4 ++-- components/controls/CollapsibleSection.qml | 4 ++-- components/controls/CustomSpinBox.qml | 4 ++-- components/controls/FilledSlider.qml | 4 ++-- components/controls/IconButton.qml | 2 +- components/controls/IconTextButton.qml | 4 ++-- components/controls/Menu.qml | 4 ++-- components/controls/SpinBoxRow.qml | 4 ++-- components/controls/SplitButton.qml | 4 ++-- components/controls/SplitButtonRow.qml | 4 ++-- components/controls/StyledInputField.qml | 2 +- components/controls/StyledRadioButton.qml | 4 ++-- components/controls/StyledScrollBar.qml | 4 ++-- components/controls/StyledSlider.qml | 4 ++-- components/controls/StyledSwitch.qml | 6 +++--- components/controls/StyledTextField.qml | 4 ++-- components/controls/SwitchRow.qml | 4 ++-- components/controls/TextButton.qml | 2 +- components/controls/ToggleButton.qml | 4 ++-- components/controls/ToggleRow.qml | 4 ++-- components/controls/Tooltip.qml | 4 ++-- components/effects/ColouredIcon.qml | 4 ++-- components/effects/Elevation.qml | 2 +- components/effects/InnerBorder.qml | 4 ++-- components/effects/OpacityMask.qml | 2 +- components/filedialog/CurrentItem.qml | 4 ++-- components/filedialog/DialogButtons.qml | 2 +- components/filedialog/FileDialog.qml | 6 +++--- components/filedialog/FolderContents.qml | 12 ++++++------ components/filedialog/HeaderBar.qml | 4 ++-- components/filedialog/Sidebar.qml | 4 ++-- components/images/CachingIconImage.qml | 4 ++-- components/images/CachingImage.qml | 6 +++--- components/widgets/ExtraIndicator.qml | 2 +- config/Config.qml | 6 +++--- config/ServiceConfig.qml | 2 +- config/UserPaths.qml | 2 +- modules/BatteryMonitor.qml | 6 +++--- modules/IdleMonitors.qml | 6 +++--- modules/Shortcuts.qml | 8 ++++---- modules/areapicker/AreaPicker.qml | 6 +++--- modules/areapicker/Picker.qml | 12 ++++++------ modules/background/Background.qml | 6 +++--- modules/background/DesktopClock.qml | 6 +++--- modules/background/Visualiser.qml | 10 +++++----- modules/background/Wallpaper.qml | 4 ++-- modules/bar/Bar.qml | 8 ++++---- modules/bar/BarWrapper.qml | 4 ++-- modules/bar/components/ActiveWindow.qml | 4 ++-- modules/bar/components/Clock.qml | 2 +- modules/bar/components/OsIcon.qml | 2 +- modules/bar/components/Power.qml | 2 +- modules/bar/components/Settings.qml | 4 ++-- modules/bar/components/SettingsIcon.qml | 4 ++-- modules/bar/components/StatusIcons.qml | 12 ++++++------ modules/bar/components/Tray.qml | 6 +++--- modules/bar/components/TrayItem.qml | 4 ++-- .../components/workspaces/ActiveIndicator.qml | 2 +- modules/bar/components/workspaces/OccupiedBg.qml | 4 ++-- .../components/workspaces/SpecialWorkspaces.qml | 10 +++++----- modules/bar/components/workspaces/Workspace.qml | 8 ++++---- modules/bar/components/workspaces/Workspaces.qml | 10 +++++----- modules/bar/popouts/ActiveWindow.qml | 10 +++++----- modules/bar/popouts/Audio.qml | 8 ++++---- modules/bar/popouts/Background.qml | 4 ++-- modules/bar/popouts/Battery.qml | 4 ++-- modules/bar/popouts/Bluetooth.qml | 8 ++++---- modules/bar/popouts/Content.qml | 9 ++++----- modules/bar/popouts/LockStatus.qml | 2 +- modules/bar/popouts/Network.qml | 6 +++--- modules/bar/popouts/TrayMenu.qml | 8 ++++---- modules/bar/popouts/WirelessPassword.qml | 6 +++--- modules/bar/popouts/Wrapper.qml | 10 +++++----- modules/bar/popouts/kblayout/KbLayoutModel.qml | 7 ++++--- modules/controlcenter/ControlCenter.qml | 6 +++--- modules/controlcenter/NavRail.qml | 6 +++--- modules/controlcenter/Panes.qml | 6 +++--- modules/controlcenter/Session.qml | 2 +- modules/controlcenter/WindowFactory.qml | 4 ++-- modules/controlcenter/WindowTitle.qml | 4 ++-- .../controlcenter/appearance/AppearancePane.qml | 12 ++++++------ .../appearance/sections/AnimationsSection.qml | 6 +++--- .../appearance/sections/BackgroundSection.qml | 6 +++--- .../appearance/sections/BorderSection.qml | 6 +++--- .../appearance/sections/ColorSchemeSection.qml | 8 ++++---- .../appearance/sections/ColorVariantSection.qml | 8 ++++---- .../appearance/sections/FontsSection.qml | 6 +++--- .../appearance/sections/ScalesSection.qml | 6 +++--- .../appearance/sections/ThemeModeSection.qml | 4 ++-- .../appearance/sections/TransparencySection.qml | 6 +++--- modules/controlcenter/audio/AudioPane.qml | 8 ++++---- modules/controlcenter/bluetooth/BtPane.qml | 8 ++++---- modules/controlcenter/bluetooth/Details.qml | 8 ++++---- modules/controlcenter/bluetooth/DeviceList.qml | 10 +++++----- modules/controlcenter/bluetooth/Settings.qml | 6 +++--- .../components/ConnectedButtonGroup.qml | 4 ++-- .../controlcenter/components/DeviceDetails.qml | 6 +++--- modules/controlcenter/components/DeviceList.qml | 8 ++++---- .../controlcenter/components/PaneTransition.qml | 2 +- .../controlcenter/components/ReadonlySlider.qml | 4 ++-- .../controlcenter/components/SettingsHeader.qml | 4 ++-- modules/controlcenter/components/SliderInput.qml | 4 ++-- .../controlcenter/components/SplitPaneLayout.qml | 6 +++--- .../components/SplitPaneWithDetails.qml | 8 ++++---- .../controlcenter/components/WallpaperGrid.qml | 4 ++-- .../controlcenter/dashboard/DashboardPane.qml | 10 +++++----- .../controlcenter/dashboard/GeneralSection.qml | 4 ++-- .../dashboard/PerformanceSection.qml | 2 +- modules/controlcenter/launcher/LauncherPane.qml | 14 +++++++------- modules/controlcenter/launcher/Settings.qml | 4 ++-- .../controlcenter/network/EthernetDetails.qml | 6 +++--- modules/controlcenter/network/EthernetList.qml | 6 +++--- modules/controlcenter/network/EthernetPane.qml | 4 ++-- .../controlcenter/network/EthernetSettings.qml | 4 ++-- .../controlcenter/network/NetworkSettings.qml | 8 ++++---- modules/controlcenter/network/NetworkingPane.qml | 10 +++++----- modules/controlcenter/network/VpnDetails.qml | 8 ++++---- modules/controlcenter/network/VpnList.qml | 8 ++++---- modules/controlcenter/network/VpnSettings.qml | 10 +++++----- .../controlcenter/network/WirelessDetails.qml | 6 +++--- modules/controlcenter/network/WirelessList.qml | 8 ++++---- modules/controlcenter/network/WirelessPane.qml | 4 ++-- .../network/WirelessPasswordDialog.qml | 8 ++++---- .../controlcenter/network/WirelessSettings.qml | 4 ++-- modules/controlcenter/state/BluetoothState.qml | 2 +- modules/controlcenter/taskbar/TaskbarPane.qml | 10 +++++----- modules/dashboard/Background.qml | 4 ++-- modules/dashboard/Content.qml | 8 ++++---- modules/dashboard/Dash.qml | 4 ++-- modules/dashboard/LyricMenu.qml | 4 ++-- modules/dashboard/LyricsView.qml | 6 +++--- modules/dashboard/Media.qml | 14 +++++++------- modules/dashboard/Performance.qml | 2 +- modules/dashboard/Tabs.qml | 8 ++++---- modules/dashboard/Weather.qml | 4 ++-- modules/dashboard/Wrapper.qml | 6 +++--- modules/dashboard/dash/Calendar.qml | 8 ++++---- modules/dashboard/dash/DateTime.qml | 4 ++-- modules/dashboard/dash/Media.qml | 6 +++--- modules/dashboard/dash/Resources.qml | 2 +- modules/dashboard/dash/User.qml | 4 ++-- modules/dashboard/dash/Weather.qml | 2 +- modules/drawers/Backgrounds.qml | 14 +++++++------- modules/drawers/Border.qml | 4 ++-- modules/drawers/Drawers.qml | 12 ++++++------ modules/drawers/Exclusions.qml | 4 ++-- modules/drawers/Interactions.qml | 6 +++--- modules/drawers/Panels.qml | 16 ++++++++-------- modules/launcher/AppList.qml | 10 +++++----- modules/launcher/Background.qml | 4 ++-- modules/launcher/Content.qml | 4 ++-- modules/launcher/ContentList.qml | 2 +- modules/launcher/WallpaperList.qml | 4 ++-- modules/launcher/Wrapper.qml | 4 ++-- modules/launcher/items/ActionItem.qml | 2 +- modules/launcher/items/AppItem.qml | 8 ++++---- modules/launcher/items/CalcItem.qml | 8 ++++---- modules/launcher/items/SchemeItem.qml | 4 ++-- modules/launcher/items/VariantItem.qml | 4 ++-- modules/launcher/items/WallpaperItem.qml | 4 ++-- modules/launcher/services/Actions.qml | 4 ++-- modules/launcher/services/Apps.qml | 4 ++-- modules/launcher/services/M3Variants.qml | 4 ++-- modules/launcher/services/Schemes.qml | 6 +++--- modules/lock/Center.qml | 4 ++-- modules/lock/Content.qml | 4 ++-- modules/lock/Fetch.qml | 6 +++--- modules/lock/InputField.qml | 6 +++--- modules/lock/Lock.qml | 2 +- modules/lock/LockSurface.qml | 6 +++--- modules/lock/Media.qml | 4 ++-- modules/lock/NotifDock.qml | 8 ++++---- modules/lock/NotifGroup.qml | 10 +++++----- modules/lock/Pam.qml | 4 ++-- modules/lock/Resources.qml | 4 ++-- modules/lock/WeatherInfo.qml | 4 ++-- modules/notifications/Background.qml | 4 ++-- modules/notifications/Content.qml | 6 +++--- modules/notifications/Notification.qml | 12 ++++++------ modules/notifications/Wrapper.qml | 2 +- modules/osd/Background.qml | 4 ++-- modules/osd/Content.qml | 4 ++-- modules/osd/Wrapper.qml | 4 ++-- modules/session/Background.qml | 4 ++-- modules/session/Content.qml | 4 ++-- modules/session/Wrapper.qml | 2 +- modules/sidebar/Background.qml | 4 ++-- modules/sidebar/Content.qml | 4 ++-- modules/sidebar/Notif.qml | 6 +++--- modules/sidebar/NotifActionList.qml | 8 ++++---- modules/sidebar/NotifDock.qml | 10 +++++----- modules/sidebar/NotifDockList.qml | 4 ++-- modules/sidebar/NotifGroup.qml | 8 ++++---- modules/sidebar/NotifGroupList.qml | 8 ++++---- modules/sidebar/Wrapper.qml | 2 +- modules/utilities/Background.qml | 4 ++-- modules/utilities/Content.qml | 4 ++-- modules/utilities/RecordingDeleteModal.qml | 8 ++++---- modules/utilities/Wrapper.qml | 4 ++-- modules/utilities/cards/IdleInhibit.qml | 4 ++-- modules/utilities/cards/Record.qml | 4 ++-- modules/utilities/cards/RecordingList.qml | 12 ++++++------ modules/utilities/cards/Toggles.qml | 6 +++--- modules/utilities/toasts/ToastItem.qml | 6 +++--- modules/utilities/toasts/Toasts.qml | 6 +++--- modules/windowinfo/Buttons.qml | 6 +++--- modules/windowinfo/Details.qml | 6 +++--- modules/windowinfo/Preview.qml | 10 +++++----- modules/windowinfo/WindowInfo.qml | 8 ++++---- services/Audio.qml | 8 ++++---- services/Brightness.qml | 6 +++--- services/Colours.qml | 8 ++++---- services/GameMode.qml | 8 ++++---- services/Hypr.qml | 10 +++++----- services/LyricsService.qml | 8 ++++---- services/Network.qml | 2 +- services/NetworkUsage.qml | 7 ++----- services/Nmcli.qml | 2 +- services/NotifData.qml | 8 ++++---- services/Notifs.qml | 12 ++++++------ services/Players.qml | 6 +++--- services/Recorder.qml | 2 +- services/Screens.qml | 2 +- services/SystemUsage.qml | 4 ++-- services/Time.qml | 4 ++-- services/VPN.qml | 4 ++-- services/Visibilities.qml | 2 +- services/Wallpapers.qml | 8 ++++---- services/Weather.qml | 6 +++--- utils/Icons.qml | 4 ++-- utils/NetworkConnection.qml | 2 +- utils/Paths.qml | 6 +++--- utils/Searcher.qml | 3 +-- utils/SysInfo.qml | 6 +++--- 245 files changed, 671 insertions(+), 675 deletions(-) diff --git a/components/Anim.qml b/components/Anim.qml index 6883a7984..505243a79 100644 --- a/components/Anim.qml +++ b/components/Anim.qml @@ -1,5 +1,5 @@ -import qs.config import QtQuick +import qs.config NumberAnimation { duration: Appearance.anim.durations.normal diff --git a/components/CAnim.qml b/components/CAnim.qml index 49484b789..f2f4e40fe 100644 --- a/components/CAnim.qml +++ b/components/CAnim.qml @@ -1,5 +1,5 @@ -import qs.config import QtQuick +import qs.config ColorAnimation { duration: Appearance.anim.durations.normal diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml index f88dc4ed8..cdba17bc1 100644 --- a/components/ConnectionHeader.qml +++ b/components/ConnectionHeader.qml @@ -1,7 +1,7 @@ -import qs.components -import qs.config import QtQuick import QtQuick.Layouts +import qs.components +import qs.config ColumnLayout { id: root diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml index cdb2cb029..9063a6d26 100644 --- a/components/ConnectionInfoSection.qml +++ b/components/ConnectionInfoSection.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml index 640d5f743..67315b9c5 100644 --- a/components/PropertyRow.qml +++ b/components/PropertyRow.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 775456c77..8b57cdd43 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml index 502e91895..a77247692 100644 --- a/components/SectionHeader.qml +++ b/components/SectionHeader.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/components/StateLayer.qml b/components/StateLayer.qml index 7cd19b575..f3e3ea13d 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -1,6 +1,6 @@ +import QtQuick import qs.services import qs.config -import QtQuick MouseArea { id: root diff --git a/components/StyledClippingRect.qml b/components/StyledClippingRect.qml index 8f2630c13..6484e0e6f 100644 --- a/components/StyledClippingRect.qml +++ b/components/StyledClippingRect.qml @@ -1,5 +1,5 @@ -import Quickshell.Widgets import QtQuick +import Quickshell.Widgets ClippingRectangle { id: root diff --git a/components/StyledText.qml b/components/StyledText.qml index ed961d26a..5bf8add34 100644 --- a/components/StyledText.qml +++ b/components/StyledText.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound +import QtQuick import qs.services import qs.config -import QtQuick Text { id: root diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index 957899e5c..844765005 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -1,9 +1,9 @@ import ".." -import qs.services -import qs.config -import Caelestia.Internal import QtQuick import QtQuick.Templates +import Caelestia.Internal +import qs.services +import qs.config BusyIndicator { id: root diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index a15cd900b..8cd585eef 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,8 +1,8 @@ import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Shapes +import qs.services +import qs.config Shape { id: root diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 68f806c10..7e164ebbd 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -1,9 +1,9 @@ import ".." +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index eaf2eb591..f63b1402b 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import qs.services +import qs.config RowLayout { id: root diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index 80dd44c5f..c865482c8 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -1,9 +1,9 @@ import ".." import "../effects" -import qs.services -import qs.config import QtQuick import QtQuick.Templates +import qs.services +import qs.config Slider { id: root diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index 2e5f4a3dd..569998941 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -1,7 +1,7 @@ import ".." +import QtQuick import qs.services import qs.config -import QtQuick StyledRect { id: root diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index 74c8cf2ba..d213b9be8 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -1,8 +1,8 @@ import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import qs.services +import qs.config StyledRect { id: root diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index b5a672df5..b60a36399 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound import ".." import "../effects" -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import qs.services +import qs.config Elevation { id: root diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index c43cbc782..b2c6ef1a8 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -1,9 +1,9 @@ import ".." +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index d8d256bf1..0e9aef8f1 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -1,8 +1,8 @@ import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import qs.services +import qs.config Row { id: root diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index b8e5f9ced..b4f9cf0c3 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index cd6717658..9c3390fe3 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick import qs.components import qs.services import qs.config -import QtQuick Item { id: root diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index d0a242bf6..458e6fe07 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Templates import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Templates RadioButton { id: root diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index bd7a7af08..4c30dda3a 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -1,8 +1,8 @@ import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Templates +import qs.services +import qs.config ScrollBar { id: root diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index 0ef229df2..21b2f2cbf 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Templates import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Templates Slider { id: root diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index ce93cd505..b56410a9f 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -1,9 +1,9 @@ import ".." -import qs.services -import qs.config import QtQuick -import QtQuick.Templates import QtQuick.Shapes +import QtQuick.Templates +import qs.services +import qs.config Switch { id: root diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 6f3532dcf..bf8e5b54b 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Controls +import qs.services +import qs.config TextField { id: root diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 7e950cb01..6c82d72cc 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -1,9 +1,9 @@ import ".." +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index 40419e489..1066910f0 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -1,7 +1,7 @@ import ".." +import QtQuick import qs.services import qs.config -import QtQuick StyledRect { id: root diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index c054db8b3..1531b8b4d 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml index 130af7158..2ec35dc77 100644 --- a/components/controls/ToggleRow.qml +++ b/components/controls/ToggleRow.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.config -import QtQuick -import QtQuick.Layouts RowLayout { id: root diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index 4330d5717..d550cc921 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -1,9 +1,9 @@ import ".." +import QtQuick +import QtQuick.Controls import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Controls Popup { id: root diff --git a/components/effects/ColouredIcon.qml b/components/effects/ColouredIcon.qml index 5ef4d4cc0..570246bf0 100644 --- a/components/effects/ColouredIcon.qml +++ b/components/effects/ColouredIcon.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound -import Caelestia -import Quickshell.Widgets import QtQuick +import Quickshell.Widgets +import Caelestia IconImage { id: root diff --git a/components/effects/Elevation.qml b/components/effects/Elevation.qml index fb29f16e8..cfe5cb79a 100644 --- a/components/effects/Elevation.qml +++ b/components/effects/Elevation.qml @@ -1,7 +1,7 @@ import ".." -import qs.services import QtQuick import QtQuick.Effects +import qs.services RectangularShadow { property int level diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index d4a751f84..462f77af0 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Effects +import qs.services +import qs.config StyledRect { property alias innerRadius: maskInner.radius diff --git a/components/effects/OpacityMask.qml b/components/effects/OpacityMask.qml index f852f4478..8e625034e 100644 --- a/components/effects/OpacityMask.qml +++ b/components/effects/OpacityMask.qml @@ -1,5 +1,5 @@ -import Quickshell import QtQuick +import Quickshell ShaderEffect { required property Item source diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index a33523b7f..0603a626b 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -1,8 +1,8 @@ import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Shapes +import qs.services +import qs.config Item { id: root diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index 5a30a12f2..725f8fc90 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -1,7 +1,7 @@ +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick.Layouts StyledRect { id: root diff --git a/components/filedialog/FileDialog.qml b/components/filedialog/FileDialog.qml index f3187a55b..90dd2bc20 100644 --- a/components/filedialog/FileDialog.qml +++ b/components/filedialog/FileDialog.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import qs.components +import qs.services LazyLoader { id: loader diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 9789a3da2..56a4a2849 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Caelestia.Models import qs.components -import qs.components.filedialog import qs.components.controls +import qs.components.filedialog import qs.components.images import qs.services import qs.config import qs.utils -import Caelestia.Models -import Quickshell -import QtQuick -import QtQuick.Layouts -import QtQuick.Effects Item { id: root diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index 404711643..b44ac58b3 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import qs.services +import qs.config StyledRect { id: root diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index a6389864e..d6acb8115 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.filedialog import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/components/images/CachingIconImage.qml b/components/images/CachingIconImage.qml index d22b5131d..001e95de4 100644 --- a/components/images/CachingIconImage.qml +++ b/components/images/CachingIconImage.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound -import qs.utils -import Quickshell.Widgets import QtQuick +import Quickshell.Widgets +import qs.utils Item { id: root diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index 83bd5e47a..5c5f8bbbe 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -1,7 +1,7 @@ -import qs.utils -import Caelestia.Internal -import Quickshell import QtQuick +import Quickshell +import Caelestia.Internal +import qs.utils Image { id: root diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index db73ea08f..44738d85d 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -1,8 +1,8 @@ import ".." import "../effects" +import QtQuick import qs.services import qs.config -import QtQuick StyledRect { required property int extra diff --git a/config/Config.qml b/config/Config.qml index 7ec9bbe06..cb8eb9f7a 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -1,10 +1,10 @@ pragma Singleton -import qs.utils -import Caelestia +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia +import qs.utils Singleton { id: root diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml index 5294fb691..5f3c24fb1 100644 --- a/config/ServiceConfig.qml +++ b/config/ServiceConfig.qml @@ -1,5 +1,5 @@ -import Quickshell.Io import QtQuick +import Quickshell.Io JsonObject { property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" diff --git a/config/UserPaths.qml b/config/UserPaths.qml index ea4bf4597..b7a2e34ba 100644 --- a/config/UserPaths.qml +++ b/config/UserPaths.qml @@ -1,5 +1,5 @@ -import qs.utils import Quickshell.Io +import qs.utils JsonObject { property string wallpaperDir: `${Paths.pictures}/Wallpapers` diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index 9d6e4e845..0596a1aa6 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -1,8 +1,8 @@ -import qs.config -import Caelestia +import QtQuick import Quickshell import Quickshell.Services.UPower -import QtQuick +import Caelestia +import qs.config Scope { id: root diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml index b7ce05843..440dc704d 100644 --- a/modules/IdleMonitors.qml +++ b/modules/IdleMonitors.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import "lock" -import qs.config -import qs.services -import Caelestia.Internal import Quickshell import Quickshell.Wayland +import Caelestia.Internal +import qs.services +import qs.config Scope { id: root diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index 64509dacb..d73e63511 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -1,9 +1,9 @@ -import qs.components.misc -import qs.modules.controlcenter -import qs.services -import Caelestia import Quickshell import Quickshell.Io +import Caelestia +import qs.components.misc +import qs.services +import qs.modules.controlcenter Scope { id: root diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml index f9b046bee..76cc10399 100644 --- a/modules/areapicker/AreaPicker.qml +++ b/modules/areapicker/AreaPicker.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import Quickshell +import Quickshell.Io +import Quickshell.Wayland import qs.components.containers import qs.components.misc import qs.services -import Quickshell -import Quickshell.Wayland -import Quickshell.Io Scope { LazyLoader { diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 3af11fcaf..e6272737a 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Caelestia +import QtQuick +import QtQuick.Effects import Quickshell import Quickshell.Io import Quickshell.Wayland -import QtQuick -import QtQuick.Effects +import Caelestia +import qs.components +import qs.services +import qs.config MouseArea { id: root diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 5c7cc388a..95109e121 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Wayland import qs.components.containers import qs.services import qs.config -import Quickshell -import Quickshell.Wayland -import QtQuick Loader { asynchronous: true diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index e81fd3af8..86f9c623f 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts -import QtQuick.Effects Item { id: root diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 813c0a077..780abdf4b 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Widgets +import Caelestia.Services import qs.components import qs.services import qs.config -import Caelestia.Services -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Effects Item { id: root diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index ba9677cf0..10d743baa 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick import qs.components -import qs.components.images import qs.components.filedialog +import qs.components.images import qs.services import qs.config import qs.utils -import QtQuick Item { id: root diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index d4e8e977b..b82a17c74 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config import "popouts" as BarPopouts import "components" import "components/workspaces" -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import qs.components +import qs.services +import qs.config ColumnLayout { id: root diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 8d5f9a738..df29f0af1 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components import qs.config import qs.modules.bar.popouts as BarPopouts -import Quickshell -import QtQuick Item { id: root diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 0ae727b30..0fa277ee4 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick import qs.components import qs.services -import qs.utils import qs.config -import QtQuick +import qs.utils Item { id: root diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 96ad65564..90ed78cf1 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound +import QtQuick import qs.components import qs.services import qs.config -import QtQuick StyledRect { id: root diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index 0ad3bf30c..fc16a4fca 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -1,9 +1,9 @@ +import QtQuick import qs.components import qs.components.effects import qs.services import qs.config import qs.utils -import QtQuick Item { id: root diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index d56d37c89..5f5738115 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -1,7 +1,7 @@ +import QtQuick import qs.components import qs.services import qs.config -import QtQuick Item { id: root diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index c235c3ceb..f6ed88af6 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -1,8 +1,8 @@ +import QtQuick import qs.components -import qs.modules.controlcenter import qs.services import qs.config -import QtQuick +import qs.modules.controlcenter Item { id: root diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index c235c3ceb..f6ed88af6 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -1,8 +1,8 @@ +import QtQuick import qs.components -import qs.modules.controlcenter import qs.services import qs.config -import QtQuick +import qs.modules.controlcenter Item { id: root diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index b968d6877..455e04ef1 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.utils -import qs.config +import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth import Quickshell.Services.UPower -import QtQuick -import QtQuick.Layouts +import qs.components +import qs.services +import qs.config +import qs.utils StyledRect { id: root diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index b5f0b020b..79b57755e 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Services.SystemTray import qs.components import qs.services import qs.config -import Quickshell -import Quickshell.Services.SystemTray -import QtQuick StyledRect { id: root diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index 99119073d..c5cb9fe82 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell.Services.SystemTray import qs.components.effects import qs.services import qs.config import qs.utils -import Quickshell.Services.SystemTray -import QtQuick MouseArea { id: root diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index bad146eb2..e8a52d4d4 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -1,8 +1,8 @@ +import QtQuick import qs.components import qs.components.effects import qs.services import qs.config -import QtQuick StyledRect { id: root diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index ac4f36f38..0ed1831fa 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components import qs.services import qs.config -import Quickshell -import QtQuick Item { id: root diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 058a6a5f3..e62ed7190 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland import qs.components import qs.components.effects import qs.services -import qs.utils import qs.config -import Quickshell -import Quickshell.Hyprland -import QtQuick -import QtQuick.Layouts +import qs.utils Item { id: root diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index 7c35f96eb..bfbdc2ac1 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components import qs.services -import qs.utils import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts +import qs.utils ColumnLayout { id: root diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index 43f8fba68..f205dfac0 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound -import qs.services -import qs.config -import qs.components -import Quickshell import QtQuick -import QtQuick.Layouts import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import qs.components +import qs.services +import qs.config StyledClippingRect { id: root diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 0cb5f69bb..7e4bd60a0 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -1,11 +1,11 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Wayland +import Quickshell.Widgets import qs.components import qs.services -import qs.utils import qs.config -import Quickshell.Widgets -import Quickshell.Wayland -import QtQuick -import QtQuick.Layouts +import qs.utils Item { id: root diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 20cf826e0..52ac3b806 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.Pipewire import qs.components import qs.components.controls import qs.services import qs.config -import Quickshell.Services.Pipewire -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls Item { id: root diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml index 075b69881..cfba86d3a 100644 --- a/modules/bar/popouts/Background.qml +++ b/modules/bar/popouts/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 50cea105c..86d903d24 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell.Services.UPower import qs.components import qs.services import qs.config -import Quickshell.Services.UPower -import QtQuick Column { id: root diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index cd24dce3a..2e87270a1 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 5ba8b3c07..1c4792e1e 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -1,12 +1,11 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config +import "./kblayout" +import QtQuick import Quickshell import Quickshell.Services.SystemTray -import QtQuick - -import "./kblayout" +import qs.components +import qs.config Item { id: root diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml index 7d74530e3..9b61e0372 100644 --- a/modules/bar/popouts/LockStatus.qml +++ b/modules/bar/popouts/LockStatus.qml @@ -1,7 +1,7 @@ +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick.Layouts ColumnLayout { spacing: Appearance.spacing.small diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 4c61636fd..53350ee73 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components import qs.components.controls import qs.services import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 09194d97e..50bd60a50 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Widgets import qs.components import qs.services import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Controls StackView { id: root diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 4d0b7aed5..4d439cc57 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components import qs.components.controls import qs.services import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 888611669..06bec9b0b 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland import qs.components import qs.services import qs.config -import qs.modules.windowinfo import qs.modules.controlcenter -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland -import QtQuick +import qs.modules.windowinfo Item { id: root diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index d5515f1aa..65c809b3d 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -1,11 +1,9 @@ pragma ComponentBehavior: Bound import QtQuick - import Quickshell.Io - -import qs.config import Caelestia +import qs.config // TODO: handle this better later @@ -17,9 +15,11 @@ Item { ListModel { id: _visibleModel } + property alias visibleModel: _visibleModel property string activeLabel: "" + property int activeIndex: -1 function start() { @@ -132,6 +132,7 @@ Item { } property var _xkbMap: ({}) + property bool _notifiedLimit: false Process { diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 637d127c7..f542b5970 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components import qs.components.controls import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index b4eb4cd81..037ea0d64 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components import qs.services import qs.config import qs.modules.controlcenter -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index cd7147069..5660ea7d3 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -7,13 +7,13 @@ import "appearance" import "taskbar" import "launcher" import "dashboard" +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets import qs.components import qs.services import qs.config import qs.modules.controlcenter -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts ClippingRectangle { id: root diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 8a8545f0f..e38396810 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -1,5 +1,5 @@ -import QtQuick import "./state" +import QtQuick import qs.modules.controlcenter QtObject { diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index 068e970be..266af9095 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -1,9 +1,9 @@ pragma Singleton +import QtQuick +import Quickshell import qs.components import qs.services -import Quickshell -import QtQuick Singleton { id: root diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index 0364fab43..a55445c5a 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -1,8 +1,8 @@ +import QtQuick +import Quickshell import qs.components import qs.services import qs.config -import Quickshell -import QtQuick StyledRect { id: root diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 2d22425ee..c42220762 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -4,19 +4,19 @@ import ".." import "../components" import "./sections" import "../../launcher/services" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Models import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.components.images import qs.services import qs.config import qs.utils -import Caelestia.Models -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index 0cba5cecd..e4d8a0333 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 7f528e996..8b50c1242 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index a259f934e..e0c677cf7 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 4b4559ae6..0e96de70f 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts CollapsibleSection { title: qsTr("Color scheme") diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index 3de9e4b3c..0977c2bbe 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts CollapsibleSection { title: qsTr("Color variant") diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 8c288608e..dc42f3b9e 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index b0e6e38b8..6d5d5b303 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index 04eed9113..c63c73aaf 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick CollapsibleSection { title: qsTr("Theme mode") diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 9a48629c1..77582f9c6 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 159c862e2..fe03d8b37 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 1b9a7fcfa..97ea99c57 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -3,13 +3,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import Quickshell.Bluetooth +import Quickshell.Widgets import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.config -import Quickshell.Widgets -import Quickshell.Bluetooth -import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 9b728fcf6..9b347acdc 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts StyledFlickable { id: root diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index a943f2806..b53829b93 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts DeviceList { id: root diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index 557e671b9..6936c1ea2 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 4115003f2..15b1896cb 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -1,11 +1,11 @@ import ".." +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index 8e5cdb2ce..c150abd7d 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Layouts import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.config -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 7c5292da1..f121eefe2 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml index 5d80dbec2..ac9c3371a 100644 --- a/modules/controlcenter/components/PaneTransition.qml +++ b/modules/controlcenter/components/PaneTransition.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound -import qs.config import QtQuick +import qs.config SequentialAnimation { id: root diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index 169d63653..105270451 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -1,11 +1,11 @@ import ".." import "../components" +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index 0dc190c05..6d392f5ea 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config import QtQuick import QtQuick.Layouts +import qs.components +import qs.config Item { id: root diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 1aed5cbc3..d677fc303 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 5c1a8be68..5bf5c41ad 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets import qs.components import qs.components.effects import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts RowLayout { id: root diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index 79b23abc0..92d9c2c84 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets import qs.components -import qs.components.effects import qs.components.containers +import qs.components.effects import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 588d51d1f..674886a7f 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import Caelestia.Models import qs.components import qs.components.controls import qs.components.effects import qs.components.images import qs.services import qs.config -import Caelestia.Models -import QtQuick GridView { id: root diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index d7186a790..bd6b9d5f1 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -2,17 +2,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 95e7531ed..61e83d3c7 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -1,11 +1,11 @@ import ".." import "../components" +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts SectionContainer { id: root diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index ac84752b6..eebf5fd29 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -5,8 +5,8 @@ import QtQuick.Layouts import Quickshell.Services.UPower import qs.components import qs.components.controls -import qs.config import qs.services +import qs.config SectionContainer { id: root diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 6de344691..4703cace4 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -3,19 +3,19 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "../../launcher/services" +import "../../../utils/scripts/fuzzysort.js" as Fuzzy +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import Caelestia -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import "../../../utils/scripts/fuzzysort.js" as Fuzzy Item { id: root diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 5eaf6e0e0..95fc2fb07 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 4e60b3d48..9b78ccafc 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config -import QtQuick -import QtQuick.Layouts DeviceDetails { id: root diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 87e00015d..4fcd5b92d 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts DeviceList { id: root diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 59d82bb08..8fb833fde 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import Quickshell.Widgets import qs.components import qs.components.containers import qs.config -import Quickshell.Widgets -import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 90bfcf46a..3d99c47c2 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index bda7cb18a..15ac56d24 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 0a6b20e27..990c4a269 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -3,17 +3,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 23e4010b4..8d91067c7 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts DeviceDetails { id: root diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index e8b49d38c..7f0bf3b18 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 49d801d9a..d335b6588 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index beaaff3f0..913e5d75a 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -3,15 +3,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import QtQuick.Layouts import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import QtQuick -import QtQuick.Layouts DeviceDetails { id: root diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 1713022eb..b1738f76a 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -3,16 +3,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts DeviceList { id: root diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 8150af9cf..cccf8222c 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import Quickshell.Widgets import qs.components import qs.components.containers import qs.config -import Quickshell.Widgets -import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 6db01bc06..0c2cb7143 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "." +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index b4eb391d4..7b929bf98 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml index 8678672df..9b16bfe59 100644 --- a/modules/controlcenter/state/BluetoothState.qml +++ b/modules/controlcenter/state/BluetoothState.qml @@ -1,5 +1,5 @@ -import Quickshell.Bluetooth import QtQuick +import Quickshell.Bluetooth QtObject { id: root diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 7fcdec9e5..d61e932dc 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -2,17 +2,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml index e2a91f741..c6223eb6c 100644 --- a/modules/dashboard/Background.qml +++ b/modules/dashboard/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 9012639ca..4a6d9e61a 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets import qs.components import qs.components.filedialog import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index c39decbf4..13073c14d 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -1,9 +1,9 @@ +import "dash" +import QtQuick.Layouts import qs.components import qs.components.filedialog import qs.services import qs.config -import "dash" -import QtQuick.Layouts GridLayout { id: root diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index 0dc923cc3..0dae62467 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml index 29fd96455..285e92f8c 100644 --- a/modules/dashboard/LyricsView.qml +++ b/modules/dashboard/LyricsView.qml @@ -1,10 +1,10 @@ +import QtQuick +import QtQuick.Effects +import Quickshell import qs.components import qs.components.containers import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Effects StyledListView { id: root diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 8a8b6bdc5..490461851 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import Quickshell +import Quickshell.Services.Mpris +import Caelestia.Services import qs.components import qs.components.controls import qs.services -import qs.utils import qs.config -import Caelestia.Services -import Quickshell -import Quickshell.Services.Mpris -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes +import qs.utils Item { id: root diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 14108cad0..8d8d2c016 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -5,8 +5,8 @@ import Quickshell.Services.UPower import Caelestia.Internal import qs.components import qs.components.misc -import qs.config import qs.services +import qs.config Item { id: root diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 5ac454556..adeab928d 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Widgets import qs.components import qs.components.controls import qs.services import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Controls Item { id: root diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/Weather.qml index d15d1ed6f..a87ce29a1 100644 --- a/modules/dashboard/Weather.qml +++ b/modules/dashboard/Weather.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 3413f6e5a..596c21b75 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia import qs.components import qs.components.filedialog import qs.config import qs.utils -import Caelestia -import Quickshell -import QtQuick Item { id: root diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index bf64ac5f7..bed3c4d34 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts import qs.components -import qs.components.effects import qs.components.controls +import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts CustomMouseArea { id: root diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 90d2f0b6d..7f786a198 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 4451e5d7f..1a4f0c938 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,10 +1,10 @@ +import QtQuick +import QtQuick.Shapes +import Caelestia.Services import qs.components import qs.services import qs.config import qs.utils -import Caelestia.Services -import QtQuick -import QtQuick.Shapes Item { id: root diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index 7f44a9d0b..f5ac45569 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -1,8 +1,8 @@ +import QtQuick import qs.components import qs.components.misc import qs.services import qs.config -import QtQuick Row { id: root diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 1ae2ea2d0..5fb71effa 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -1,11 +1,11 @@ +import QtQuick import qs.components import qs.components.effects -import qs.components.images import qs.components.filedialog +import qs.components.images import qs.services import qs.config import qs.utils -import QtQuick Row { id: root diff --git a/modules/dashboard/dash/Weather.qml b/modules/dashboard/dash/Weather.qml index 766802f74..666a27010 100644 --- a/modules/dashboard/dash/Weather.qml +++ b/modules/dashboard/dash/Weather.qml @@ -1,7 +1,7 @@ +import QtQuick import qs.components import qs.services import qs.config -import QtQuick Item { id: root diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index 8810f50e2..b79cd9195 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -1,14 +1,14 @@ +import QtQuick +import QtQuick.Shapes import qs.config -import qs.modules.osd as Osd +import qs.modules.dashboard as Dashboard +import qs.modules.launcher as Launcher import qs.modules.notifications as Notifications +import qs.modules.osd as Osd import qs.modules.session as Session -import qs.modules.launcher as Launcher -import qs.modules.dashboard as Dashboard -import qs.modules.bar.popouts as BarPopouts -import qs.modules.utilities as Utilities import qs.modules.sidebar as Sidebar -import QtQuick -import QtQuick.Shapes +import qs.modules.utilities as Utilities +import qs.modules.bar.popouts as BarPopouts Shape { id: root diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml index 6fdd73bd7..13f92a4b2 100644 --- a/modules/drawers/Border.qml +++ b/modules/drawers/Border.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Effects import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Effects Item { id: root diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 73877feb3..864ad5c46 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland import qs.components import qs.components.containers import qs.services import qs.config import qs.utils import qs.modules.bar -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland -import QtQuick -import QtQuick.Controls -import QtQuick.Effects Variants { model: Screens.screens diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index 0ec1f8ef5..f43afb9a2 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components.containers import qs.config import qs.modules.bar as Bar -import Quickshell -import QtQuick Scope { id: root diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 780990447..fcb128a93 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -1,11 +1,11 @@ +import QtQuick +import QtQuick.Controls +import Quickshell import qs.components import qs.components.controls import qs.config import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts -import Quickshell -import QtQuick -import QtQuick.Controls CustomMouseArea { id: root diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 98313ae10..f2531cfa3 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -1,17 +1,17 @@ +import QtQuick +import Quickshell import qs.components import qs.config -import qs.modules.osd as Osd +import qs.modules.bar as Bar +import qs.modules.dashboard as Dashboard +import qs.modules.launcher as Launcher import qs.modules.notifications as Notifications +import qs.modules.osd as Osd import qs.modules.session as Session -import qs.modules.launcher as Launcher -import qs.modules.dashboard as Dashboard -import qs.modules.bar as Bar -import qs.modules.bar.popouts as BarPopouts +import qs.modules.sidebar as Sidebar import qs.modules.utilities as Utilities +import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities.toasts as Toasts -import qs.modules.sidebar as Sidebar -import Quickshell -import QtQuick Item { id: root diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 2c9d52fea..15d6e44a6 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import qs.modules.launcher.items -import qs.modules.launcher.services +import QtQuick +import Quickshell import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config -import Quickshell -import QtQuick +import qs.modules.launcher.items +import qs.modules.launcher.services StyledListView { id: root diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml index 709c7d035..508c75d4b 100644 --- a/modules/launcher/Background.qml +++ b/modules/launcher/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 40be12274..88ff73e6d 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import qs.modules.launcher.services +import QtQuick import qs.components import qs.components.controls import qs.services import qs.config -import QtQuick +import qs.modules.launcher.services Item { id: root diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 204f183b1..775b193fa 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick import qs.components import qs.components.controls import qs.services import qs.config import qs.utils -import QtQuick Item { id: root diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index 542a64e8a..ad03e9dec 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import "items" +import QtQuick +import Quickshell import qs.components.controls import qs.services import qs.config -import Quickshell -import QtQuick PathView { id: root diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index a69cc99e0..cc5e86c6a 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components import qs.config -import Quickshell -import QtQuick Item { id: root diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index ce540ffe9..a3ef00dda 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -1,7 +1,7 @@ +import QtQuick import qs.components import qs.services import qs.config -import QtQuick Item { id: root diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 812172b5a..0b64c3da0 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -1,11 +1,11 @@ -import qs.modules.launcher.services +import QtQuick +import Quickshell +import Quickshell.Widgets import qs.components import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import QtQuick +import qs.modules.launcher.services Item { id: root diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 4c27c5c7f..369b35ef6 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -1,10 +1,10 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia import qs.components import qs.services import qs.config -import Caelestia -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index c041b3258..6cc47aa48 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -1,8 +1,8 @@ -import qs.modules.launcher.services +import QtQuick import qs.components import qs.services import qs.config -import QtQuick +import qs.modules.launcher.services Item { id: root diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index 9554f5092..b63539b73 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -1,8 +1,8 @@ -import qs.modules.launcher.services +import QtQuick import qs.components import qs.services import qs.config -import QtQuick +import qs.modules.launcher.services Item { id: root diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 22e2bff28..beb93426a 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -1,10 +1,10 @@ +import QtQuick +import Caelestia.Models import qs.components import qs.components.effects import qs.components.images import qs.services import qs.config -import Caelestia.Models -import QtQuick Item { id: root diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 5c1cb6bb8..2a76012fe 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -1,11 +1,11 @@ pragma Singleton import ".." +import QtQuick +import Quickshell import qs.services import qs.config import qs.utils -import Quickshell -import QtQuick Searcher { id: root diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index 7f2d64556..1f1f35707 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -1,9 +1,9 @@ pragma Singleton +import Quickshell +import Caelestia import qs.config import qs.utils -import Caelestia -import Quickshell Searcher { id: root diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml index 963a4d435..a951b8c65 100644 --- a/modules/launcher/services/M3Variants.qml +++ b/modules/launcher/services/M3Variants.qml @@ -1,10 +1,10 @@ pragma Singleton import ".." +import QtQuick +import Quickshell import qs.config import qs.utils -import Quickshell -import QtQuick Searcher { id: root diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml index 646dd10a4..d389aa176 100644 --- a/modules/launcher/services/Schemes.qml +++ b/modules/launcher/services/Schemes.qml @@ -1,11 +1,11 @@ pragma Singleton import ".." -import qs.config -import qs.utils +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import qs.config +import qs.utils Searcher { id: root diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 24fb8d4d0..73b6e6705 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.images import qs.services import qs.config import qs.utils -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index a024ddc23..4427099fb 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts RowLayout { id: root diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index e3feb6934..ab6de7b6c 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.UPower import qs.components import qs.components.effects import qs.services import qs.config import qs.utils -import Quickshell.Services.UPower -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index 885487b90..1286267a7 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index 283906cbf..5d70266a0 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import qs.components.misc import Quickshell import Quickshell.Io import Quickshell.Wayland +import qs.components.misc Scope { property alias lock: lock diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 54fa1b943..46862f83f 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Effects +import Quickshell.Wayland import qs.components import qs.services import qs.config -import Quickshell.Wayland -import QtQuick -import QtQuick.Effects WlSessionLockSurface { id: root diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 12fe964f9..cabe60b9a 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.effects import qs.services import qs.config -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 9a4dde64d..62439d0c3 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 1c8483449..133dc6266 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index c7f05a7c3..8345ebc4d 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -1,9 +1,9 @@ -import qs.config +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Services.Pam -import QtQuick +import qs.config Scope { id: root diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 82c004c22..33c0e4c00 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -1,10 +1,10 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.components.misc import qs.services import qs.config -import QtQuick -import QtQuick.Layouts GridLayout { id: root diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index edabff57e..213bf155d 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml index a44cb19b7..4d7a5ff76 100644 --- a/modules/notifications/Background.qml +++ b/modules/notifications/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 96f09b28b..bd18276ef 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,11 +1,11 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.widgets import qs.services import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick Item { id: root diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index faced0827..7b580a74f 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes StyledRect { id: root diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index accc9d71d..2b581e93e 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -1,6 +1,6 @@ +import QtQuick import qs.components import qs.config -import QtQuick Item { id: root diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml index 78955c7a8..a609f4601 100644 --- a/modules/osd/Background.qml +++ b/modules/osd/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index bcc89fdf8..5ec6ad983 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config import qs.utils -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 5b192772b..939b57de1 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components import qs.services import qs.config -import Quickshell -import QtQuick Item { id: root diff --git a/modules/session/Background.qml b/modules/session/Background.qml index 78955c7a8..a609f4601 100644 --- a/modules/session/Background.qml +++ b/modules/session/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 722f08047..405fce1ce 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components import qs.services import qs.config import qs.utils -import Quickshell -import QtQuick Column { id: root diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index a4a16ce82..2924f776f 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound +import QtQuick import qs.components import qs.config -import QtQuick Item { id: root diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml index beefdf5c4..4cc142628 100644 --- a/modules/sidebar/Background.qml +++ b/modules/sidebar/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index e09dd6a9a..7dbbd06c3 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 01593f2b5..fe56798f7 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell import qs.components import qs.services import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 472152aef..370a79cbc 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.effects import qs.services import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index daa1dfda1..f041cc9be 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 8ab76a578..7ffb64182 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components import qs.services import qs.config -import Quickshell -import QtQuick Item { id: root diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index da3f2b80d..f1d1502f9 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services import qs.config import qs.utils -import Quickshell -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index a01cfb0a4..fe7510b55 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config -import qs.services -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import qs.components +import qs.services +import qs.config Item { id: root diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index d38de3e63..ad2564131 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound +import QtQuick import qs.components import qs.config -import QtQuick Item { id: root diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml index fbce89616..975461a58 100644 --- a/modules/utilities/Background.qml +++ b/modules/utilities/Background.qml @@ -1,8 +1,8 @@ +import QtQuick +import QtQuick.Shapes import qs.components import qs.services import qs.config -import QtQuick -import QtQuick.Shapes ShapePath { id: root diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 507debc0c..6dcfedcdf 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -1,9 +1,9 @@ import "cards" +import QtQuick +import QtQuick.Layouts import qs.components import qs.config import qs.modules.bar.popouts as BarPopouts -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 5bb49b118..4d6d8af2d 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import Caelestia import qs.components import qs.components.controls import qs.components.effects import qs.services import qs.config -import Caelestia -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes Loader { id: root diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 329e85eb2..66a616f07 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell import qs.components import qs.config import qs.modules.bar.popouts as BarPopouts -import Quickshell -import QtQuick Item { id: root diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index cacf4c7c3..20d232a37 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -1,9 +1,9 @@ +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index b8653159c..9dd16ec51 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts import qs.components import qs.components.controls import qs.services import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index af006cf97..7fb56f3ba 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Models import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services import qs.config import qs.utils -import Caelestia.Models -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 03aa406f8..996b027c3 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services import qs.config import qs.modules.bar.popouts as BarPopouts -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index f47550006..52473a48e 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -1,10 +1,10 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia import qs.components import qs.components.effects import qs.services import qs.config -import Caelestia -import QtQuick -import QtQuick.Layouts StyledRect { id: root diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 2915404e0..ac8772f57 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia import qs.components import qs.config -import Caelestia -import Quickshell -import QtQuick Item { id: root diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 101405f79..15f71956c 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets import qs.components import qs.services import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index f9ee66a68..3934a993d 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -1,9 +1,9 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Hyprland import qs.components import qs.services import qs.config -import Quickshell.Hyprland -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 4e46af86f..2683e5447 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland import qs.components import qs.services import qs.config -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index 919b3fbb1..d7cfc2fa1 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -1,10 +1,10 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland import qs.components import qs.services import qs.config -import Quickshell -import Quickshell.Hyprland -import QtQuick -import QtQuick.Layouts Item { id: root diff --git a/services/Audio.qml b/services/Audio.qml index 515e93a40..100f00101 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -1,11 +1,11 @@ pragma Singleton -import qs.config -import Caelestia.Services -import Caelestia +import QtQuick import Quickshell import Quickshell.Services.Pipewire -import QtQuick +import Caelestia +import Caelestia.Services +import qs.config Singleton { id: root diff --git a/services/Brightness.qml b/services/Brightness.qml index a41a0d7a5..6e828d918 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -1,11 +1,11 @@ pragma Singleton pragma ComponentBehavior: Bound -import qs.config -import qs.components.misc +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import qs.components.misc +import qs.config Singleton { id: root diff --git a/services/Colours.qml b/services/Colours.qml index 09ec669a7..469118167 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -1,13 +1,13 @@ pragma Singleton pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Io +import Caelestia import qs.services import qs.config import qs.utils -import Caelestia -import Quickshell -import Quickshell.Io -import QtQuick Singleton { id: root diff --git a/services/GameMode.qml b/services/GameMode.qml index ee0e6a99e..3a3fb7bda 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -1,11 +1,11 @@ pragma Singleton -import qs.services -import qs.config -import Caelestia +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia +import qs.services +import qs.config Singleton { id: root diff --git a/services/Hypr.qml b/services/Hypr.qml index 9690e788b..98c6e7186 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -1,13 +1,13 @@ pragma Singleton -import qs.components.misc -import qs.config -import Caelestia -import Caelestia.Internal +import QtQuick import Quickshell import Quickshell.Hyprland import Quickshell.Io -import QtQuick +import Caelestia +import Caelestia.Internal +import qs.components.misc +import qs.config Singleton { id: root diff --git a/services/LyricsService.qml b/services/LyricsService.qml index 213a69a9e..f96dde918 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -1,12 +1,12 @@ pragma Singleton -import qs.config -import qs.utils -import Caelestia +import "../utils/scripts/lrcparser.js" as Lrc import QtQuick import Quickshell import Quickshell.Io -import "../utils/scripts/lrcparser.js" as Lrc +import Caelestia +import qs.config +import qs.utils Singleton { id: root diff --git a/services/Network.qml b/services/Network.qml index b32f84a21..cef0def81 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -1,8 +1,8 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Io -import QtQuick import qs.services Singleton { diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 55c0c6edf..1f74c97bf 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -1,13 +1,10 @@ pragma Singleton -import qs.config - +import QtQuick import Quickshell import Quickshell.Io - import Caelestia.Internal - -import QtQuick +import qs.config Singleton { id: root diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 212acce82..c3c05360d 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1,9 +1,9 @@ pragma Singleton pragma ComponentBehavior: Bound +import QtQuick import Quickshell import Quickshell.Io -import QtQuick Singleton { id: root diff --git a/services/NotifData.qml b/services/NotifData.qml index 7ad024e7a..3fde0145a 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Services.Notifications +import Caelestia import qs.services import qs.config import qs.utils -import Caelestia -import Quickshell -import Quickshell.Services.Notifications -import QtQuick QtObject { id: notif diff --git a/services/Notifs.qml b/services/Notifs.qml index 1fa1a158b..4e6c0dd6b 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -1,15 +1,15 @@ pragma Singleton pragma ComponentBehavior: Bound -import qs.components.misc -import qs.config -import qs.services -import qs.utils -import Caelestia +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Notifications -import QtQuick +import Caelestia +import qs.components.misc +import qs.services +import qs.config +import qs.utils Singleton { id: root diff --git a/services/Players.qml b/services/Players.qml index 5317317ff..d51b5df38 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -1,12 +1,12 @@ pragma Singleton -import qs.components.misc -import qs.config +import QtQml import Quickshell import Quickshell.Io import Quickshell.Services.Mpris -import QtQml import Caelestia +import qs.components.misc +import qs.config Singleton { id: root diff --git a/services/Recorder.qml b/services/Recorder.qml index ff8a7d10d..8ad00145e 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -1,8 +1,8 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Io -import QtQuick Singleton { id: root diff --git a/services/Screens.qml b/services/Screens.qml index a64751785..9fed887f3 100644 --- a/services/Screens.qml +++ b/services/Screens.qml @@ -1,8 +1,8 @@ pragma Singleton +import Quickshell import qs.config import qs.utils -import Quickshell Singleton { id: root diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 413ad7654..79691fead 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -1,9 +1,9 @@ pragma Singleton -import qs.config +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import qs.config Singleton { id: root diff --git a/services/Time.qml b/services/Time.qml index d918d0b6e..d41c9142c 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -1,8 +1,8 @@ pragma Singleton -import qs.config -import Quickshell import QtQuick +import Quickshell +import qs.config Singleton { property alias enabled: clock.enabled diff --git a/services/VPN.qml b/services/VPN.qml index 297d76fff..2b25813b2 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -1,10 +1,10 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Io -import QtQuick -import qs.config import Caelestia +import qs.config Singleton { id: root diff --git a/services/Visibilities.qml b/services/Visibilities.qml index 4305f5795..391870502 100644 --- a/services/Visibilities.qml +++ b/services/Visibilities.qml @@ -1,8 +1,8 @@ pragma Singleton +import Quickshell import qs.components import qs.services -import Quickshell Singleton { property var screens: new Map() diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 18310eaf8..602abb205 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -1,12 +1,12 @@ pragma Singleton +import QtQuick +import Quickshell +import Quickshell.Io +import Caelestia.Models import qs.services import qs.config import qs.utils -import Caelestia.Models -import Quickshell -import Quickshell.Io -import QtQuick Searcher { id: root diff --git a/services/Weather.qml b/services/Weather.qml index 36e69265f..d226365fd 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -1,10 +1,10 @@ pragma Singleton +import QtQuick +import Quickshell +import Caelestia import qs.config import qs.utils -import Caelestia -import Quickshell -import QtQuick Singleton { id: root diff --git a/utils/Icons.qml b/utils/Icons.qml index 34f8049bf..8a62f62d7 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -1,9 +1,9 @@ pragma Singleton -import qs.config +import QtQuick import Quickshell import Quickshell.Services.Notifications -import QtQuick +import qs.config Singleton { id: root diff --git a/utils/NetworkConnection.qml b/utils/NetworkConnection.qml index e55b87bc4..8331813d9 100644 --- a/utils/NetworkConnection.qml +++ b/utils/NetworkConnection.qml @@ -1,7 +1,7 @@ pragma Singleton -import qs.services import QtQuick +import qs.services /** * NetworkConnection diff --git a/utils/Paths.qml b/utils/Paths.qml index 3a0e4ee2c..b0926348d 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -1,9 +1,9 @@ pragma Singleton -import qs.config -import Caelestia -import Quickshell import QtQuick +import Quickshell +import Caelestia +import qs.config Singleton { id: root diff --git a/utils/Searcher.qml b/utils/Searcher.qml index 053b73bba..102c9e766 100644 --- a/utils/Searcher.qml +++ b/utils/Searcher.qml @@ -1,8 +1,7 @@ -import Quickshell - import "scripts/fzf.js" as Fzf import "scripts/fuzzysort.js" as Fuzzy import QtQuick +import Quickshell Singleton { required property list list diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index aaa1ad31d..74c94e9bc 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -1,10 +1,10 @@ pragma Singleton -import qs.config -import qs.utils +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import qs.config +import qs.utils Singleton { id: root From 95992d85d6c325b7641e7a97c2ea31c282ea646f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 06:09:17 +1100 Subject: [PATCH 148/409] feat: also ensure correct file structure --- scripts/qml-lint-conventions.py | 120 ++++++++++++++++++++++++++++++++ 1 file changed, 120 insertions(+) diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py index a191d1a11..cef46657c 100755 --- a/scripts/qml-lint-conventions.py +++ b/scripts/qml-lint-conventions.py @@ -50,6 +50,7 @@ class Section(IntEnum): } RULE_COLOURS = { + "file-structure": RED, "import-order": GREEN, "section-order": YELLOW, "missing-section-separator": CYAN, @@ -175,6 +176,123 @@ def fix_imports(lines: list[str]) -> list[str]: return lines[:first] + sorted_imports + lines[last + 1 :] +def check_file_structure(lines: list[str], rel: str) -> list[Violation]: + """Check file-level structure: pragmas, then imports, then content.""" + violations = [] + pragma_indices: list[int] = [] + import_indices: list[int] = [] + content_start: int | None = None + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("pragma "): + pragma_indices.append(i) + elif IMPORT_RE.match(stripped): + import_indices.append(i) + else: + content_start = i + break + + # Pragmas must come before imports + if pragma_indices and import_indices: + if pragma_indices[-1] > import_indices[0]: + violations.append( + Violation(rel, pragma_indices[-1] + 1, "file-structure", "pragmas should appear before imports") + ) + + # Separator between pragmas and imports + if pragma_indices and import_indices: + gap = import_indices[0] - pragma_indices[-1] - 1 + if gap == 0: + violations.append( + Violation( + rel, import_indices[0] + 1, "file-structure", "blank line expected between pragmas and imports" + ) + ) + elif gap > 1: + violations.append( + Violation( + rel, + pragma_indices[-1] + 3, + "file-structure", + "only one blank line expected between pragmas and imports", + ) + ) + + # No blank lines within imports + for j in range(1, len(import_indices)): + if import_indices[j] != import_indices[j - 1] + 1: + for gap_line in range(import_indices[j - 1] + 1, import_indices[j]): + if not lines[gap_line].strip(): + violations.append( + Violation(rel, gap_line + 1, "file-structure", "no blank lines expected within imports") + ) + + # Separator between imports/pragmas and content + last_header = import_indices[-1] if import_indices else (pragma_indices[-1] if pragma_indices else None) + if last_header is not None and content_start is not None: + gap = content_start - last_header - 1 + label = "imports" if import_indices else "pragmas" + if gap == 0: + violations.append( + Violation( + rel, content_start + 1, "file-structure", f"blank line expected between {label} and content" + ) + ) + elif gap > 1: + violations.append( + Violation( + rel, + last_header + 3, + "file-structure", + f"only one blank line expected between {label} and content", + ) + ) + + return violations + + +def fix_file_structure(lines: list[str]) -> list[str]: + """Ensure correct file structure: pragmas, blank, imports (no gaps), blank, content.""" + pragmas: list[str] = [] + imports: list[str] = [] + content_start: int | None = None + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("pragma "): + pragmas.append(line) + elif IMPORT_RE.match(stripped): + imports.append(line) + else: + content_start = i + break + + if content_start is None: + content_start = len(lines) + + # Skip any blank lines at the start of content + while content_start < len(lines) and not lines[content_start].strip(): + content_start += 1 + + result: list[str] = [] + has_content = content_start < len(lines) + if pragmas: + result.extend(pragmas) + if imports or has_content: + result.append("") + if imports: + result.extend(imports) + if has_content: + result.append("") + result.extend(lines[content_start:]) + return result + + def fix_section_separators(lines: list[str]) -> list[str]: """Insert blank lines between different sections and return modified lines.""" insertions: list[int] = [] @@ -265,6 +383,7 @@ def fix_file(filepath: Path) -> bool: lines = text.splitlines() lines = fix_imports(lines) + lines = fix_file_structure(lines) lines = fix_section_separators(lines) new_text = "\n".join(lines) if text.endswith("\n"): @@ -358,6 +477,7 @@ def check_file(filepath: Path) -> list[Violation]: except (OSError, UnicodeDecodeError): return violations + violations.extend(check_file_structure(lines, rel)) violations.extend(check_imports(filepath, lines, rel)) scopes: dict[str, ScopeTracker] = {} # indent -> tracker From f46764fd5dbe40890f0a4f83c0186fb879521d62 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 15:38:51 +1100 Subject: [PATCH 149/409] fix: Weather name shadowing --- modules/dashboard/Content.qml | 2 +- modules/dashboard/Dash.qml | 2 +- modules/dashboard/{Weather.qml => WeatherTab.qml} | 0 modules/dashboard/dash/{Weather.qml => SmallWeather.qml} | 0 4 files changed, 2 insertions(+), 2 deletions(-) rename modules/dashboard/{Weather.qml => WeatherTab.qml} (100%) rename modules/dashboard/dash/{Weather.qml => SmallWeather.qml} (100%) diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 4a6d9e61a..5b3bbbe6b 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -188,7 +188,7 @@ Item { Component { id: weatherComponent - Weather {} + WeatherTab {} } Behavior on contentX { diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 13073c14d..c0657f58e 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -40,7 +40,7 @@ GridLayout { radius: Appearance.rounding.large * 1.5 - Weather {} + SmallWeather {} } Rect { diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/WeatherTab.qml similarity index 100% rename from modules/dashboard/Weather.qml rename to modules/dashboard/WeatherTab.qml diff --git a/modules/dashboard/dash/Weather.qml b/modules/dashboard/dash/SmallWeather.qml similarity index 100% rename from modules/dashboard/dash/Weather.qml rename to modules/dashboard/dash/SmallWeather.qml From 25515fd32b84ab7320bac305a7c050933747c8c3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 22 Mar 2026 16:08:57 +1100 Subject: [PATCH 150/409] chore: fix rest of formatting issues --- components/controls/StyledInputField.qml | 14 +- config/LauncherConfig.qml | 30 +- config/UtilitiesConfig.qml | 50 +- .../bar/components/workspaces/OccupiedBg.qml | 10 +- .../workspaces/SpecialWorkspaces.qml | 140 ++--- modules/bar/popouts/TrayMenu.qml | 12 +- modules/bar/popouts/WirelessPassword.qml | 14 +- .../bar/popouts/kblayout/KbLayoutModel.qml | 20 +- .../sections/ColorSchemeSection.qml | 3 +- .../sections/ColorVariantSection.qml | 3 +- .../appearance/sections/FontsSection.qml | 9 +- modules/controlcenter/audio/AudioPane.qml | 66 +-- .../controlcenter/components/DeviceList.qml | 3 +- .../controlcenter/components/SliderInput.qml | 16 +- .../components/SplitPaneWithDetails.qml | 10 +- .../components/WallpaperGrid.qml | 16 +- .../controlcenter/launcher/LauncherPane.qml | 8 +- modules/controlcenter/network/VpnList.qml | 3 +- modules/controlcenter/network/VpnSettings.qml | 3 +- .../controlcenter/network/WirelessList.qml | 3 +- .../network/WirelessPasswordDialog.qml | 3 +- services/LyricsService.qml | 181 +++--- services/Network.qml | 12 +- services/Nmcli.qml | 550 +++++++++--------- 24 files changed, 582 insertions(+), 597 deletions(-) diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index 9c3390fe3..f56f79fb3 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -61,13 +61,6 @@ Item { readOnly: root.readOnly enabled: root.enabled - Binding { - target: inputField - property: "text" - value: root.text - when: !inputField.activeFocus - } - onTextChanged: { root.text = text; root.textEdited(text); @@ -76,6 +69,13 @@ Item { onEditingFinished: { root.editingFinished(); } + + Binding { + target: inputField + property: "text" + value: root.text + when: !inputField.activeFocus + } } } } diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml index d9e3a7384..05088af05 100644 --- a/config/LauncherConfig.qml +++ b/config/LauncherConfig.qml @@ -15,21 +15,6 @@ JsonObject { property UseFuzzy useFuzzy: UseFuzzy {} property Sizes sizes: Sizes {} - component UseFuzzy: JsonObject { - property bool apps: false - property bool actions: false - property bool schemes: false - property bool variants: false - property bool wallpapers: false - } - - component Sizes: JsonObject { - property int itemWidth: 600 - property int itemHeight: 57 - property int wallpaperWidth: 280 - property int wallpaperHeight: 200 - } - property list actions: [ { name: "Calculator", @@ -144,4 +129,19 @@ JsonObject { dangerous: false } ] + + component UseFuzzy: JsonObject { + property bool apps: false + property bool actions: false + property bool schemes: false + property bool variants: false + property bool wallpapers: false + } + + component Sizes: JsonObject { + property int itemWidth: 600 + property int itemHeight: 57 + property int wallpaperWidth: 280 + property int wallpaperHeight: 200 + } } diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index 4d1dd6e8d..017ab4ad8 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -8,31 +8,6 @@ JsonObject { property Toasts toasts: Toasts {} property Vpn vpn: Vpn {} - component Sizes: JsonObject { - property int width: 430 - property int toastWidth: 430 - } - - component Toasts: JsonObject { - property bool configLoaded: true - property bool chargingChanged: true - property bool gameModeChanged: true - property bool dndChanged: true - property bool audioOutputChanged: true - property bool audioInputChanged: true - property bool capsLockChanged: true - property bool numLockChanged: true - property bool kbLayoutChanged: true - property bool kbLimit: true - property bool vpnChanged: true - property bool nowPlaying: false - } - - component Vpn: JsonObject { - property bool enabled: false - property list provider: ["netbird"] - } - property list quickToggles: [ { id: "wifi", @@ -63,4 +38,29 @@ JsonObject { enabled: false } ] + + component Sizes: JsonObject { + property int width: 430 + property int toastWidth: 430 + } + + component Toasts: JsonObject { + property bool configLoaded: true + property bool chargingChanged: true + property bool gameModeChanged: true + property bool dndChanged: true + property bool audioOutputChanged: true + property bool audioInputChanged: true + property bool capsLockChanged: true + property bool numLockChanged: true + property bool kbLayoutChanged: true + property bool kbLimit: true + property bool vpnChanged: true + property bool nowPlaying: false + } + + component Vpn: JsonObject { + property bool enabled: false + property list provider: ["netbird"] + } } diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 0ed1831fa..3f0871603 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -90,14 +90,14 @@ Item { } } - component Pill: QtObject { - property int start - property int end - } - Component { id: pillComp Pill {} } + + component Pill: QtObject { + property int start + property int end + } } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index e62ed7190..cef8a9906 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -159,6 +159,76 @@ Item { } } + Loader { + asynchronous: true + active: Config.bar.workspaces.activeIndicator + anchors.fill: parent + + sourceComponent: Item { + StyledClippingRect { + id: indicator + + anchors.left: parent.left + anchors.right: parent.right + + y: (view.currentItem?.y ?? 0) - view.contentY + implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 + + color: Colours.palette.m3tertiary + radius: Appearance.rounding.full + + Colouriser { + source: view + sourceColor: Colours.palette.m3onSurface + colorizationColor: Colours.palette.m3onTertiary + + anchors.horizontalCenter: parent.horizontalCenter + + x: 0 + y: -indicator.y + implicitWidth: view.width + implicitHeight: view.height + } + + Behavior on y { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + + Behavior on implicitHeight { + Anim { + easing.bezierCurve: Appearance.anim.curves.emphasized + } + } + } + } + } + + MouseArea { + property real startY + + anchors.fill: view + + drag.target: view.contentItem + drag.axis: Drag.YAxis + drag.maximumY: 0 + drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) + + onPressed: event => startY = event.y + + onClicked: event => { + if (Math.abs(event.y - startY) > drag.threshold) + return; + + const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; + if (ws?.modelData) + Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); + else + Hypr.dispatch("togglespecialworkspace special"); + } + } + component SpecialWsDelegate: ColumnLayout { id: ws @@ -297,74 +367,4 @@ Item { } } } - - Loader { - asynchronous: true - active: Config.bar.workspaces.activeIndicator - anchors.fill: parent - - sourceComponent: Item { - StyledClippingRect { - id: indicator - - anchors.left: parent.left - anchors.right: parent.right - - y: (view.currentItem?.y ?? 0) - view.contentY - implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 - - color: Colours.palette.m3tertiary - radius: Appearance.rounding.full - - Colouriser { - source: view - sourceColor: Colours.palette.m3onSurface - colorizationColor: Colours.palette.m3onTertiary - - anchors.horizontalCenter: parent.horizontalCenter - - x: 0 - y: -indicator.y - implicitWidth: view.width - implicitHeight: view.height - } - - Behavior on y { - Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - - Behavior on implicitHeight { - Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - } - } - } - - MouseArea { - property real startY - - anchors.fill: view - - drag.target: view.contentItem - drag.axis: Drag.YAxis - drag.maximumY: 0 - drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) - - onPressed: event => startY = event.y - - onClicked: event => { - if (Math.abs(event.y - startY) > drag.threshold) - return; - - const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; - if (ws?.modelData) - Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); - else - Hypr.dispatch("togglespecialworkspace special"); - } - } } diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 50bd60a50..4347e39e1 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -26,6 +26,12 @@ StackView { popEnter: NoAnim {} popExit: NoAnim {} + Component { + id: subMenuComp + + SubMenu {} + } + component NoAnim: Transition { NumberAnimation { duration: 0 @@ -221,10 +227,4 @@ StackView { } } } - - Component { - id: subMenuComp - - SubMenu {} - } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 4d439cc57..e80e9148e 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -267,6 +267,13 @@ ColumnLayout { focus: true activeFocusOnTab: true + Component.onCompleted: { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + } + } + Keys.onPressed: event => { // Ensure we have focus when receiving keyboard input if (!activeFocus) { @@ -318,13 +325,6 @@ ColumnLayout { } } - Component.onCompleted: { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - } - } - StyledRect { anchors.fill: parent radius: Appearance.rounding.normal diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 65c809b3d..4caebbd29 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -10,17 +10,11 @@ import qs.config Item { id: model - visible: false - - ListModel { - id: _visibleModel - } - property alias visibleModel: _visibleModel - property string activeLabel: "" - property int activeIndex: -1 + property var _xkbMap: ({}) + property bool _notifiedLimit: false function start() { _xkbXmlBase.running = true; @@ -127,13 +121,15 @@ Item { return code.toUpperCase() + " - " + code; } + visible: false + ListModel { - id: _layoutsModel + id: _visibleModel } - property var _xkbMap: ({}) - - property bool _notifiedLimit: false + ListModel { + id: _layoutsModel + } Process { id: _xkbXmlBase diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 0e96de70f..b954cd458 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -35,6 +35,7 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -140,8 +141,6 @@ CollapsibleSection { } } } - - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index 0977c2bbe..b3cc4cfba 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -32,6 +32,7 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -84,8 +85,6 @@ CollapsibleSection { font.pointSize: Appearance.font.size.large } } - - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index dc42f3b9e..47b738f19 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -55,6 +55,7 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -93,8 +94,6 @@ CollapsibleSection { } } } - - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -137,6 +136,7 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -175,8 +175,6 @@ CollapsibleSection { } } } - - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -221,6 +219,7 @@ CollapsibleSection { radius: Appearance.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -259,8 +258,6 @@ CollapsibleSection { } } } - - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index fe03d8b37..172132f6b 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -94,6 +94,7 @@ Item { color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal + implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -126,8 +127,6 @@ Item { font.weight: Audio.sink?.id === modelData.id ? 500 : 400 } } - - implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -172,6 +171,7 @@ Item { color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" radius: Appearance.rounding.normal + implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 StateLayer { function onClicked(): void { @@ -204,8 +204,6 @@ Item { font.weight: Audio.source?.id === modelData.id ? 500 : 400 } } - - implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -278,16 +276,6 @@ Item { text = Math.round(Audio.volume * 100).toString(); } - Connections { - function onVolumeChanged() { - if (!outputVolumeInput.hasFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); - } - } - - target: Audio - } - onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -303,6 +291,16 @@ Item { text = Math.round(Audio.volume * 100).toString(); } } + + Connections { + function onVolumeChanged() { + if (!outputVolumeInput.hasFocus) { + outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + } + } + + target: Audio + } } StyledText { @@ -396,16 +394,6 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } - Connections { - function onSourceVolumeChanged() { - if (!inputVolumeInput.hasFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); - } - } - - target: Audio - } - onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -421,6 +409,16 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } } + + Connections { + function onSourceVolumeChanged() { + if (!inputVolumeInput.hasFocus) { + inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + } + } + + target: Audio + } } StyledText { @@ -530,16 +528,6 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } - Connections { - function onAudioChanged() { - if (!streamVolumeInput.hasFocus && modelData?.audio) { - streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); - } - } - - target: modelData - } - onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -555,6 +543,16 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } } + + Connections { + function onAudioChanged() { + if (!streamVolumeInput.hasFocus && modelData?.audio) { + streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); + } + } + + target: modelData + } } StyledText { diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index f121eefe2..2134d8cfe 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -23,6 +23,7 @@ ColumnLayout { property Component headerComponent: null property Component titleSuffix: null property bool showHeader: true + property alias view: view signal itemSelected(var item) @@ -61,8 +62,6 @@ ColumnLayout { } } - property alias view: view - StyledText { visible: root.description !== "" Layout.fillWidth: true diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index d677fc303..df3acf41e 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -152,14 +152,6 @@ ColumnLayout { to: root.to stepSize: root.stepSize - // Use Binding to allow slider to move freely during dragging - Binding { - target: slider - property: "value" - value: root.value - when: !slider.pressed - } - onValueChanged: { // Update input field text in real-time as slider moves during dragging // Always update when slider value changes (during dragging or external updates) @@ -176,5 +168,13 @@ ColumnLayout { inputField.text = root.formatValue(newValue); } } + + // Use Binding to allow slider to move freely during dragging + Binding { + target: slider + property: "value" + value: root.value + when: !slider.pressed + } } } diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index 92d9c2c84..e8dcb269c 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -48,6 +48,11 @@ Item { nextComponent = targetComponent; } + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = root.paneIdGenerator(pane); + } + Loader { id: rightLoader @@ -74,11 +79,6 @@ Item { ] } } - - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = root.paneIdGenerator(pane); - } } } } diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 674886a7f..6a65c8e15 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -154,16 +154,16 @@ GridView { opacity: 0 + Component.onCompleted: { + opacity = 1; + } + Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } - - Component.onCompleted: { - opacity = 1; - } } } @@ -219,16 +219,16 @@ GridView { opacity: 0 + Component.onCompleted: { + opacity = 1; + } + Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } - - Component.onCompleted: { - opacity = 1; - } } } } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 4703cace4..677cf4182 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -317,6 +317,10 @@ Item { opacity: 0 + Component.onCompleted: { + opacity = 1; + } + Behavior on opacity { NumberAnimation { duration: 1000 @@ -324,10 +328,6 @@ Item { } } - Component.onCompleted: { - opacity = 1; - } - StateLayer { function onClicked(): void { root.session.launcher.active = modelData; diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 7f0bf3b18..3646841ce 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -98,6 +98,7 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -258,8 +259,6 @@ ColumnLayout { } } } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index d335b6588..ae689050e 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -82,6 +82,7 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined + implicitHeight: 60 color: Colours.tPalette.m3surfaceContainerHigh radius: Appearance.rounding.normal @@ -147,8 +148,6 @@ ColumnLayout { } } } - - implicitHeight: 60 } } } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index b1738f76a..bff142d3a 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -110,6 +110,7 @@ DeviceList { required property var modelData width: ListView.view ? ListView.view.width : undefined + implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) radius: Appearance.rounding.normal @@ -214,8 +215,6 @@ DeviceList { } } } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 0c2cb7143..40711730a 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -105,6 +105,7 @@ Item { color: Colours.tPalette.m3surface opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 + Keys.onEscapePressed: closeDialog() Behavior on opacity { Anim {} @@ -135,8 +136,6 @@ Item { } } - Keys.onEscapePressed: closeDialog() - ColumnLayout { id: content diff --git a/services/LyricsService.qml b/services/LyricsService.qml index f96dde918..53be9f90a 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -38,96 +38,6 @@ Singleton { "Referer": "https://music.163.com/" }) - ListModel { - id: lyricsModel - } - ListModel { - id: fetchedCandidatesModel - } - - Timer { - id: seekTimer - - interval: 500 - onTriggered: root.isManualSeeking = false - } - - // If no local lyrics were loaded within the interval, fall back to NetEase - Timer { - id: fallbackTimer - - interval: 200 - onTriggered: { - if (lyricsModel.count === 0) { - root.backend = "NetEase"; - fallbackToOnline(); - } - } - } - - Timer { - id: loadDebounce - - interval: 50 - onTriggered: root._doLoadLyrics() - } - - FileView { - id: lyricsMapFileView - - path: root.lyricsMapFile - printErrors: false - onLoaded: { - try { - root.lyricsMap = JSON.parse(text()); - } catch (e) { - root.lyricsMap = {}; - } - } - } - - FileView { - id: lrcFile - - printErrors: false - onLoaded: { - fallbackTimer.stop(); - let parsed = Lrc.parseLrc(text()); - if (parsed.length > 0) { - root.backend = "Local"; - updateModel(parsed); - loading = false; - } else { - root.backend = "NetEase"; - fallbackToOnline(); - } - } - } - - Connections { - function onActiveChanged() { - root.player = Players.active; - loadLyrics(); - } - - target: Players - } - - Connections { - function onMetadataChanged() { - loadLyrics(); - } - - target: root.player - ignoreUnknownSignals: true - } - - Process { - id: saveLyricsMap - - command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] - } - function getMetadata() { if (!player || !player.metadata) return null; @@ -345,4 +255,95 @@ Singleton { seekTimer.restart(); } + + ListModel { + id: lyricsModel + } + + ListModel { + id: fetchedCandidatesModel + } + + Timer { + id: seekTimer + + interval: 500 + onTriggered: root.isManualSeeking = false + } + + // If no local lyrics were loaded within the interval, fall back to NetEase + Timer { + id: fallbackTimer + + interval: 200 + onTriggered: { + if (lyricsModel.count === 0) { + root.backend = "NetEase"; + fallbackToOnline(); + } + } + } + + Timer { + id: loadDebounce + + interval: 50 + onTriggered: root._doLoadLyrics() + } + + FileView { + id: lyricsMapFileView + + path: root.lyricsMapFile + printErrors: false + onLoaded: { + try { + root.lyricsMap = JSON.parse(text()); + } catch (e) { + root.lyricsMap = {}; + } + } + } + + FileView { + id: lrcFile + + printErrors: false + onLoaded: { + fallbackTimer.stop(); + let parsed = Lrc.parseLrc(text()); + if (parsed.length > 0) { + root.backend = "Local"; + updateModel(parsed); + loading = false; + } else { + root.backend = "NetEase"; + fallbackToOnline(); + } + } + } + + Connections { + function onActiveChanged() { + root.player = Players.active; + loadLyrics(); + } + + target: Players + } + + Connections { + function onMetadataChanged() { + loadLyrics(); + } + + target: root.player + ignoreUnknownSignals: true + } + + Process { + id: saveLyricsMap + + command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] + } } diff --git a/services/Network.qml b/services/Network.qml index cef0def81..4e0b809ca 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -313,6 +313,12 @@ Singleton { } } + Component { + id: apComp + + AccessPoint {} + } + component AccessPoint: QtObject { required property var lastIpcObject readonly property string ssid: lastIpcObject.ssid @@ -323,10 +329,4 @@ Singleton { readonly property string security: lastIpcObject.security readonly property bool isSecure: security.length > 0 } - - Component { - id: apComp - - AccessPoint {} - } } diff --git a/services/Nmcli.qml b/services/Nmcli.qml index c3c05360d..7af9513bb 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -872,247 +872,6 @@ Singleton { return false; } - component CommandProcess: Process { - id: proc - - property var callback: null - property list command: [] - property bool callbackCalled: false - property int exitCode: 0 - - signal processFinished - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - - stdout: StdioCollector { - id: stdoutCollector - } - - stderr: StdioCollector { - id: stderrCollector - - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - root.handlePasswordRequired(proc, error, output, -1); - } - } - } - - onExited: code => { // qmllint disable signal-handler-parameters - exitCode = code; - - Qt.callLater(() => { - if (callbackCalled) { - processFinished(); - return; - } - - if (proc.callback) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; - const success = exitCode === 0; - const cmdIsConnection = isConnectionCommand(proc.command); - - if (root.handlePasswordRequired(proc, error, output, exitCode)) { - processFinished(); - return; - } - - const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); - - if (!success && cmdIsConnection && root.pendingConnection) { - const failedSsid = root.pendingConnection.ssid; - root.connectionFailed(failedSsid); - } - - callbackCalled = true; - callback({ - success: success, - output: output, - error: error, - exitCode: proc.exitCode, - needsPassword: needsPassword || false - }); - processFinished(); - } else { - processFinished(); - } - }); - } - } - - Component { - id: commandProc - - CommandProcess {} - } - - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } - - Component { - id: apComp - - AccessPoint {} - } - - Timer { - id: connectionCheckTimer - - interval: 4000 - onTriggered: { - if (root.pendingConnection) { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (!connected && root.pendingConnection.callback) { - let foundPasswordError = false; - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection) { - const pending = root.pendingConnection; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - foundPasswordError = true; - break; - } - } - } - } - } - - if (!foundPasswordError) { - const pending = root.pendingConnection; - const failedSsid = pending.ssid; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.connectionFailed(failedSsid); - pending.callback({ - success: false, - output: "", - error: "Connection timeout", - exitCode: -1, - needsPassword: false - }); - } - } else if (connected) { - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - } - - Timer { - id: immediateCheckTimer - - property int checkCount: 0 - - interval: 500 - repeat: true - triggeredOnStart: false - - onTriggered: { - if (root.pendingConnection) { - checkCount++; - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (connected) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - if (root.pendingConnection.callback) { - root.pendingConnection.callback({ - success: true, - output: "Connected", - error: "", - exitCode: 0 - }); - } - root.pendingConnection = null; - } else { - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - return; - } - } - } - } - } - - if (checkCount >= 6) { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } else { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - function checkPendingConnection(): void { if (root.pendingConnection) { Qt.callLater(() => { @@ -1264,40 +1023,9 @@ Singleton { return details; } - Process { - id: rescanProc - - command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: root.getNetworks() // qmllint disable signal-handler-parameters - } - - Process { - id: monitorProc - - running: true - command: ["nmcli", "monitor"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: SplitParser { - onRead: root.refreshOnConnectionChange() - } - onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters - } - - Timer { - id: monitorRestartTimer - - interval: 2000 - onTriggered: { - monitorProc.running = true; - } - } - - function refreshOnConnectionChange(): void { - getNetworks(networks => { - const newActive = root.active; + function refreshOnConnectionChange(): void { + getNetworks(networks => { + const newActive = root.active; if (newActive && newActive.active) { Qt.callLater(() => { @@ -1361,4 +1089,276 @@ Singleton { } }, 2000); } + + Component { + id: commandProc + + CommandProcess {} + } + + Component { + id: apComp + + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + + property int checkCount: 0 + + interval: 500 + repeat: true + triggeredOnStart: false + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.command)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + + Process { + id: rescanProc + + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] + onExited: root.getNetworks() // qmllint disable signal-handler-parameters + } + + Process { + id: monitorProc + + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: SplitParser { + onRead: root.refreshOnConnectionChange() + } + onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters + } + + Timer { + id: monitorRestartTimer + + interval: 2000 + onTriggered: { + monitorProc.running = true; + } + } + + component CommandProcess: Process { + id: proc + + property var callback: null + property list command: [] + property bool callbackCalled: false + property int exitCode: 0 + + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + } + + stderr: StdioCollector { + id: stderrCollector + + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + root.handlePasswordRequired(proc, error, output, -1); + } + } + } + + onExited: code => { // qmllint disable signal-handler-parameters + exitCode = code; + + Qt.callLater(() => { + if (callbackCalled) { + processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.command); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); + return; + } + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + callbackCalled = true; + callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + processFinished(); + } else { + processFinished(); + } + }); + } + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } } From 0f10af0f18e18b3d9843a7aa16a557c192d19178 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:50:48 +1100 Subject: [PATCH 151/409] fix: iterator invalidation in AppDb updateApps Was removing from list via take() while iterating through it --- plugin/src/Caelestia/appdb.cpp | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 6952c0e08..5dc4d7240 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -303,11 +303,13 @@ void AppDb::updateApps() { newIds.insert(entry->property("id").toString()); } - for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) { - const auto& id = *it; - if (!newIds.contains(id)) { + for (auto it = m_apps.begin(); it != m_apps.end();) { + if (!newIds.contains(it.key())) { dirty = true; - m_apps.take(id)->deleteLater(); + it.value()->deleteLater(); + it = m_apps.erase(it); + } else { + ++it; } } From 04b4ae3ba8f5d281cdabd543375844d20fb7d422 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:52:00 +1100 Subject: [PATCH 152/409] fix: use QPointer for CachingImageManager::m_item Raw QQuickItem* was not tracked for destruction, causing use-after-free if the QML engine destroyed the item while an async sha256 future was in flight. --- plugin/src/Caelestia/Internal/cachingimagemanager.hpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp index 3611699b6..1b707414d 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp @@ -2,6 +2,7 @@ #include #include +#include #include namespace caelestia::internal { @@ -18,8 +19,7 @@ class CachingImageManager : public QObject { public: explicit CachingImageManager(QObject* parent = nullptr) - : QObject(parent) - , m_item(nullptr) {} + : QObject(parent) {} [[nodiscard]] QQuickItem* item() const; void setItem(QQuickItem* item); @@ -46,7 +46,7 @@ class CachingImageManager : public QObject { private: QString m_shaPath; - QQuickItem* m_item; + QPointer m_item; QUrl m_cacheDir; QString m_path; From b85b0e7afb90fcf333969f8c41bd6b9f26202ab7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:52:25 +1100 Subject: [PATCH 153/409] fix: use QPointer for ImageAnalyser::m_sourceItem Raw QQuickItem* was not tracked for destruction, causing use-after-free if the source item was destroyed between setSourceItem() and deferred requestUpdate() invocations. --- plugin/src/Caelestia/imageanalyser.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/imageanalyser.hpp b/plugin/src/Caelestia/imageanalyser.hpp index bbea2b32e..63fbf9691 100644 --- a/plugin/src/Caelestia/imageanalyser.hpp +++ b/plugin/src/Caelestia/imageanalyser.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace caelestia { @@ -48,7 +49,7 @@ class ImageAnalyser : public QObject { QFutureWatcher* const m_futureWatcher; QString m_source; - QQuickItem* m_sourceItem; + QPointer m_sourceItem; int m_rescaleSize; QColor m_dominantColour; From d5c6e981262c4a3d49f9d2bf6bf75b11d244e5ad Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:52:44 +1100 Subject: [PATCH 154/409] fix: null check qmlEngine() in CUtils::saveItem qmlEngine(this) can return nullptr during engine shutdown, causing a null pointer dereference in the async callback. --- plugin/src/Caelestia/cutils.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index 6e3bfa99c..b6ec33a88 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -75,13 +75,20 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { if (watcher->result()) { if (onSaved.isCallable()) { - onSaved.call( - { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); + QJSValueList args = { QJSValue(path.toLocalFile()) }; + if (engine) { + args << engine->toScriptValue(QVariant::fromValue(path)); + } + onSaved.call(args); } } else { qWarning() << "CUtils::saveItem: failed to save" << path; if (onFailed.isCallable()) { - onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); + if (engine) { + onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); + } else { + onFailed.call(); + } } } watcher->deleteLater(); From 598e6d91fdaba71b468381062e28713e2c20ae16 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 01:53:02 +1100 Subject: [PATCH 155/409] fix: initialize FileSystemModel::m_sortReverse m_sortReverse was not initialized, causing undefined behavior when used in compareEntries(). --- plugin/src/Caelestia/Models/filesystemmodel.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp index cf8eae822..c3315858a 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.hpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp @@ -132,7 +132,7 @@ class FileSystemModel : public QAbstractListModel { bool m_recursive; bool m_watchChanges; bool m_showHidden; - bool m_sortReverse; + bool m_sortReverse = false; Filter m_filter; QStringList m_nameFilters; From a034467ed210374895b842a4a89f7b608c609758 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:03:28 +1100 Subject: [PATCH 156/409] fix: check grabToImage result for nullptr --- plugin/src/Caelestia/imageanalyser.cpp | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp index 3b3cdf841..0b623162b 100644 --- a/plugin/src/Caelestia/imageanalyser.cpp +++ b/plugin/src/Caelestia/imageanalyser.cpp @@ -134,6 +134,11 @@ void ImageAnalyser::update() { if (m_sourceItem) { const QSharedPointer grabResult = m_sourceItem->grabToImage(); + if (!grabResult) { + QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + return; + } QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); }); From 501a14bd2a8ab7703aac6a224824a23818552554 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 24 Mar 2026 02:33:25 +1100 Subject: [PATCH 157/409] fix: compiler warnings Mostly QFutureWatcher leaks, fixed by using QFuture.then Also calculator false positive leak --- .../Internal/cachingimagemanager.cpp | 17 +------- .../src/Caelestia/Models/filesystemmodel.cpp | 40 +++++++------------ .../src/Caelestia/Services/audiocollector.cpp | 2 +- plugin/src/Caelestia/appdb.cpp | 2 +- plugin/src/Caelestia/qalculator.cpp | 20 ++++------ 5 files changed, 26 insertions(+), 55 deletions(-) diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp index 1c15cd203..9f63f99a2 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -105,34 +105,26 @@ void CachingImageManager::updateSource(const QString& path) { m_shaPath = path; - const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); - - const auto watcher = new QFutureWatcher(this); - - connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { + QtConcurrent::run(&CachingImageManager::sha256sum, path).then(this, [path, this](const QString& sha) { if (m_path != path) { - // Object is destroyed or path has changed, ignore - watcher->deleteLater(); return; } const QSize size = effectiveSize(); if (!m_item || !size.width() || !size.height()) { - watcher->deleteLater(); return; } const QString fillMode = m_item->property("fillMode").toString(); // clang-format off const QString filename = QString("%1@%2x%3-%4.png") - .arg(watcher->result()).arg(size.width()).arg(size.height()) + .arg(sha).arg(size.width()).arg(size.height()) .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); // clang-format on const QUrl cache = m_cacheDir.resolved(QUrl(filename)); if (m_cachePath == cache) { - watcher->deleteLater(); return; } @@ -141,7 +133,6 @@ void CachingImageManager::updateSource(const QString& path) { if (!cache.isLocalFile()) { qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; - watcher->deleteLater(); return; } @@ -157,11 +148,7 @@ void CachingImageManager::updateSource(const QString& path) { if (m_shaPath == path) { m_shaPath = QString(); } - - watcher->deleteLater(); }); - - watcher->setFuture(future); } QUrl CachingImageManager::cachePath() const { diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp index 4eb94cd49..267a43946 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.cpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp @@ -219,7 +219,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { if (m_recursive && m_watchChanges) { const auto currentDir = m_dir; const bool showHidden = m_showHidden; - const auto future = QtConcurrent::run([showHidden, path]() { + auto future = QtConcurrent::run([showHidden, path]() { QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; if (showHidden) { filters |= QDir::Hidden; @@ -232,16 +232,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { } return dirs; }); - const auto watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { - const auto paths = watcher->result(); + future.then(this, [currentDir, showHidden, this](const QStringList& paths) { if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { // Ignore if dir or showHidden has changed m_watcher.addPaths(paths); } - watcher->deleteLater(); }); - watcher->setFuture(future); } } @@ -295,7 +291,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { oldPaths << entry->path(); } - const auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { + auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; std::optional iter; @@ -353,7 +349,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { newPaths.insert(path); } - if (promise.isCanceled() || newPaths == oldPaths) { + if (promise.isCanceled()) { return; } @@ -365,23 +361,17 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { } m_futures.insert(dir, future); - const auto watcher = new QFutureWatcher, QSet>>(this); - - connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { - m_futures.remove(dir); - - if (!watcher->future().isResultReadyAt(0)) { - watcher->deleteLater(); - return; - } - - const auto result = watcher->result(); - applyChanges(result.first, result.second); - - watcher->deleteLater(); - }); - - watcher->setFuture(future); + future + .then(this, + [dir, this](QPair, QSet> result) { + m_futures.remove(dir); + if (!result.first.isEmpty() || !result.second.isEmpty()) { + applyChanges(result.first, result.second); + } + }) + .onCanceled(this, [dir, this]() { + m_futures.remove(dir); + }); } void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp index 15634059e..fb051ccbd 100644 --- a/plugin/src/Caelestia/Services/audiocollector.cpp +++ b/plugin/src/Caelestia/Services/audiocollector.cpp @@ -221,7 +221,7 @@ AudioCollector::AudioCollector(QObject* parent) , m_writeBuffer(&m_buffer2) {} AudioCollector::~AudioCollector() { - stop(); + AudioCollector::stop(); } void AudioCollector::start() { diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 5dc4d7240..1e33990da 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -11,7 +11,7 @@ AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) , m_entry(entry) , m_frequency(frequency) { const auto mo = m_entry->metaObject(); - const auto tmo = metaObject(); + const auto tmo = &AppEntry::staticMetaObject; for (const auto& prop : { "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) { diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp index bfc977e8f..c72421793 100644 --- a/plugin/src/Caelestia/qalculator.cpp +++ b/plugin/src/Caelestia/qalculator.cpp @@ -1,7 +1,6 @@ #include "qalculator.hpp" #include -#include #include namespace caelestia { @@ -11,7 +10,10 @@ QMutex Qalculator::s_calculatorMutex; Qalculator::Qalculator(QObject* parent) : QObject(parent) { if (!CALCULATOR) { - new Calculator(); + // Calculator constructor sets the global `calculator` pointer (CALCULATOR macro), + // but we need to assign it to a var so compiler doesn't flag it as a leak + static const auto* const instance = new Calculator(); + Q_UNUSED(instance) CALCULATOR->loadExchangeRates(); CALCULATOR->loadGlobalDefinitions(); CALCULATOR->loadLocalDefinitions(); @@ -79,7 +81,7 @@ void Qalculator::evalAsync(const QString& expr) { emit busyChanged(); } - const auto future = QtConcurrent::run([expr]() -> QPair { + QtConcurrent::run([expr]() -> QPair { QMutexLocker locker(&s_calculatorMutex); EvaluationOptions eo; @@ -109,18 +111,12 @@ void Qalculator::evalAsync(const QString& expr) { const QString rawStr = QString::fromStdString(result); return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; - }); - - auto* watcher = new QFutureWatcher>(this); - - connect(watcher, &QFutureWatcher>::finished, this, [this, watcher, gen]() { - watcher->deleteLater(); - + }).then(this, [this, gen](QPair result) { if (gen != m_generation) { return; } - const auto [formatted, raw] = watcher->result(); + const auto& [formatted, raw] = result; if (m_result != formatted) { m_result = formatted; @@ -135,8 +131,6 @@ void Qalculator::evalAsync(const QString& expr) { emit busyChanged(); } }); - - watcher->setFuture(future); } QString Qalculator::result() const { From f15b902c0edbe140402e00de57dabecf9d2d31be Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:04:39 +1100 Subject: [PATCH 158/409] feat: add blobs module --- plugin/src/Caelestia/Blobs/CMakeLists.txt | 19 + plugin/src/Caelestia/Blobs/blobgroup.cpp | 75 ++++ plugin/src/Caelestia/Blobs/blobgroup.hpp | 53 +++ .../src/Caelestia/Blobs/blobinvertedrect.cpp | 56 +++ .../src/Caelestia/Blobs/blobinvertedrect.hpp | 56 +++ plugin/src/Caelestia/Blobs/blobmaterial.cpp | 98 +++++ plugin/src/Caelestia/Blobs/blobmaterial.hpp | 47 +++ plugin/src/Caelestia/Blobs/blobrect.cpp | 132 +++++++ plugin/src/Caelestia/Blobs/blobrect.hpp | 81 ++++ plugin/src/Caelestia/Blobs/blobshape.cpp | 365 ++++++++++++++++++ plugin/src/Caelestia/Blobs/blobshape.hpp | 71 ++++ plugin/src/Caelestia/Blobs/shaders/blob.frag | 277 +++++++++++++ plugin/src/Caelestia/Blobs/shaders/blob.vert | 29 ++ plugin/src/Caelestia/CMakeLists.txt | 3 +- 14 files changed, 1361 insertions(+), 1 deletion(-) create mode 100644 plugin/src/Caelestia/Blobs/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Blobs/blobgroup.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobgroup.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobinvertedrect.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobinvertedrect.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobmaterial.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobmaterial.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobrect.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobrect.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobshape.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobshape.hpp create mode 100644 plugin/src/Caelestia/Blobs/shaders/blob.frag create mode 100644 plugin/src/Caelestia/Blobs/shaders/blob.vert diff --git a/plugin/src/Caelestia/Blobs/CMakeLists.txt b/plugin/src/Caelestia/Blobs/CMakeLists.txt new file mode 100644 index 000000000..f44be0080 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/CMakeLists.txt @@ -0,0 +1,19 @@ +qml_module(caelestia-blobs + URI Caelestia.Blobs + SOURCES + blobgroup.cpp + blobshape.cpp + blobrect.cpp + blobinvertedrect.cpp + blobmaterial.cpp + LIBRARIES + Qt::Quick +) + +qt_add_shaders(caelestia-blobs "blob_shaders" + BATCHABLE OPTIMIZED NOHLSL NOMSL + PREFIX "/" + FILES + shaders/blob.frag + shaders/blob.vert +) diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp new file mode 100644 index 000000000..f06c4d513 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -0,0 +1,75 @@ +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" +#include "blobshape.hpp" + +BlobGroup::BlobGroup(QObject* parent) + : QObject(parent) {} + +BlobGroup::~BlobGroup() { + for (auto* shape : std::as_const(m_shapes)) + shape->m_group = nullptr; + if (m_invertedRect) + static_cast(m_invertedRect)->m_group = nullptr; +} + +void BlobGroup::setSmoothing(qreal s) { + if (qFuzzyCompare(m_smoothing, s)) + return; + m_smoothing = s; + emit smoothingChanged(); + markDirty(); +} + +void BlobGroup::setColor(const QColor& c) { + if (m_color == c) + return; + m_color = c; + emit colorChanged(); + markDirty(); +} + +void BlobGroup::addShape(BlobShape* shape) { + if (!shape || m_shapes.contains(shape)) + return; + m_shapes.append(shape); + markDirty(); +} + +void BlobGroup::removeShape(BlobShape* shape) { + m_shapes.removeOne(shape); + markDirty(); +} + +void BlobGroup::setInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect == rect) + return; + m_invertedRect = rect; + markDirty(); +} + +void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect != rect) + return; + m_invertedRect = nullptr; + markDirty(); +} + +void BlobGroup::markDirty() { + m_physicsUpdated = false; + for (auto* shape : std::as_const(m_shapes)) { + shape->polish(); + shape->update(); + } + if (m_invertedRect) { + static_cast(m_invertedRect)->polish(); + static_cast(m_invertedRect)->update(); + } +} + +void BlobGroup::ensurePhysicsUpdated() { + if (m_physicsUpdated) + return; + m_physicsUpdated = true; + for (auto* shape : std::as_const(m_shapes)) + shape->updatePhysics(); +} diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp new file mode 100644 index 000000000..4c9ed6915 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +class BlobShape; +class BlobInvertedRect; + +class BlobGroup : public QObject { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY + smoothingChanged) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + +public: + explicit BlobGroup(QObject* parent = nullptr); + ~BlobGroup() override; + + qreal smoothing() const { return m_smoothing; } + + void setSmoothing(qreal s); + + QColor color() const { return m_color; } + + void setColor(const QColor& c); + + void addShape(BlobShape* shape); + void removeShape(BlobShape* shape); + + void setInvertedRect(BlobInvertedRect* rect); + void clearInvertedRect(BlobInvertedRect* rect); + + const QList& shapes() const { return m_shapes; } + + BlobInvertedRect* invertedRect() const { return m_invertedRect; } + + void markDirty(); + void ensurePhysicsUpdated(); + +signals: + void smoothingChanged(); + void colorChanged(); + +private: + qreal m_smoothing = 32.0; + QColor m_color{ 0x44, 0x88, 0xff }; + QList m_shapes; + BlobInvertedRect* m_invertedRect = nullptr; + bool m_physicsUpdated = false; +}; diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp new file mode 100644 index 000000000..68024738c --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -0,0 +1,56 @@ +#include "blobinvertedrect.hpp" +#include "blobgroup.hpp" + +BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) + : BlobShape(parent) {} + +BlobInvertedRect::~BlobInvertedRect() { + if (m_group) + m_group->clearInvertedRect(this); +} + +void BlobInvertedRect::setBorderLeft(qreal v) { + if (qFuzzyCompare(m_borderLeft, v)) + return; + m_borderLeft = v; + emit borderLeftChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderRight(qreal v) { + if (qFuzzyCompare(m_borderRight, v)) + return; + m_borderRight = v; + emit borderRightChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderTop(qreal v) { + if (qFuzzyCompare(m_borderTop, v)) + return; + m_borderTop = v; + emit borderTopChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderBottom(qreal v) { + if (qFuzzyCompare(m_borderBottom, v)) + return; + m_borderBottom = v; + emit borderBottomChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::registerWithGroup() { + if (m_group) + m_group->setInvertedRect(this); +} + +void BlobInvertedRect::unregisterFromGroup() { + if (m_group) + m_group->clearInvertedRect(this); +} diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp new file mode 100644 index 000000000..958e2a8de --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "blobshape.hpp" + +#include + +class BlobInvertedRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY + borderLeftChanged) + Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY + borderRightChanged) + Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY + borderTopChanged) + Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY + borderBottomChanged) + +public: + explicit BlobInvertedRect(QQuickItem* parent = nullptr); + ~BlobInvertedRect() override; + + qreal borderLeft() const { return m_borderLeft; } + + void setBorderLeft(qreal v); + + qreal borderRight() const { return m_borderRight; } + + void setBorderRight(qreal v); + + qreal borderTop() const { return m_borderTop; } + + void setBorderTop(qreal v); + + qreal borderBottom() const { return m_borderBottom; } + + void setBorderBottom(qreal v); + +signals: + void borderLeftChanged(); + void borderRightChanged(); + void borderTopChanged(); + void borderBottomChanged(); + +protected: + bool isInvertedRect() const override { return true; } + + void registerWithGroup() override; + void unregisterFromGroup() override; + +private: + qreal m_borderLeft = 0; + qreal m_borderRight = 0; + qreal m_borderTop = 0; + qreal m_borderBottom = 0; +}; diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp new file mode 100644 index 000000000..f2ead2ed2 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp @@ -0,0 +1,98 @@ +#include "blobmaterial.hpp" + +#include + +QSGMaterialType* BlobMaterial::type() const { + static QSGMaterialType s_type; + return &s_type; +} + +QSGMaterialShader* BlobMaterial::createShader( + QSGRendererInterface::RenderMode) const { + return new BlobMaterialShader; +} + +int BlobMaterial::compare(const QSGMaterial* other) const { + if (this < other) + return -1; + if (this > other) + return 1; + return 0; +} + +BlobMaterialShader::BlobMaterialShader() { + setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb")); + setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); +} + +bool BlobMaterialShader::updateUniformData( + RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { + Q_UNUSED(oldMaterial); + auto* mat = static_cast(newMaterial); + QByteArray* buf = state.uniformData(); + Q_ASSERT(buf->size() >= 1440); + + if (state.isMatrixDirty()) { + const QMatrix4x4 m = state.combinedMatrix(); + memcpy(buf->data(), m.constData(), 64); + } + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + memcpy(buf->data() + 64, &opacity, 4); + } + + // Padded rect (offset 68) + memcpy(buf->data() + 68, &mat->m_paddedX, 4); + memcpy(buf->data() + 72, &mat->m_paddedY, 4); + memcpy(buf->data() + 76, &mat->m_paddedW, 4); + memcpy(buf->data() + 80, &mat->m_paddedH, 4); + + // Smooth factor (offset 84) + memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); + + // Rect count (offset 88) + memcpy(buf->data() + 88, &mat->m_rectCount, 4); + + // My index (offset 92) + memcpy(buf->data() + 92, &mat->m_myIndex, 4); + + // Color as vec4 (offset 96, 16 bytes) + const float color[4] = { + static_cast(mat->m_color.redF()), + static_cast(mat->m_color.greenF()), + static_cast(mat->m_color.blueF()), + static_cast(mat->m_color.alphaF()), + }; + memcpy(buf->data() + 96, color, 16); + + // Has inverted (offset 112) + memcpy(buf->data() + 112, &mat->m_hasInverted, 4); + + // Inverted radius (offset 116) + memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); + + // Padding at 120-127 (skip) + + // Inverted outer (offset 128, 16 bytes) + memcpy(buf->data() + 128, mat->m_invertedOuter, 16); + + // Inverted inner (offset 144, 16 bytes) + memcpy(buf->data() + 144, mat->m_invertedInner, 16); + + // Rect data (offset 160, each rect = 5 vec4s = 80 bytes) + const int count = qMin(mat->m_rectCount, 16); + for (int i = 0; i < count; ++i) { + const auto& r = mat->m_rects[i]; + const int base = 160 + i * 80; + const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; + const float d1[4] = { r.radius, r.offsetX, r.offsetY, r.minEig }; + const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; + memcpy(buf->data() + base, d0, 16); + memcpy(buf->data() + base + 16, d1, 16); + memcpy(buf->data() + base + 32, r.invDeform, 16); + memcpy(buf->data() + base + 48, d3, 16); + memcpy(buf->data() + base + 64, r.cornerFill, 16); + } + + return true; +} diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp new file mode 100644 index 000000000..ef80fea1a --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +struct BlobRectData { + float cx = 0, cy = 0, hw = 0, hh = 0; + float radius = 0; + float offsetX = 0, offsetY = 0; + float minEig = 1.0f; + // Inverse of 2x2 deformation matrix, column-major for GLSL + float invDeform[4] = { 1, 0, 0, 1 }; + // Screen-space AABB half-extents of the deformed rect + float screenHalfX = 0, screenHalfY = 0; + // Pre-computed corner fill factors (tr, br, bl, tl) + float cornerFill[4] = { 1, 1, 1, 1 }; +}; + +class BlobMaterial : public QSGMaterial { +public: + QSGMaterialType* type() const override; + QSGMaterialShader* createShader( + QSGRendererInterface::RenderMode) const override; + int compare(const QSGMaterial* other) const override; + + float m_paddedX = 0; + float m_paddedY = 0; + float m_paddedW = 0; + float m_paddedH = 0; + float m_smoothFactor = 32.0f; + int m_rectCount = 0; + int m_myIndex = -2; + QColor m_color{ 0x44, 0x88, 0xff }; + int m_hasInverted = 0; + float m_invertedRadius = 0; + float m_invertedOuter[4] = {}; + float m_invertedInner[4] = {}; + BlobRectData m_rects[16] = {}; +}; + +class BlobMaterialShader : public QSGMaterialShader { +public: + BlobMaterialShader(); + bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, + QSGMaterial* oldMaterial) override; +}; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp new file mode 100644 index 000000000..e03baa22a --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -0,0 +1,132 @@ +#include "blobrect.hpp" +#include "blobgroup.hpp" + +#include +#include + +BlobRect::BlobRect(QQuickItem* parent) + : BlobShape(parent) {} + +BlobRect::~BlobRect() { + if (m_group) + m_group->removeShape(this); +} + +void BlobRect::updatePolish() { + BlobShape::updatePolish(); + + if (m_physicsActive) { + QMetaObject::invokeMethod( + this, + [this]() { + if (m_physicsActive && m_group) + m_group->markDirty(); + }, + Qt::QueuedConnection); + } +} + +void BlobRect::updatePhysics() { + const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); + + if (!m_hasPrevPos) { + m_prevScenePos = scenePos; + m_elapsed.start(); + m_hasPrevPos = true; + return; + } + + const float dt = static_cast(m_elapsed.restart()) / 1000.0f; + if (dt > 0.1f || dt < 0.001f) { + m_prevScenePos = scenePos; + // Still check atRest on skipped frames to avoid getting stuck + if (m_physicsActive) + checkAtRest(0.0f); + return; + } + + const float velX = + static_cast(scenePos.x() - m_prevScenePos.x()) / dt; + const float velY = + static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + m_prevScenePos = scenePos; + + const float speed = std::sqrt(velX * velX + velY * velY); + + if (!m_physicsActive) { + if (speed < 5.0f) + return; + m_physicsActive = true; + } + + // Compute target deformation matrix from velocity + // R(θ) * diag(stretch, compress) * R(θ)^T + const float kStretchFactor = static_cast(m_deformScale); + constexpr float kMaxStretch = 0.35f; + + float target00 = 1.0f; + float target01 = 0.0f; + float target11 = 1.0f; + + if (speed > 5.0f) { + const float targetStretch = + 1.0f + std::min(speed * kStretchFactor, kMaxStretch); + const float targetCompress = 1.0f / targetStretch; + + const float cosA = velX / speed; + const float sinA = velY / speed; + const float cos2 = cosA * cosA; + const float sin2 = sinA * sinA; + const float cs = cosA * sinA; + + target00 = targetStretch * cos2 + targetCompress * sin2; + target01 = (targetStretch - targetCompress) * cs; + target11 = targetStretch * sin2 + targetCompress * cos2; + } + + // Underdamped spring on each matrix component + const float kStiffness = static_cast(m_stiffness); + const float kDamping = static_cast(m_damping); + + const float accel00 = + -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; + m_dmVel00 += accel00 * dt; + m_dm00 += m_dmVel00 * dt; + + const float accel01 = + -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; + m_dmVel01 += accel01 * dt; + m_dm01 += m_dmVel01 * dt; + + const float accel11 = + -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; + m_dmVel11 += accel11 * dt; + m_dm11 += m_dmVel11 * dt; + + m_deformMatrix = QMatrix4x4( + m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + updateCenteredDeformMatrix(); + + checkAtRest(speed); +} + +void BlobRect::checkAtRest(float speed) { + constexpr float kEpsilon = 0.002f; + const bool atRest = + std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && + std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && + std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && + speed < 5.0f; + + if (atRest) { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = 0.0f; + m_dmVel01 = 0.0f; + m_dmVel11 = 0.0f; + m_deformMatrix = QMatrix4x4(); // identity + updateCenteredDeformMatrix(); + m_physicsActive = false; + } +} diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp new file mode 100644 index 000000000..86d4666d3 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -0,0 +1,81 @@ +#pragma once + +#include "blobshape.hpp" + +#include +#include + +class BlobRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY + stiffnessChanged) + Q_PROPERTY( + qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) + Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY + deformScaleChanged) + +public: + explicit BlobRect(QQuickItem* parent = nullptr); + ~BlobRect() override; + + qreal stiffness() const { return m_stiffness; } + + void setStiffness(qreal s) { + if (!qFuzzyCompare(m_stiffness, s)) { + m_stiffness = s; + emit stiffnessChanged(); + } + } + + qreal damping() const { return m_damping; } + + void setDamping(qreal d) { + if (!qFuzzyCompare(m_damping, d)) { + m_damping = d; + emit dampingChanged(); + } + } + + qreal deformScale() const { return m_deformScale; } + + void setDeformScale(qreal s) { + if (!qFuzzyCompare(m_deformScale, s)) { + m_deformScale = s; + emit deformScaleChanged(); + } + } + +signals: + void stiffnessChanged(); + void dampingChanged(); + void deformScaleChanged(); + +protected: + void updatePolish() override; + void updatePhysics() override; + +private: + void checkAtRest(float speed); + + // Physics state + QPointF m_prevScenePos; + QElapsedTimer m_elapsed; + bool m_physicsActive = false; + bool m_hasPrevPos = false; + + // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, + // m11) Rest state is identity: m00=1, m01=0, m11=1 + float m_dm00 = 1.0f; + float m_dm01 = 0.0f; + float m_dm11 = 1.0f; + + // Spring velocities for each component + float m_dmVel00 = 0.0f; + float m_dmVel01 = 0.0f; + float m_dmVel11 = 0.0f; + + qreal m_stiffness = 200.0; + qreal m_damping = 16.0; + qreal m_deformScale = 0.0005; +}; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp new file mode 100644 index 000000000..73b5cfe6a --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -0,0 +1,365 @@ +#include "blobshape.hpp" +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" + +#include +#include + +#include +#include + +static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { + // Bounding box of the deformed shape: |M * corners| + const float dm00 = dm(0, 0), dm01 = dm(0, 1); + const float dm10 = dm(1, 0), dm11 = dm(1, 1); + const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh; + const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh; + const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3)); + const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3)); + return std::max(extraX, extraY); +} + +static float cpuSdBox(float px, float py, float cx, float cy, float hw, + float hh) { + const float dx = std::abs(px - cx) - hw; + const float dy = std::abs(py - cy) - hh; + const float mdx = std::max(dx, 0.0f); + const float mdy = std::max(dy, 0.0f); + return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f); +} + +static float cpuSmoothstep(float edge0, float edge1, float x) { + const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +BlobShape::BlobShape(QQuickItem* parent) + : QQuickItem(parent) { + setFlag(ItemHasContents); +} + +void BlobShape::setGroup(BlobGroup* g) { + if (m_group == g) + return; + if (m_group && isComponentComplete()) + unregisterFromGroup(); + m_group = g; + if (m_group && isComponentComplete()) + registerWithGroup(); + emit groupChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::setRadius(qreal r) { + if (qFuzzyCompare(m_radius, r)) + return; + m_radius = r; + emit radiusChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::componentComplete() { + QQuickItem::componentComplete(); + if (m_group) + registerWithGroup(); +} + +void BlobShape::geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + updateCenteredDeformMatrix(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::updateCenteredDeformMatrix() { + const auto cx = static_cast(width()) * 0.5f; + const auto cy = static_cast(height()) * 0.5f; + QMatrix4x4 result; + result.translate(cx, cy); + result *= m_deformMatrix; + result.translate(-cx, -cy); + m_centeredDeformMatrix = result; + emit deformMatrixChanged(); +} + +void BlobShape::registerWithGroup() { + if (m_group) + m_group->addShape(this); +} + +void BlobShape::unregisterFromGroup() { + if (m_group) + m_group->removeShape(this); +} + +void BlobShape::updatePolish() { + if (!m_group) + return; + + // Ensure all shapes have up-to-date physics (only once per frame) + m_group->ensurePhysicsUpdated(); + + // When inverted rect renders everything, skip spatial query for others + if (!isInvertedRect() && m_group->invertedRect()) + return; + + const QPointF scenePos = mapToScene(QPointF(0, 0)); + const float pad = static_cast(m_group->smoothing()); + + if (isInvertedRect()) { + m_cachedPaddedX = static_cast(scenePos.x()); + m_cachedPaddedY = static_cast(scenePos.y()); + m_cachedPaddedW = static_cast(width()); + m_cachedPaddedH = static_cast(height()); + m_localPaddedRect = QRectF(0, 0, width(), height()); + } else { + const float hw = static_cast(width()) * 0.5f; + const float hh = static_cast(height()) * 0.5f; + const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh); + + m_cachedPaddedX = static_cast(scenePos.x()) - totalPad; + m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; + m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; + m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; + m_localPaddedRect = QRectF(static_cast(-totalPad), + static_cast(-totalPad), + width() + 2.0 * static_cast(totalPad), + height() + 2.0 * static_cast(totalPad)); + } + + // Filter nearby normal rects + m_cachedRects.clear(); + m_cachedMyIndex = -2; + const QRectF myPadded(static_cast(m_cachedPaddedX), + static_cast(m_cachedPaddedY), + static_cast(m_cachedPaddedW), + static_cast(m_cachedPaddedH)); + + for (BlobShape* other : m_group->shapes()) { + if (other->isInvertedRect()) + continue; + + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); + + bool include = false; + if (isInvertedRect()) { + include = true; + } else { + const float otherHW = static_cast(other->width()) * 0.5f; + const float otherHH = static_cast(other->height()) * 0.5f; + const float otherPad = + pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); + const QRectF otherPadded( + otherScene.x() - static_cast(otherPad), + otherScene.y() - static_cast(otherPad), + other->width() + 2.0 * static_cast(otherPad), + other->height() + 2.0 * static_cast(otherPad)); + include = myPadded.intersects(otherPadded); + } + + if (include) { + if (other == this) + m_cachedMyIndex = static_cast(m_cachedRects.size()); + + const QMatrix4x4& dm = other->m_deformMatrix; + const float a = dm(0, 0), b = dm(1, 0); + const float c = dm(0, 1), d = dm(1, 1); + + BlobRectData r; + r.cx = static_cast(otherScene.x() + other->width() / 2.0); + r.cy = static_cast(otherScene.y() + other->height() / 2.0); + r.hw = static_cast(other->width() / 2.0); + r.hh = static_cast(other->height() / 2.0); + r.radius = static_cast(other->radius()); + r.offsetX = dm(0, 3); + r.offsetY = dm(1, 3); + + // Pre-compute inverse deformation matrix + const float det = a * d - c * b; + const float invDet = + std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; + r.invDeform[0] = d * invDet; + r.invDeform[1] = -b * invDet; + r.invDeform[2] = -c * invDet; + r.invDeform[3] = a * invDet; + + // Pre-compute minimum eigenvalue (avoids per-pixel sqrt) + const float halfTr = 0.5f * (a + d); + const float halfDiff = 0.5f * (a - d); + r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c); + + // Pre-compute screen-space AABB half-extents + r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh; + r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; + + m_cachedRects.append(r); + } + } + + if (isInvertedRect()) + m_cachedMyIndex = -1; + + // Cache inverted rect data + m_cachedHasInverted = false; + m_cachedInvertedRadius = 0; + memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); + memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); + + auto* inv = m_group->invertedRect(); + if (inv) { + m_cachedHasInverted = true; + m_cachedInvertedRadius = static_cast(inv->radius()); + + const QPointF invScene = inv->mapToScene(QPointF(0, 0)); + const float outerCX = + static_cast(invScene.x() + inv->width() / 2.0); + const float outerCY = + static_cast(invScene.y() + inv->height() / 2.0); + const float outerHW = static_cast(inv->width() / 2.0); + const float outerHH = static_cast(inv->height() / 2.0); + + const float innerCX = + outerCX + + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); + const float innerCY = + outerCY + + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); + const float innerHW = + outerHW - + static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); + const float innerHH = + outerHH - + static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); + + m_cachedInvertedOuter[0] = outerCX; + m_cachedInvertedOuter[1] = outerCY; + m_cachedInvertedOuter[2] = outerHW; + m_cachedInvertedOuter[3] = outerHH; + + m_cachedInvertedInner[0] = innerCX; + m_cachedInvertedInner[1] = innerCY; + m_cachedInvertedInner[2] = innerHW; + m_cachedInvertedInner[3] = innerHH; + } + + // Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) + const float smoothFactor = pad; + const auto rectCount = m_cachedRects.size(); + for (qsizetype i = 0; i < rectCount; ++i) { + auto& ri = m_cachedRects[i]; + float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; + + const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; + const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; + const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; + const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; + + for (qsizetype j = 0; j < rectCount; ++j) { + if (j == i) + continue; + const auto& rj = m_cachedRects[j]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, + cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + } + + if (m_cachedHasInverted) { + const float icx = m_cachedInvertedInner[0]; + const float icy = m_cachedInvertedInner[1]; + const float ihw = m_cachedInvertedInner[2]; + const float ihh = m_cachedInvertedInner[3]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, + -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); + } + + ri.cornerFill[0] = fTr; + ri.cornerFill[1] = fBr; + ri.cornerFill[2] = fBl; + ri.cornerFill[3] = fTl; + } +} + +QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + delete oldNode; + return nullptr; + } + + // When an inverted rect exists, it renders everything in a single pass + if (!isInvertedRect() && m_group->invertedRect()) { + delete oldNode; + return nullptr; + } + + auto* node = static_cast(oldNode); + if (!node) { + node = new QSGGeometryNode; + + auto* geometry = new QSGGeometry( + QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // Update geometry + auto* geometry = node->geometry(); + auto* v = geometry->vertexDataAsTexturedPoint2D(); + + const float x0 = static_cast(m_localPaddedRect.x()); + const float y0 = static_cast(m_localPaddedRect.y()); + const float x1 = x0 + static_cast(m_localPaddedRect.width()); + const float y1 = y0 + static_cast(m_localPaddedRect.height()); + + v[0].set(x0, y0, 0.0f, 0.0f); + v[1].set(x1, y0, 1.0f, 0.0f); + v[2].set(x0, y1, 0.0f, 1.0f); + v[3].set(x1, y1, 1.0f, 1.0f); + + node->markDirty(QSGNode::DirtyGeometry); + + // Update material + auto* material = static_cast(node->material()); + material->m_paddedX = m_cachedPaddedX; + material->m_paddedY = m_cachedPaddedY; + material->m_paddedW = m_cachedPaddedW; + material->m_paddedH = m_cachedPaddedH; + material->m_smoothFactor = static_cast(m_group->smoothing()); + material->m_myIndex = m_cachedMyIndex; + material->m_color = m_group->color(); + material->m_hasInverted = m_cachedHasInverted ? 1 : 0; + material->m_invertedRadius = m_cachedInvertedRadius; + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, + sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, + sizeof(m_cachedInvertedInner)); + + const int count = + static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = m_cachedRects[i]; + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp new file mode 100644 index 000000000..8c6f11655 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -0,0 +1,71 @@ +#pragma once + +#include "blobmaterial.hpp" + +#include +#include +#include + +class BlobGroup; + +class BlobShape : public QQuickItem { + Q_OBJECT + Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + Q_PROPERTY( + QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + + friend class BlobGroup; + +public: + explicit BlobShape(QQuickItem* parent = nullptr); + ~BlobShape() override = default; + + BlobGroup* group() const { return m_group; } + + void setGroup(BlobGroup* g); + + qreal radius() const { return m_radius; } + + void setRadius(qreal r); + + QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + +signals: + void groupChanged(); + void radiusChanged(); + void deformMatrixChanged(); + +protected: + void componentComplete() override; + void geometryChange( + const QRectF& newGeometry, const QRectF& oldGeometry) override; + void updatePolish() override; + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + + virtual bool isInvertedRect() const { return false; } + + virtual void updatePhysics() {} + + virtual void registerWithGroup(); + virtual void unregisterFromGroup(); + void updateCenteredDeformMatrix(); + + BlobGroup* m_group = nullptr; + qreal m_radius = 0; + QMatrix4x4 m_deformMatrix; // identity by default + QMatrix4x4 m_centeredDeformMatrix; + + // Cached data from updatePolish + float m_cachedPaddedX = 0; + float m_cachedPaddedY = 0; + float m_cachedPaddedW = 0; + float m_cachedPaddedH = 0; + QRectF m_localPaddedRect; + QVector m_cachedRects; + int m_cachedMyIndex = -2; + bool m_cachedHasInverted = false; + float m_cachedInvertedRadius = 0; + float m_cachedInvertedOuter[4] = {}; + float m_cachedInvertedInner[4] = {}; +}; diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag new file mode 100644 index 000000000..f4baa7317 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -0,0 +1,277 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { + vec2 d = abs(p - center) - halfSize + vec2(radius); + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius; +} + +float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { + // r = (topRight, bottomRight, bottomLeft, topLeft) + p -= center; + r.xy = (p.x > 0.0) ? r.xy : r.wz; + r.x = (p.y > 0.0) ? r.y : r.x; + vec2 q = abs(p) - halfSize + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; +} + +float sdBox(vec2 p, vec2 center, vec2 halfSize) { + vec2 d = abs(p - center) - halfSize; + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); +} + +float smin(float a, float b, float k) { + // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) + float h = max(k - abs(a - b), 0.0) / k; + return min(a, b) - h * h * h * k * (1.0/6.0); +} + +float sminNoBulge(float a, float b, float k) { + // Cubic smooth min with reduced outward expansion when shapes overlap + float h = max(k - abs(a - b), 0.0) / k; + float blend = h * h * h * k * (1.0/6.0); + blend *= smoothstep(-k, 0.0, min(a, b)); + return min(a, b) - blend; +} + +float smax(float a, float b, float k) { + float h = max(k - abs(a - b), 0.0) / k; + return max(a, b) + h * h * h * k * (1.0/6.0); +} + +float smaxSharpA(float a, float b, float k) { + // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). + // Used for the frame outer edge so it always fills to the edges. + float h = max(k - abs(a - b), 0.0) / k; + float blend = h * h * h * k * (1.0/6.0); + blend *= smoothstep(0.0, k * 0.5, -a); + return max(a, b) + blend; +} + +void main() { + vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); + + float mergedSdf = 1e10; + int owner = -2; + float minDist = 1e10; + + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; // cx, cy, hw, hh + vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig + vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix + vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 + vec4 fills = rectData[i * 5 + 4]; // f_tr, f_br, f_bl, f_tl + + // Offset center for asymmetric deformation + vec2 center = rect.xy + props.yz; + + // Apply pre-computed inverse deformation to the evaluation point + mat2 invDeform = mat2(invDm.xy, invDm.zw); + vec2 transformedPixel = center + invDeform * (pixel - center); + + // Use pre-computed corner fill factors + float br = props.x; + float minR = 2.0; + vec4 radii = max(br * fills, vec4(minR)); + float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); + + // Use pre-computed minimum eigenvalue for SDF correction + d *= max(props.w, 0.01); + + // Scale SDF on the axis facing a nearby border to narrow the smin blend zone + // in that direction only, without reducing k (which would cause sharp edges). + if (hasInverted != 0) { + vec2 screenHalf = sh.xy; + + float distY0 = (center.y + screenHalf.y) - (invertedInner.y - invertedInner.w); + float distY1 = (invertedInner.y + invertedInner.w) - (center.y - screenHalf.y); + float distX0 = (center.x + screenHalf.x) - (invertedInner.x - invertedInner.z); + float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); + + // 0 = far from border, 1 = at border (max compression) + float yProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distY0), + smoothstep(0.0, smoothFactor, distY1) + ); + float xProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distX0), + smoothstep(0.0, smoothFactor, distX1) + ); + + // Smooth axis weights: gradient-based at corners, face-based inside. + vec2 q = abs(pixel - center) - screenHalf; + vec2 qp = max(q, vec2(0.0)); + float cornerLen = length(qp); + + // Gradient direction in corner region (smooth 90-degree rotation) + float gradX = qp.x / max(cornerLen, 0.001); + float gradY = qp.y / max(cornerLen, 0.001); + + // Smooth face weights for inside/edge (no hard branch) + float faceY = smoothstep(-4.0, 4.0, q.y - q.x); + float faceX = 1.0 - faceY; + + // Blend: gradient in corner region, face-based inside + float t = smoothstep(0.0, 2.0, cornerLen); + float xWeight = mix(faceX, gradX, t); + float yWeight = mix(faceY, gradY, t); + + float boost = 3.0; + float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost; + d *= scale; + } + + // Rect-to-rect edge sink: indent this rect's edge where another + // rect is slightly past it, fading once past threshold. + if (rectCount > 1) { + vec2 iSh = sh.xy; + float sinkT = smoothFactor * 0.75; + float sinkOff = smoothFactor * (1.0 / 6.0); + float rectSinkVal = 0.0; + + for (int j = 0; j < rectCount; j++) { + if (j == i) continue; + + vec4 jR = rectData[j * 5]; + vec4 jP = rectData[j * 5 + 1]; + vec2 jSh = rectData[j * 5 + 3].xy; + vec2 jC = jR.xy + jP.yz; + + // Penetration of j past i's edges (positive = past) + float pT = (jC.y + jSh.y) - (center.y - iSh.y) - sinkOff; + float pB = (center.y + iSh.y) - (jC.y - jSh.y) - sinkOff; + float pL = (jC.x + jSh.x) - (center.x - iSh.x) - sinkOff; + float pR = (center.x + iSh.x) - (jC.x - jSh.x) - sinkOff; + + // Smooth bump: rises then falls, zero outside [0, sinkT] + float aT = smoothstep(0.0, sinkT * 0.4, pT) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pT)); + float aB = smoothstep(0.0, sinkT * 0.4, pB) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pB)); + float aL = smoothstep(0.0, sinkT * 0.4, pL) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pL)); + float aR = smoothstep(0.0, sinkT * 0.4, pR) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pR)); + + // Lateral falloff from rect j's extent + float hLat = max(abs(pixel.x - jC.x) - jSh.x, 0.0); + float vLat = max(abs(pixel.y - jC.y) - jSh.y, 0.0); + float latF = smoothFactor * 2.0; + + // Perpendicular zone (near rect i's edge only) + float zT = 1.0 - smoothstep(center.y - iSh.y, center.y - iSh.y + smoothFactor, pixel.y); + float zB = smoothstep(center.y + iSh.y - smoothFactor, center.y + iSh.y, pixel.y); + float zL = 1.0 - smoothstep(center.x - iSh.x, center.x - iSh.x + smoothFactor, pixel.x); + float zR = smoothstep(center.x + iSh.x - smoothFactor, center.x + iSh.x, pixel.x); + + float s = max( + max(aT * smoothstep(latF, 0.0, hLat) * zT, + aB * smoothstep(latF, 0.0, hLat) * zB), + max(aL * smoothstep(latF, 0.0, vLat) * zL, + aR * smoothstep(latF, 0.0, vLat) * zR) + ); + rectSinkVal = max(rectSinkVal, s); + } + d += rectSinkVal * smoothFactor * 0.25; + } + + mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); + if (d < minDist) { + minDist = d; + owner = i; + } + } + + if (hasInverted != 0) { + float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; + float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); + + // Border sinks: track the opposite rect edge, clamped to border thickness + float innerTop = invertedInner.y - invertedInner.w; + float innerBot = invertedInner.y + invertedInner.w; + float innerLeft = invertedInner.x - invertedInner.z; + float innerRight = invertedInner.x + invertedInner.z; + float outerTop = invertedOuter.y - invertedOuter.w; + float outerBot = invertedOuter.y + invertedOuter.w; + float outerLeft = invertedOuter.x - invertedOuter.z; + float outerRight = invertedOuter.x + invertedOuter.z; + + float sinkValue = 0.0; + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; + vec4 sinkProps = rectData[i * 5 + 1]; + vec2 sinkSh = rectData[i * 5 + 3].xy; + + // Screen-space center (with offset) and pre-computed AABB half-extents + vec2 ctr = rect.xy + sinkProps.yz; + + // Delay sink to absorb smin blend depth (cubic smin max = k/6) + float preOff = smoothFactor * (1.0/6.0); + + // Top border: track rect's BOTTOM edge, only within border thickness + float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); + + // Bottom border: track rect's TOP edge + float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot); + + // Left border: track rect's RIGHT edge + float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft); + + // Right border: track rect's LEFT edge + float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight); + + // Lateral distance from pixel to rect's extent along each edge + float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); + float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); + + // Perpendicular proximity: full strength in border, fade inside inner area + float topZone = 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); + float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); + float leftZone = 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); + float rightZone = smoothstep(innerRight - smoothFactor, innerRight, pixel.x); + + float s = smoothFactor * 2.0; + float sink = max( + max(topPen * smoothstep(s, 0.0, hLat) * topZone, + botPen * smoothstep(s, 0.0, hLat) * botZone), + max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, + rightPen * smoothstep(s, 0.0, vLat) * rightZone) + ); + sinkValue = max(sinkValue, sink); + } + + dInner -= sinkValue; + + float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); + + mergedSdf = smin(mergedSdf, dFrame, smoothFactor); + if (dFrame < minDist) { + owner = -1; + } + } + + // myIndex == -1: inverted rect renders everything (frame + blobs) + // myIndex >= 0: individual rect renders only its owned pixels + if (myIndex >= 0 && owner != myIndex) + discard; + + float fw = fwidth(mergedSdf); + float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); + fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; +} diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.vert b/plugin/src/Caelestia/Blobs/shaders/blob.vert new file mode 100644 index 000000000..e71d81042 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.vert @@ -0,0 +1,29 @@ +#version 440 + +layout(location = 0) in vec4 qt_VertexPosition; +layout(location = 1) in vec2 qt_VertexTexCoord; + +layout(location = 0) out vec2 qt_TexCoord0; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +void main() { + gl_Position = qt_Matrix * qt_VertexPosition; + qt_TexCoord0 = qt_VertexTexCoord; +} diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 1b7d0e493..e8b724796 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) +find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus) find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) @@ -60,3 +60,4 @@ qml_module(caelestia add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) +add_subdirectory(Blobs) From 87f33d55ee8ef3ab3351d02493d810acad833ee9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 25 Mar 2026 12:04:55 +1100 Subject: [PATCH 159/409] feat: sdf blob based drawers --- modules/bar/popouts/Wrapper.qml | 3 +- modules/dashboard/Wrapper.qml | 38 ++++---- modules/drawers/Drawers.qml | 145 +++++++++++++++++++++++++++++- modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 8 +- modules/launcher/Wrapper.qml | 33 +++---- modules/notifications/Wrapper.qml | 11 +-- modules/osd/Wrapper.qml | 37 ++++---- modules/session/Wrapper.qml | 37 ++++---- modules/sidebar/Wrapper.qml | 37 ++++---- modules/utilities/Wrapper.qml | 36 ++++---- 11 files changed, 273 insertions(+), 114 deletions(-) diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 06bec9b0b..f58b56da1 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -15,7 +15,8 @@ Item { required property ShellScreen screen - readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 + readonly property real shownWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth + readonly property real nonAnimWidth: x > 0 || hasCurrent ? shownWidth : 0 readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property Item current: (content.item as Content)?.current ?? null diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 596c21b75..8448faa6b 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -30,8 +30,9 @@ Item { readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 - visible: height > 0 - implicitHeight: 0 + visible: anchors.topMargin > -implicitHeight - 5 + anchors.topMargin: -implicitHeight - 5 + implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth onStateChanged: { @@ -46,32 +47,35 @@ Item { when: root.visibilities.dashboard && Config.dashboard.enabled PropertyChanges { - root.implicitHeight: content.implicitHeight + // root.implicitHeight: content.implicitHeight + root.anchors.topMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitHeight" + target: root.anchors + property: "topMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitHeight" - easing.bezierCurve: Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root.anchors + // property: "topMargin" + // easing.bezierCurve: Appearance.anim.curves.emphasized + // } + // } + + ] Timer { diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 864ad5c46..1ae98c7a1 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -6,6 +6,7 @@ import QtQuick.Effects import Quickshell import Quickshell.Hyprland import Quickshell.Wayland +import Caelestia.Blobs import qs.components import qs.components.containers import qs.services @@ -123,13 +124,113 @@ Variants { shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) } - Border { + // Border { + // bar: bar + // } + + // Backgrounds { + // panels: panels + // bar: bar + // } + + BlobGroup { + id: blobGroup + + color: Colours.palette.m3surface + } + + BlobInvertedRect { + anchors.fill: parent + anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers + group: blobGroup + radius: Config.border.rounding + borderLeft: bar.implicitWidth - anchors.margins + borderRight: Config.border.thickness - anchors.margins + borderTop: Config.border.thickness - anchors.margins + borderBottom: Config.border.thickness - anchors.margins + } + + PanelBg { + id: dashBg + + group: blobGroup + panel: panels.dashboard bar: bar } - Backgrounds { - panels: panels + PanelBg { + id: launcherBg + + group: blobGroup + panel: panels.launcher bar: bar + deformAmount: 0.1 + } + + PanelBg { + id: sessionBg + + group: blobGroup + panel: panels.session + bar: bar + } + + PanelBg { + id: sidebarBg + + group: blobGroup + panel: panels.sidebar + bar: bar + deformAmount: 0 + } + + PanelBg { + id: osdBg + + group: blobGroup + panel: panels.osd + bar: bar + } + + PanelBg { + id: notifsBg + + group: blobGroup + panel: panels.notifications + bar: bar + } + + PanelBg { + id: utilsBg + + group: blobGroup + panel: panels.utilities + bar: bar + } + + PanelBg { + id: popoutBg + + group: blobGroup + panel: panels.popouts + bar: bar + + x: bar.implicitWidth - (panels.popouts.isDetached ? -(win.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) + implicitWidth: panels.popouts.shownWidth + + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } } } @@ -152,6 +253,31 @@ Variants { screen: scope.modelData visibilities: visibilities bar: bar + + dashboard.transform: Matrix4x4 { + matrix: dashBg.deformMatrix + } + launcher.transform: Matrix4x4 { + matrix: launcherBg.deformMatrix + } + session.transform: Matrix4x4 { + matrix: sessionBg.deformMatrix + } + sidebar.transform: Matrix4x4 { + matrix: sidebarBg.deformMatrix + } + osd.transform: Matrix4x4 { + matrix: osdBg.deformMatrix + } + notifications.transform: Matrix4x4 { + matrix: notifsBg.deformMatrix + } + utilities.transform: Matrix4x4 { + matrix: utilsBg.deformMatrix + } + popouts.transform: Matrix4x4 { + matrix: popoutBg.deformMatrix + } } BarWrapper { @@ -171,4 +297,17 @@ Variants { } } } + + component PanelBg: BlobRect { + required property Item panel + required property Item bar + property real deformAmount: 0.15 + + x: panel.x + bar.implicitWidth + y: panel.y + Config.border.thickness + implicitWidth: panel.width + implicitHeight: panel.height + radius: Config.border.rounding + deformScale: deformAmount / 10000 + } } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fcb128a93..6008a853a 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -102,7 +102,7 @@ CustomMouseArea { visibilities.bar = false; } - if (panels.sidebar.width === 0) { + if (panels.sidebar.anchors.rightMargin === -panels.sidebar.implicitWidth - 5) { // Show osd on hover const showOsd = inRightPanel(panels.osd, x, y); diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index f2531cfa3..006205fd4 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -42,8 +42,8 @@ Item { visibilities: root.visibilities anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: session.width + sidebar.width + anchors.right: session.left + // anchors.rightMargin: session.width + sidebar.width } Notifications.Wrapper { @@ -66,8 +66,8 @@ Item { panels: root anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - anchors.rightMargin: sidebar.width + anchors.right: sidebar.left + // anchors.rightMargin: sidebar.width } Launcher.Wrapper { diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index cc5e86c6a..f5c866afe 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -24,8 +24,9 @@ Item { onMaxHeightChanged: timer.start() - visible: height > 0 - implicitHeight: 0 + visible: anchors.bottomMargin > -implicitHeight - 5 + anchors.bottomMargin: -implicitHeight - 5 + implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth onShouldBeActiveChanged: { @@ -43,28 +44,30 @@ Item { id: showAnim Anim { - target: root - property: "implicitHeight" - to: root.contentHeight + target: root.anchors + property: "bottomMargin" + to: 0 duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - ScriptAction { - script: root.implicitHeight = Qt.binding(() => content.implicitHeight) - } + // ScriptAction { + // script: root.implicitHeight = Qt.binding(() => content.implicitHeight) + // } } SequentialAnimation { id: hideAnim - ScriptAction { - script: root.implicitHeight = root.implicitHeight - } + // ScriptAction { + // script: root.implicitHeight = root.implicitHeight + // } Anim { - target: root - property: "implicitHeight" - to: 0 - easing.bezierCurve: Appearance.anim.curves.emphasized + target: root.anchors + property: "bottomMargin" + to: -content.implicitHeight - 5 + // easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 2b581e93e..c0756b9a7 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -10,23 +10,24 @@ Item { property alias osdPanel: content.osdPanel property alias sessionPanel: content.sessionPanel - visible: height > 0 + visible: anchors.topMargin > -implicitHeight - 5 + anchors.topMargin: -5 implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) implicitHeight: content.implicitHeight states: State { name: "hidden" - when: root.visibilities.sidebar && Config.sidebar.enabled + // when: root.visibilities.sidebar && Config.sidebar.enabled PropertyChanges { - root.implicitHeight: 0 + root.anchors.topMargin: -implicitHeight - 5 } } transitions: Transition { Anim { - target: root - property: "implicitHeight" + target: root.anchors + property: "topMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 939b57de1..87350d926 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -34,8 +34,9 @@ Item { brightness = root.monitor?.brightness ?? 0; } - visible: width > 0 - implicitWidth: 0 + visible: anchors.rightMargin > -implicitWidth + anchors.rightMargin: -implicitWidth + implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight states: State { @@ -43,31 +44,33 @@ Item { when: root.shouldBeActive PropertyChanges { - root.implicitWidth: content.implicitWidth + root.anchors.rightMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitWidth" + target: root.anchors + property: "rightMargin" easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitWidth" + // easing.bezierCurve: Appearance.anim.curves.emphasized + // } + // } + + ] Connections { diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 2924f776f..b829409b8 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -11,8 +11,9 @@ Item { required property var panels readonly property real nonAnimWidth: content.implicitWidth - visible: width > 0 - implicitWidth: 0 + visible: anchors.rightMargin > -implicitWidth - 1 + anchors.rightMargin: -implicitWidth - 1 + implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight states: State { @@ -20,31 +21,33 @@ Item { when: root.visibilities.session && Config.session.enabled PropertyChanges { - root.implicitWidth: root.nonAnimWidth + root.anchors.rightMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitWidth" + target: root.anchors + property: "rightMargin" easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitWidth" + // easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized + // } + // } + + ] Loader { diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index ad2564131..67929bd7e 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -11,40 +11,43 @@ Item { required property var panels readonly property Props props: Props {} - visible: width > 0 - implicitWidth: 0 + visible: anchors.rightMargin > -implicitWidth - 5 + anchors.rightMargin: -implicitWidth - 5 + implicitWidth: Config.sidebar.sizes.width states: State { name: "visible" when: root.visibilities.sidebar && Config.sidebar.enabled PropertyChanges { - root.implicitWidth: Config.sidebar.sizes.width + root.anchors.rightMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitWidth" + target: root.anchors + property: "rightMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitWidth" + // easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized + // } + // } + + ] Loader { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 66a616f07..59288f5df 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -22,8 +22,9 @@ Item { } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) - visible: height > 0 - implicitHeight: 0 + visible: anchors.bottomMargin > -implicitHeight - 5 + anchors.bottomMargin: -implicitHeight - 5 + implicitHeight: content.implicitHeight implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width onStateChanged: { @@ -38,32 +39,33 @@ Item { when: root.shouldBeActive PropertyChanges { - root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + root.anchors.bottomMargin: 0 } } transitions: [ Transition { - from: "" - to: "visible" + // from: "" + // to: "visible" Anim { - target: root - property: "implicitHeight" + target: root.anchors + property: "bottomMargin" duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitHeight" - easing.bezierCurve: Appearance.anim.curves.emphasized - } } + // Transition { + // from: "visible" + // to: "" + + // Anim { + // target: root + // property: "implicitHeight" + // easing.bezierCurve: Appearance.anim.curves.emphasized + // } + // } + ] Timer { From f6d7e40f35597997fe7ec8aa43d15a13b671b521 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 25 Mar 2026 21:13:00 +1100 Subject: [PATCH 160/409] fix: tweak deform amounts --- modules/drawers/Drawers.qml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 1ae98c7a1..ed435d9a6 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -156,6 +156,7 @@ Variants { group: blobGroup panel: panels.dashboard bar: bar + deformAmount: 0.1 } PanelBg { @@ -173,6 +174,7 @@ Variants { group: blobGroup panel: panels.session bar: bar + deformAmount: 0.25 } PanelBg { @@ -190,6 +192,7 @@ Variants { group: blobGroup panel: panels.osd bar: bar + deformAmount: 0.3 } PanelBg { From 6103ce19e8937fb1729715a24abafc69c6618090 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 19:45:45 +0100 Subject: [PATCH 161/409] improve performace of SDF MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Avoid fullscreen shading every pixel - Evaluate each BlobRect: localized quad, only nearby rects, instead of all 8 rects - O(1-3) per pixel for most passes, rather than O(N²) with N=8(~72 iterations). - Closed panels excluded entirely from loop. --- modules/drawers/Drawers.qml | 9 +- .../src/Caelestia/Blobs/blobinvertedrect.cpp | 117 ++++++++++++++++++ .../src/Caelestia/Blobs/blobinvertedrect.hpp | 2 + plugin/src/Caelestia/Blobs/blobshape.cpp | 14 +-- plugin/src/Caelestia/Blobs/shaders/blob.frag | 18 ++- 5 files changed, 139 insertions(+), 21 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index ed435d9a6..ff1d6a4d6 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -153,7 +153,6 @@ Variants { PanelBg { id: dashBg - group: blobGroup panel: panels.dashboard bar: bar deformAmount: 0.1 @@ -162,7 +161,6 @@ Variants { PanelBg { id: launcherBg - group: blobGroup panel: panels.launcher bar: bar deformAmount: 0.1 @@ -171,7 +169,6 @@ Variants { PanelBg { id: sessionBg - group: blobGroup panel: panels.session bar: bar deformAmount: 0.25 @@ -180,7 +177,6 @@ Variants { PanelBg { id: sidebarBg - group: blobGroup panel: panels.sidebar bar: bar deformAmount: 0 @@ -189,7 +185,6 @@ Variants { PanelBg { id: osdBg - group: blobGroup panel: panels.osd bar: bar deformAmount: 0.3 @@ -198,7 +193,6 @@ Variants { PanelBg { id: notifsBg - group: blobGroup panel: panels.notifications bar: bar } @@ -206,7 +200,6 @@ Variants { PanelBg { id: utilsBg - group: blobGroup panel: panels.utilities bar: bar } @@ -214,7 +207,6 @@ Variants { PanelBg { id: popoutBg - group: blobGroup panel: panels.popouts bar: bar @@ -306,6 +298,7 @@ Variants { required property Item bar property real deformAmount: 0.15 + group: panel.width > 0 && panel.height > 0 ? blobGroup : null x: panel.x + bar.implicitWidth y: panel.y + Config.border.thickness implicitWidth: panel.width diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp index 68024738c..3e392b4af 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -1,9 +1,126 @@ #include "blobinvertedrect.hpp" #include "blobgroup.hpp" +#include "blobmaterial.hpp" + +#include +#include + +#include +#include BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) : BlobShape(parent) {} +static void setFrameIndices(quint16* idx) { + // Top strip: 0-1-4, 1-5-4 + idx[0] = 0; idx[1] = 1; idx[2] = 4; + idx[3] = 1; idx[4] = 5; idx[5] = 4; + // Right strip: 1-2-5, 2-6-5 + idx[6] = 1; idx[7] = 2; idx[8] = 5; + idx[9] = 2; idx[10] = 6; idx[11] = 5; + // Bottom strip: 2-3-6, 3-7-6 + idx[12] = 2; idx[13] = 3; idx[14] = 6; + idx[15] = 3; idx[16] = 7; idx[17] = 6; + // Left strip: 3-0-7, 0-4-7 + idx[18] = 3; idx[19] = 0; idx[20] = 7; + idx[21] = 0; idx[22] = 4; idx[23] = 7; +} + +QSGNode* BlobInvertedRect::updatePaintNode( + QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + delete oldNode; + return nullptr; + } + + const float pad = static_cast(m_group->smoothing()); + + // Compute inner hole boundary in local coords + // Inset past the inner border edge by 2x smoothing to cover the blend zone + const float inset = pad * 2.0f; + const float holeLeft = static_cast(m_borderLeft) + inset; + const float holeTop = static_cast(m_borderTop) + inset; + const float holeRight = static_cast(width() - m_borderRight) - inset; + const float holeBot = static_cast(height() - m_borderBottom) - inset; + + // If the hole is too small or invalid, fall back to full quad + if (holeLeft >= holeRight || holeTop >= holeBot) + return BlobShape::updatePaintNode(oldNode, nullptr); + + auto* node = static_cast(oldNode); + + const bool needsRebuild = !node || node->geometry()->vertexCount() != 8; + + if (needsRebuild) { + delete oldNode; + node = new QSGGeometryNode; + + auto* geometry = new QSGGeometry( + QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, + QSGGeometry::UnsignedShortType); + geometry->setDrawingMode(QSGGeometry::DrawTriangles); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + setFrameIndices(geometry->indexDataAsUShort()); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // Outer bounds (local coords) + const float x0 = static_cast(m_localPaddedRect.x()); + const float y0 = static_cast(m_localPaddedRect.y()); + const float x1 = x0 + static_cast(m_localPaddedRect.width()); + const float y1 = y0 + static_cast(m_localPaddedRect.height()); + const float w = x1 - x0; + const float h = y1 - y0; + + // Update vertex positions and texture coordinates + auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); + + // Outer corners + v[0].set(x0, y0, 0.0f, 0.0f); + v[1].set(x1, y0, 1.0f, 0.0f); + v[2].set(x1, y1, 1.0f, 1.0f); + v[3].set(x0, y1, 0.0f, 1.0f); + // Inner corners (hole) + v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); + v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); + v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); + v[7].set(holeLeft, holeBot, (holeLeft - x0) / w, (holeBot - y0) / h); + + node->markDirty(QSGNode::DirtyGeometry); + + // Update material uniforms + auto* material = static_cast(node->material()); + material->m_paddedX = m_cachedPaddedX; + material->m_paddedY = m_cachedPaddedY; + material->m_paddedW = m_cachedPaddedW; + material->m_paddedH = m_cachedPaddedH; + material->m_smoothFactor = pad; + material->m_myIndex = m_cachedMyIndex; + material->m_color = m_group->color(); + material->m_hasInverted = m_cachedHasInverted ? 1 : 0; + material->m_invertedRadius = m_cachedInvertedRadius; + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, + sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, + sizeof(m_cachedInvertedInner)); + + const int count = + static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = m_cachedRects[i]; + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} + BlobInvertedRect::~BlobInvertedRect() { if (m_group) m_group->clearInvertedRect(this); diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp index 958e2a8de..207244ded 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -45,6 +45,8 @@ class BlobInvertedRect : public BlobShape { protected: bool isInvertedRect() const override { return true; } + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + void registerWithGroup() override; void unregisterFromGroup() override; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index 73b5cfe6a..b99bd8e11 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -102,10 +102,6 @@ void BlobShape::updatePolish() { // Ensure all shapes have up-to-date physics (only once per frame) m_group->ensurePhysicsUpdated(); - // When inverted rect renders everything, skip spatial query for others - if (!isInvertedRect() && m_group->invertedRect()) - return; - const QPointF scenePos = mapToScene(QPointF(0, 0)); const float pad = static_cast(m_group->smoothing()); @@ -142,6 +138,10 @@ void BlobShape::updatePolish() { if (other->isInvertedRect()) continue; + // Skip zero-size rects + if (other->width() <= 0 || other->height() <= 0) + continue; + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); bool include = false; @@ -299,12 +299,6 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { return nullptr; } - // When an inverted rect exists, it renders everything in a single pass - if (!isInvertedRect() && m_group->invertedRect()) { - delete oldNode; - return nullptr; - } - auto* node = static_cast(oldNode); if (!node) { node = new QSGGeometryNode; diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index f4baa7317..66912609c 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -85,6 +85,11 @@ void main() { // Offset center for asymmetric deformation vec2 center = rect.xy + props.yz; + // AABB early-out: skip rects far from this pixel + vec2 extent = sh.xy + vec2(smoothFactor * 1.5); + if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) + continue; + // Apply pre-computed inverse deformation to the evaluation point mat2 invDeform = mat2(invDm.xy, invDm.zw); vec2 transformedPixel = center + invDeform * (pixel - center); @@ -157,6 +162,12 @@ void main() { vec2 jSh = rectData[j * 5 + 3].xy; vec2 jC = jR.xy + jP.yz; + // Skip non-adjacent rects + float sinkRange = smoothFactor * 1.5; + if (abs(center.x - jC.x) > iSh.x + jSh.x + sinkRange || + abs(center.y - jC.y) > iSh.y + jSh.y + sinkRange) + continue; + // Penetration of j past i's edges (positive = past) float pT = (jC.y + jSh.y) - (center.y - iSh.y) - sinkOff; float pB = (center.y + iSh.y) - (jC.y - jSh.y) - sinkOff; @@ -266,9 +277,10 @@ void main() { } } - // myIndex == -1: inverted rect renders everything (frame + blobs) - // myIndex >= 0: individual rect renders only its owned pixels - if (myIndex >= 0 && owner != myIndex) + // Each renderer only outputs pixels it owns + // myIndex == -1: inverted rect renders border-owned pixels + // myIndex >= 0: individual rect renders its owned pixels + if (owner != myIndex) discard; float fw = fwidth(mergedSdf); From ebccf801f465f391691e37524acb224d00c6cca1 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 20:27:56 +0100 Subject: [PATCH 162/409] physics throttling, targeted dirty marking, proximity-based border - Snap deformation to rest when imperceptible instead of pumping frames - Only trigger markDirty on geometry changes exceeding 0.5px - Only include inverted rect data when rect is near the border --- plugin/src/Caelestia/Blobs/blobgroup.cpp | 34 ++++++++++++++++ plugin/src/Caelestia/Blobs/blobgroup.hpp | 1 + plugin/src/Caelestia/Blobs/blobrect.cpp | 31 +++++++++++---- plugin/src/Caelestia/Blobs/blobshape.cpp | 50 ++++++++++++++++++------ 4 files changed, 96 insertions(+), 20 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp index f06c4d513..ab4a31e65 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.cpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -66,6 +66,40 @@ void BlobGroup::markDirty() { } } +void BlobGroup::markShapeDirty(BlobShape* source) { + m_physicsUpdated = false; + + source->polish(); + source->update(); + + // Use cached padded rects to find spatial neighbors + const float pad = static_cast(m_smoothing) * 2.0f; + const QRectF srcRect( + static_cast(source->m_cachedPaddedX - pad), + static_cast(source->m_cachedPaddedY - pad), + static_cast(source->m_cachedPaddedW + pad * 2.0f), + static_cast(source->m_cachedPaddedH + pad * 2.0f)); + + for (auto* shape : std::as_const(m_shapes)) { + if (shape == source) + continue; + const QRectF otherRect( + static_cast(shape->m_cachedPaddedX), + static_cast(shape->m_cachedPaddedY), + static_cast(shape->m_cachedPaddedW), + static_cast(shape->m_cachedPaddedH)); + if (srcRect.intersects(otherRect)) { + shape->polish(); + shape->update(); + } + } + + if (m_invertedRect && static_cast(m_invertedRect) != source) { + static_cast(m_invertedRect)->polish(); + static_cast(m_invertedRect)->update(); + } +} + void BlobGroup::ensurePhysicsUpdated() { if (m_physicsUpdated) return; diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp index 4c9ed6915..b2e83edb2 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.hpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -38,6 +38,7 @@ class BlobGroup : public QObject { BlobInvertedRect* invertedRect() const { return m_invertedRect; } void markDirty(); + void markShapeDirty(BlobShape* source); void ensurePhysicsUpdated(); signals: diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index e03baa22a..ff021203f 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -16,13 +16,30 @@ void BlobRect::updatePolish() { BlobShape::updatePolish(); if (m_physicsActive) { - QMetaObject::invokeMethod( - this, - [this]() { - if (m_physicsActive && m_group) - m_group->markDirty(); - }, - Qt::QueuedConnection); + // Check if deformation is visually imperceptible + float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + + std::abs(m_dm11 - 1.0f); + float totalVel = + std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + + if (totalDelta < 0.004f && totalVel < 0.05f) { + // Snap to rest, no visible deformation + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_deformMatrix = QMatrix4x4(); + updateCenteredDeformMatrix(); + m_physicsActive = false; + } else { + QMetaObject::invokeMethod( + this, + [this]() { + if (m_physicsActive && m_group) + m_group->markDirty(); + }, + Qt::QueuedConnection); + } } } diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index b99bd8e11..a7938a50f 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -70,8 +70,15 @@ void BlobShape::geometryChange( const QRectF& newGeometry, const QRectF& oldGeometry) { QQuickItem::geometryChange(newGeometry, oldGeometry); updateCenteredDeformMatrix(); - if (m_group) - m_group->markDirty(); + if (m_group) { + // Only trigger redraw if the change is visually meaningful + const auto dx = std::abs(newGeometry.x() - oldGeometry.x()); + const auto dy = std::abs(newGeometry.y() - oldGeometry.y()); + const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); + const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); + if (dx > 0.5 || dy > 0.5 || dw > 0.5 || dh > 0.5) + m_group->markShapeDirty(this); + } } void BlobShape::updateCenteredDeformMatrix() { @@ -210,9 +217,6 @@ void BlobShape::updatePolish() { auto* inv = m_group->invertedRect(); if (inv) { - m_cachedHasInverted = true; - m_cachedInvertedRadius = static_cast(inv->radius()); - const QPointF invScene = inv->mapToScene(QPointF(0, 0)); const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); @@ -234,15 +238,35 @@ void BlobShape::updatePolish() { outerHH - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); - m_cachedInvertedOuter[0] = outerCX; - m_cachedInvertedOuter[1] = outerCY; - m_cachedInvertedOuter[2] = outerHW; - m_cachedInvertedOuter[3] = outerHH; + // Check if this rect is near the border (within 2x smoothing of inner edge) + bool nearBorder = isInvertedRect(); + if (!nearBorder) { + const float margin = pad * 2.0f; + const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f; + const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f; + const float myHW = m_cachedPaddedW * 0.5f; + const float myHH = m_cachedPaddedH * 0.5f; + // Near border if any edge of padded rect is within margin of inner edge + nearBorder = (myCX - myHW < innerCX - innerHW + margin) || + (myCX + myHW > innerCX + innerHW - margin) || + (myCY - myHH < innerCY - innerHH + margin) || + (myCY + myHH > innerCY + innerHH - margin); + } + + if (nearBorder) { + m_cachedHasInverted = true; + m_cachedInvertedRadius = static_cast(inv->radius()); - m_cachedInvertedInner[0] = innerCX; - m_cachedInvertedInner[1] = innerCY; - m_cachedInvertedInner[2] = innerHW; - m_cachedInvertedInner[3] = innerHH; + m_cachedInvertedOuter[0] = outerCX; + m_cachedInvertedOuter[1] = outerCY; + m_cachedInvertedOuter[2] = outerHW; + m_cachedInvertedOuter[3] = outerHH; + + m_cachedInvertedInner[0] = innerCX; + m_cachedInvertedInner[1] = innerCY; + m_cachedInvertedInner[2] = innerHW; + m_cachedInvertedInner[3] = innerHH; + } } // Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) From 7d01180f5c51022d96fb9c3d8e1c1ffa051026a7 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 22:08:42 +0100 Subject: [PATCH 163/409] Only assign ownership if pixel is within blend zone --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index 66912609c..b5fb7b4c3 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -203,7 +203,7 @@ void main() { } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); - if (d < minDist) { + if (d < smoothFactor && d < minDist) { minDist = d; owner = i; } From 51a12193c3fdd63873f494484267214647ba1d99 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Wed, 25 Mar 2026 22:33:13 +0100 Subject: [PATCH 164/409] correction, discard only if owner != myIndex AND mergedSdf > smoothFactor --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index b5fb7b4c3..90133563a 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -203,7 +203,7 @@ void main() { } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); - if (d < smoothFactor && d < minDist) { + if (d < minDist) { minDist = d; owner = i; } @@ -277,10 +277,11 @@ void main() { } } - // Each renderer only outputs pixels it owns + // Each renderer only outputs pixels it owns, but allow rendering + // blend zones to prevent gaps (mergedSdf < smoothFactor means in blend) // myIndex == -1: inverted rect renders border-owned pixels // myIndex >= 0: individual rect renders its owned pixels - if (owner != myIndex) + if (owner != myIndex && mergedSdf > smoothFactor) discard; float fw = fwidth(mergedSdf); From 8a5905c9dfd0e0b7d8ea0eaefd0e818e5650efa8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 15:31:35 +1100 Subject: [PATCH 165/409] fix unqual access --- modules/drawers/Drawers.qml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index ff1d6a4d6..7a5b15b33 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -153,6 +153,7 @@ Variants { PanelBg { id: dashBg + blobGroup: blobGroup panel: panels.dashboard bar: bar deformAmount: 0.1 @@ -161,6 +162,7 @@ Variants { PanelBg { id: launcherBg + blobGroup: blobGroup panel: panels.launcher bar: bar deformAmount: 0.1 @@ -169,6 +171,7 @@ Variants { PanelBg { id: sessionBg + blobGroup: blobGroup panel: panels.session bar: bar deformAmount: 0.25 @@ -177,6 +180,7 @@ Variants { PanelBg { id: sidebarBg + blobGroup: blobGroup panel: panels.sidebar bar: bar deformAmount: 0 @@ -185,6 +189,7 @@ Variants { PanelBg { id: osdBg + blobGroup: blobGroup panel: panels.osd bar: bar deformAmount: 0.3 @@ -193,6 +198,7 @@ Variants { PanelBg { id: notifsBg + blobGroup: blobGroup panel: panels.notifications bar: bar } @@ -200,6 +206,7 @@ Variants { PanelBg { id: utilsBg + blobGroup: blobGroup panel: panels.utilities bar: bar } @@ -207,6 +214,7 @@ Variants { PanelBg { id: popoutBg + blobGroup: blobGroup panel: panels.popouts bar: bar @@ -294,6 +302,7 @@ Variants { } component PanelBg: BlobRect { + required property BlobGroup blobGroup required property Item panel required property Item bar property real deformAmount: 0.15 From 9c34c232e4fc596147e0a589579fac1540b9426e Mon Sep 17 00:00:00 2001 From: Leith <181516429+leithXD@users.noreply.github.com> Date: Thu, 26 Mar 2026 05:52:03 +0100 Subject: [PATCH 166/409] fix: ignore escape and tab in input fields (#1330) --- modules/bar/popouts/WirelessPassword.qml | 9 +++++++++ modules/controlcenter/network/WirelessPasswordDialog.qml | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index e80e9148e..8858273d7 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -280,6 +280,11 @@ ColumnLayout { forceActiveFocus(); } + if (event.key === Qt.Key_Escape) { + event.accepted = false; + closeDialog(); + } + // Clear error when user starts typing if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; @@ -298,6 +303,10 @@ ColumnLayout { } event.accepted = true; } else if (event.text && event.text.length > 0) { + if (event.key === Qt.Key_Tab) { + event.accepted = false; + return; + } passwordBuffer += event.text; event.accepted = true; } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 40711730a..8700f1c43 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -202,6 +202,11 @@ Item { forceActiveFocus(); } + if (event.key === Qt.Key_Escape) { + event.accepted = false; + closeDialog(); + } + if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } @@ -219,6 +224,10 @@ Item { } event.accepted = true; } else if (event.text && event.text.length > 0) { + if (event.key === Qt.Key_Tab) { + event.accepted = false; + return; + } passwordBuffer += event.text; event.accepted = true; } From d7d53260a7b78740673a384e1faf6773c65eb97b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:13:00 +1100 Subject: [PATCH 167/409] feat: use behavior for launcher anim No need for initial load --- modules/dashboard/Wrapper.qml | 66 ++++++----------------------------- 1 file changed, 10 insertions(+), 56 deletions(-) diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 8448faa6b..cb87bf6fc 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -29,63 +29,18 @@ Item { } readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 + readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled + property real offsetScale: shouldBeActive ? 0 : 1 - visible: anchors.topMargin > -implicitHeight - 5 - anchors.topMargin: -implicitHeight - 5 + visible: offsetScale < 1 + anchors.topMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight - implicitWidth: content.implicitWidth + implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } - - states: State { - name: "visible" - when: root.visibilities.dashboard && Config.dashboard.enabled - - PropertyChanges { - // root.implicitHeight: content.implicitHeight - root.anchors.topMargin: 0 - } - } - - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "topMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root.anchors - // property: "topMargin" - // easing.bezierCurve: Appearance.anim.curves.emphasized - // } - // } - - - ] - - Timer { - id: timer - - running: true - interval: Appearance.anim.durations.extraLarge - onTriggered: { - content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); - content.visible = true; + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } @@ -95,8 +50,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - visible: false - active: true + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities From 528a282e4591d456d7ece792e8c14545bdadf334 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:40:12 +1100 Subject: [PATCH 168/409] feat: improve launcher open No janky stuff, use same Behavior pattern as launcher, and fix height changes breaking. Also allows for no initial load (though we still need to load Apps) And for async launcher loader --- modules/launcher/Wrapper.qml | 98 ++++++------------------------------ 1 file changed, 14 insertions(+), 84 deletions(-) diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index f5c866afe..6c9e09f31 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import qs.modules.launcher.services import qs.components import qs.config @@ -13,7 +14,6 @@ Item { required property var panels readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled - property int contentHeight readonly property real maxHeight: { let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; @@ -22,112 +22,42 @@ Item { return max; } - onMaxHeightChanged: timer.start() - - visible: anchors.bottomMargin > -implicitHeight - 5 - anchors.bottomMargin: -implicitHeight - 5 - implicitHeight: content.implicitHeight - implicitWidth: content.implicitWidth + property real offsetScale: shouldBeActive ? 0 : 1 onShouldBeActiveChanged: { - if (shouldBeActive) { - timer.stop(); - hideAnim.stop(); - showAnim.start(); - } else { - showAnim.stop(); - hideAnim.start(); - } + if (shouldBeActive) + implicitHeight = Qt.binding(() => content.implicitHeight); + else + implicitHeight = implicitHeight; // Break binding during close anim } - SequentialAnimation { - id: showAnim - - Anim { - target: root.anchors - property: "bottomMargin" - to: 0 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - // ScriptAction { - // script: root.implicitHeight = Qt.binding(() => content.implicitHeight) - // } - } + visible: offsetScale < 1 + anchors.bottomMargin: (-implicitHeight - 5) * offsetScale + implicitHeight: content.implicitHeight + implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open - SequentialAnimation { - id: hideAnim + Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init - // ScriptAction { - // script: root.implicitHeight = root.implicitHeight - // } + Behavior on offsetScale { Anim { - target: root.anchors - property: "bottomMargin" - to: -content.implicitHeight - 5 - // easing.bezierCurve: Appearance.anim.curves.emphasized duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - Connections { - function onEnabledChanged(): void { - timer.start(); - } - - function onMaxShownChanged(): void { - timer.start(); - } - - target: Config.launcher - } - - Connections { - function onValuesChanged(): void { - if (DesktopEntries.applications.values.length < Config.launcher.maxShown) - timer.start(); - } - - target: DesktopEntries.applications - } - - Timer { - id: timer - - interval: Appearance.anim.durations.extraLarge - onRunningChanged: { - if (running && !root.shouldBeActive) { - content.visible = false; - content.active = true; - } else { - root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; - if (showAnim.running) { - showAnim.stop(); - showAnim.start(); - } - } - } - } - Loader { id: content anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - visible: false - active: false - Component.onCompleted: timer.start() + asynchronous: true + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities panels: root.panels maxHeight: root.maxHeight - - Component.onCompleted: root.contentHeight = implicitHeight } } } From 600182ae002b21919f4c376348df88bee8194bfc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 16:46:28 +1100 Subject: [PATCH 169/409] feat: improve utilities open/close --- modules/utilities/Wrapper.qml | 67 ++++++----------------------------- 1 file changed, 11 insertions(+), 56 deletions(-) diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 59288f5df..048de1b4c 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -21,77 +21,32 @@ Item { reloadableId: "utilities" } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) + property real offsetScale: shouldBeActive ? 0 : 1 - visible: anchors.bottomMargin > -implicitHeight - 5 - anchors.bottomMargin: -implicitHeight - 5 - implicitHeight: content.implicitHeight + visible: offsetScale < 1 + anchors.bottomMargin: (-implicitHeight - 5) * offsetScale + implicitHeight: content.implicitHeight + content.anchors.margins * 2 implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } - - states: State { - name: "visible" - when: root.shouldBeActive - - PropertyChanges { - root.anchors.bottomMargin: 0 - } - } - - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "bottomMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitHeight" - // easing.bezierCurve: Appearance.anim.curves.emphasized - // } - // } - - ] - - Timer { - id: timer - - running: true - interval: Appearance.anim.durations.extraLarge - onTriggered: { - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } Loader { id: content - asynchronous: true anchors.top: parent.top anchors.left: parent.left anchors.margins: Appearance.padding.large - visible: false - active: true + asynchronous: true + active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: root.implicitWidth - Appearance.padding.large * 2 + implicitWidth: root.implicitWidth - content.anchors.margins * 2 props: root.props visibilities: root.visibilities popouts: root.popouts From 6fa80bd85778cff5a9f7c716a9bff4648884919f Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Thu, 26 Mar 2026 07:13:57 +0100 Subject: [PATCH 170/409] dashboard: lyrics enhancements (#1321) * dual lrc file search strategies, UI adjustments * qml conventions & order * format --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- config/Config.qml | 3 +- config/ServiceConfig.qml | 3 +- modules/dashboard/LyricMenu.qml | 266 +++++++++++++++++++++----------- services/LyricsService.qml | 101 +++++++++--- 4 files changed, 255 insertions(+), 118 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index cb8eb9f7a..91454b598 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -363,7 +363,8 @@ Singleton { smartScheme: services.smartScheme, defaultPlayer: services.defaultPlayer, playerAliases: services.playerAliases, - showLyrics: services.showLyrics + showLyrics: services.showLyrics, + lyricsBackend: services.lyricsBackend }; } diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml index 5f3c24fb1..a9d7c3dcf 100644 --- a/config/ServiceConfig.qml +++ b/config/ServiceConfig.qml @@ -19,5 +19,6 @@ JsonObject { "to": "YT Music" } ] - property bool showLyrics: true + property bool showLyrics: false + property string lyricsBackend: "Auto" } diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index 0dae62467..54b43eecc 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -32,7 +32,7 @@ StyledRect { anchors.margins: Appearance.padding.large spacing: Appearance.spacing.normal - // Header: icon, backend name, refresh, toggle + // Header: icon, backend selector, refresh, toggle RowLayout { Layout.fillWidth: true spacing: Appearance.padding.small @@ -44,12 +44,49 @@ StyledRect { font.pointSize: Appearance.spacing.large } - StyledText { + Rectangle { + Layout.preferredHeight: 24 + Layout.preferredWidth: 80 + radius: Appearance.rounding.small + color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) + + StyledText { + anchors.centerIn: parent + text: LyricsService.preferredBackend + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3primary + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + const backends = ["Auto", "Local", "NetEase"]; + const currentIndex = backends.indexOf(LyricsService.preferredBackend); + const nextIndex = (currentIndex + 1) % backends.length; + LyricsService.preferredBackend = backends[nextIndex]; + LyricsService.loadLyrics(); + } + } + } + + Rectangle { + Layout.preferredHeight: 24 + Layout.preferredWidth: 60 + radius: Appearance.rounding.small + visible: LyricsService.preferredBackend === "Auto" + color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) + + StyledText { + anchors.centerIn: parent + text: LyricsService.backend + font.pointSize: Appearance.font.size.small + color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary + } + } + + Item { Layout.fillWidth: true - text: LyricsService.backend - font.pointSize: Appearance.font.size.normal - color: Colours.palette.m3secondary - elide: Text.ElideRight } IconButton { @@ -66,117 +103,141 @@ StyledRect { StyledText { Layout.fillWidth: true - text: "Fetched Candidates:" + text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small elide: Text.ElideRight + visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 } - // Candidates list - ListView { - id: candidatesView - + // Local file info (shown in Local mode) + Rectangle { Layout.fillWidth: true - Layout.fillHeight: true + Layout.preferredHeight: 48 + visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 + radius: Appearance.rounding.small + color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) + + ColumnLayout { + anchors.fill: parent + anchors.margins: Appearance.padding.small + spacing: 0 - visible: LyricsService.candidatesModel.count > 0 - model: LyricsService.candidatesModel - clip: true - spacing: Appearance.spacing.small - - opacity: visible ? 1 : 0 - // Behavior on opacity { - // NumberAnimation { duration: Appearance.anim.durations.normal } - // } - - delegate: Item { - id: delegateRoot - - required property real id - required property string title - required property string artist - property bool hovered: false - property bool pressed: false - - width: ListView.view.width * 0.98 - height: 70 - anchors.horizontalCenter: parent?.horizontalCenter - scale: hovered ? 1.02 : 1.0 - - Behavior on scale { - NumberAnimation { - duration: Appearance.anim.durations.small - easing.type: Easing.OutCubic + StyledText { + Layout.fillWidth: true + text: { + const path = LyricsService.loadedLocalFile; + const parts = path.split('/'); + return parts[parts.length - 1]; } + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3tertiary + elide: Text.ElideMiddle } - Rectangle { - id: background + StyledText { + Layout.fillWidth: true + text: { + const path = LyricsService.loadedLocalFile; + const parts = path.split('/'); + if (parts.length > 2) { + return parts.slice(-3, -1).join('/'); + } + return ""; + } + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + elide: Text.ElideMiddle + } + } + } - anchors.fill: parent - radius: Appearance.rounding.small + // Candidates list + Loader { + Layout.fillWidth: true + Layout.fillHeight: true - color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) + active: LyricsService.preferredBackend !== "Local" - border.width: delegateRoot.hovered ? 1 : 0 - border.color: Colours.palette.m3primary + sourceComponent: ListView { + id: candidatesView - Behavior on color { - ColorAnimation { - duration: Appearance.anim.durations.small - } - } - Behavior on border.width { + model: LyricsService.candidatesModel + clip: true + spacing: Appearance.spacing.small + visible: LyricsService.candidatesModel.count > 0 + opacity: visible ? 1 : 0 + + delegate: Item { + id: delegateRoot + + required property real id + required property string title + required property string artist + + property bool hovered: false + property bool pressed: false + + width: ListView.view.width * 0.98 + height: 70 + + anchors.horizontalCenter: parent?.horizontalCenter + scale: hovered ? 1.02 : 1.0 + + Behavior on scale { NumberAnimation { duration: Appearance.anim.durations.small + easing.type: Easing.OutCubic } } - } - MouseArea { - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.PointingHandCursor + Rectangle { + id: background - onEntered: delegateRoot.hovered = true - onExited: delegateRoot.hovered = false - onPressed: delegateRoot.pressed = true - onReleased: delegateRoot.pressed = false - onClicked: LyricsService.selectCandidate(delegateRoot.id) - } + anchors.fill: parent + radius: Appearance.rounding.small - Row { - anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small + color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) - // Active indicator bar - Rectangle { - width: 4 - height: parent.height * 0.6 - radius: 2 - anchors.verticalCenter: parent.verticalCenter - color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" + border.width: delegateRoot.hovered ? 1 : 0 + border.color: Colours.palette.m3primary Behavior on color { ColorAnimation { duration: Appearance.anim.durations.small } } + Behavior on border.width { + NumberAnimation { + duration: Appearance.anim.durations.small + } + } } - Column { - anchors.verticalCenter: parent.verticalCenter - width: parent.width - 30 - spacing: 4 + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor - Text { - text: delegateRoot.title - font.pointSize: Appearance.font.size.normal - font.bold: true - color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface - width: parent.width - elide: Text.ElideRight + onEntered: delegateRoot.hovered = true + onExited: delegateRoot.hovered = false + onPressed: delegateRoot.pressed = true + onReleased: delegateRoot.pressed = false + onClicked: LyricsService.selectCandidate(delegateRoot.id) + } + + Row { + anchors.fill: parent + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.small + + // Active indicator bar + Rectangle { + width: 4 + height: parent.height * 0.6 + radius: 2 + anchors.verticalCenter: parent.verticalCenter + color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" Behavior on color { ColorAnimation { @@ -185,11 +246,32 @@ StyledRect { } } - Text { - text: delegateRoot.artist - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3onSurfaceVariant - elide: Text.ElideRight + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 30 + spacing: 4 + + Text { + text: delegateRoot.title + font.pointSize: Appearance.font.size.normal + font.bold: true + color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface + width: parent.width + elide: Text.ElideRight + + Behavior on color { + ColorAnimation { + duration: Appearance.anim.durations.small + } + } + } + + Text { + text: delegateRoot.artist + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + elide: Text.ElideRight + } } } } @@ -198,7 +280,7 @@ StyledRect { Item { Layout.fillHeight: true - visible: LyricsService.candidatesModel.count == 0 + visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local" } // Manual search diff --git a/services/LyricsService.qml b/services/LyricsService.qml index 53be9f90a..d41b93b4b 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -17,22 +17,17 @@ Singleton { property bool isManualSeeking: false property bool lyricsVisible: Config.services.showLyrics property string backend: "Local" + property string preferredBackend: Config.services.lyricsBackend property real currentSongId: 0 - + property string loadedLocalFile: "" property real offset + property int currentRequestId: 0 + property var lyricsMap: ({}) readonly property string lyricsDir: Paths.absolutePath(Config.paths.lyricsDir) readonly property string lyricsMapFile: Paths.absolutePath(Config.paths.lyricsDir) + "/lyrics_map.json" - - property int currentRequestId: 0 - - // The data source for the UI readonly property alias model: lyricsModel readonly property alias candidatesModel: fetchedCandidatesModel - - property var lyricsMap: ({}) - - // shared headers for all NetEase requests readonly property var _netEaseHeaders: ({ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", "Referer": "https://music.163.com/" @@ -90,7 +85,6 @@ Singleton { lyricsModel.clear(); currentIndex = -1; root.currentSongId = 0; - root.backend = "Local"; root.currentRequestId++; let requestId = root.currentRequestId; @@ -99,29 +93,47 @@ Singleton { let saved = root.lyricsMap[key]; root.offset = saved?.offset ?? 0.0; - if (saved?.neteaseId && saved?.backend === "NetEase") { + if (root.preferredBackend === "NetEase") { root.backend = "NetEase"; - root.currentSongId = saved.neteaseId; - fetchNetEaseLyrics(saved.neteaseId, requestId); - fetchNetEaseCandidates(meta.title, meta.artist, requestId); + fetchNetEase(meta.title, meta.artist, requestId); return; } - if (saved?.backend === "NetEase") { - fallbackTimer.restart(); + if (root.preferredBackend === "Local") { + root.backend = "Local"; + let cleanDir = lyricsDir.replace(/\/$/, ""); + let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; + + // Search for files matching "Artist - Title.lrc" pattern + const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); + const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); + const escapedTitle = titleStr.replace(/'/g, "'\\''"); + const escapedArtist = artistStr.replace(/'/g, "'\\''"); + findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; + findLyricsInSubdirs.requestId = requestId; + findLyricsInSubdirs.running = true; + + lrcFile.path = ""; + lrcFile.path = flatPath; return; } + // Auto mode: try local first + root.backend = "Local"; let cleanDir = lyricsDir.replace(/\/$/, ""); - let fullPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; + let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; - lrcFile.path = ""; - lrcFile.path = fullPath; - fetchNetEaseCandidates(meta.title, meta.artist, requestId); //to populate the list regardless + const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); + const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); + const escapedTitle = titleStr.replace(/'/g, "'\\''"); + const escapedArtist = artistStr.replace(/'/g, "'\\''"); + findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; + findLyricsInSubdirs.requestId = requestId; + findLyricsInSubdirs.running = true; - // if the file is missing, FileView will not fire onLoaded, so we arm the fallback timer here as a safety net. It is cancelled in onLoaded if the file loads successfully. - if (saved?.backend !== "Local") - fallbackTimer.restart(); + lrcFile.path = ""; + lrcFile.path = flatPath; + fetchNetEaseCandidates(meta.title, meta.artist, requestId); } function updateModel(parsedArray) { @@ -256,6 +268,13 @@ Singleton { seekTimer.restart(); } + onPreferredBackendChanged: { + if (Config.services.lyricsBackend !== preferredBackend) { + Config.services.lyricsBackend = preferredBackend; + Config.save(); + } + } + ListModel { id: lyricsModel } @@ -314,12 +333,15 @@ Singleton { let parsed = Lrc.parseLrc(text()); if (parsed.length > 0) { root.backend = "Local"; + root.loadedLocalFile = path; updateModel(parsed); loading = false; - } else { + } else if (root.preferredBackend === "Local") { + // Local mode only - fail immediately root.backend = "NetEase"; fallbackToOnline(); } + // In Auto mode, let the Process onExited handle fallback } } @@ -346,4 +368,35 @@ Singleton { command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] } + + Process { + id: findLyricsInSubdirs + + property int requestId: -1 + property bool foundFile: false + + stdout: SplitParser { + onRead: data => { + if (findLyricsInSubdirs.requestId === root.currentRequestId) { + const foundPath = data.trim(); + if (foundPath && foundPath.length > 0) { + findLyricsInSubdirs.foundFile = true; + fallbackTimer.stop(); + root.loadedLocalFile = foundPath; + lrcFile.path = ""; + lrcFile.path = foundPath; + } + } + } + } + + onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters + if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") { + if (lyricsModel.count === 0) { + fallbackTimer.restart(); + } + } + foundFile = false; + } + } } From ea2ed3e5c4f17569c9cd3ebfd0e53680205ebcc4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:22:51 +1100 Subject: [PATCH 171/409] fix: graphical artifact during sdf deformation --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index 90133563a..f6387a2f8 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -203,7 +203,7 @@ void main() { } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); - if (d < minDist) { + if (d < smoothFactor && d < minDist) { minDist = d; owner = i; } From 0075d64ca27d3ffdd396827a6c0b6143542e983f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 26 Mar 2026 21:01:40 +1100 Subject: [PATCH 172/409] fix: osd and session clipping --- modules/drawers/Drawers.qml | 6 ++-- modules/drawers/Interactions.qml | 16 ++++----- modules/drawers/Panels.qml | 56 ++++++++++++++++++++++---------- modules/osd/Wrapper.qml | 48 +++++++++------------------ modules/session/Wrapper.qml | 52 ++++++++++------------------- modules/sidebar/Wrapper.qml | 47 ++++++--------------------- 6 files changed, 95 insertions(+), 130 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 7a5b15b33..f4f4fc71f 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -172,9 +172,10 @@ Variants { id: sessionBg blobGroup: blobGroup - panel: panels.session + panel: panels.sessionWrapper bar: bar deformAmount: 0.25 + x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth } PanelBg { @@ -190,9 +191,10 @@ Variants { id: osdBg blobGroup: blobGroup - panel: panels.osd + panel: panels.osdWrapper bar: bar deformAmount: 0.3 + x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth } PanelBg { diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 6008a853a..c906b9d1b 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -104,7 +104,7 @@ CustomMouseArea { if (panels.sidebar.anchors.rightMargin === -panels.sidebar.implicitWidth - 5) { // Show osd on hover - const showOsd = inRightPanel(panels.osd, x, y); + const showOsd = inRightPanel(panels.osdWrapper, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { @@ -119,23 +119,23 @@ CustomMouseArea { const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); // Show/hide session on drag - if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) visibilities.session = false; // Show sidebar on drag if in session area and session is nearly fully visible - if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) + if (showSidebar && panels.session.offsetScale <= 0 && dragX < -Config.sidebar.dragThreshold) visibilities.sidebar = true; } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { // Show sidebar on drag if not in session area visibilities.sidebar = true; } } else { - const outOfSidebar = x < width - panels.sidebar.width; + const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale); // Show osd on hover - const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); + const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { @@ -148,7 +148,7 @@ CustomMouseArea { } // Show/hide session on drag - if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (pressed && outOfSidebar && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) @@ -221,7 +221,7 @@ CustomMouseArea { // Also hide dashboard and OSD if they're not being hovered const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); if (!inDashboardArea) { root.visibilities.dashboard = false; @@ -249,7 +249,7 @@ CustomMouseArea { function onOsdChanged() { if (root.visibilities.osd) { // OSD became visible, immediately check if this should be shortcut mode - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); if (!inOsdArea) { root.osdShortcutActive = true; } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 006205fd4..151401038 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -21,8 +21,10 @@ Item { required property Bar.BarWrapper bar readonly property alias osd: osd + readonly property alias osdWrapper: osdWrapper readonly property alias notifications: notifications readonly property alias session: session + readonly property alias sessionWrapper: sessionWrapper readonly property alias launcher: launcher readonly property alias dashboard: dashboard readonly property alias popouts: popouts @@ -34,16 +36,27 @@ Item { anchors.margins: Config.border.thickness anchors.leftMargin: bar.implicitWidth - Osd.Wrapper { - id: osd - - clip: session.width > 0 || sidebar.width > 0 - screen: root.screen - visibilities: root.visibilities + Item { + id: osdWrapper anchors.verticalCenter: parent.verticalCenter - anchors.right: session.left - // anchors.rightMargin: session.width + sidebar.width + anchors.right: parent.right + anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale) + clip: sidebar.visible || session.visible + + implicitWidth: osd.implicitWidth + implicitHeight: osd.implicitHeight + + Osd.Wrapper { + id: osd + + screen: root.screen + visibilities: root.visibilities + sidebarOrSessionVisible: sidebar.visible || session.visible + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } } Notifications.Wrapper { @@ -58,16 +71,26 @@ Item { anchors.right: parent.right } - Session.Wrapper { - id: session - - clip: sidebar.width > 0 - visibilities: root.visibilities - panels: root + Item { + id: sessionWrapper anchors.verticalCenter: parent.verticalCenter - anchors.right: sidebar.left - // anchors.rightMargin: sidebar.width + anchors.right: parent.right + anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale) + clip: sidebar.visible + + implicitWidth: session.implicitWidth + implicitHeight: session.implicitHeight + + Session.Wrapper { + id: session + + visibilities: root.visibilities + sidebarVisible: sidebar.visible + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } } Launcher.Wrapper { @@ -131,7 +154,6 @@ Item { id: sidebar visibilities: root.visibilities - panels: root anchors.top: notifications.bottom anchors.bottom: utilities.top diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 87350d926..39215cd87 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -11,9 +11,13 @@ Item { required property ShellScreen screen required property DrawerVisibilities visibilities + required property bool sidebarOrSessionVisible + property bool hovered readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) + property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: !shouldBeActive && sidebarOrSessionVisible ? 16 : 0 property real volume property bool muted @@ -34,44 +38,24 @@ Item { brightness = root.monitor?.brightness ?? 0; } - visible: anchors.rightMargin > -implicitWidth - anchors.rightMargin: -implicitWidth + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight - states: State { - name: "visible" - when: root.shouldBeActive - - PropertyChanges { - root.anchors.rightMargin: 0 + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "rightMargin" - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Behavior on sidebarOffset { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitWidth" - // easing.bezierCurve: Appearance.anim.curves.emphasized - // } - // } - - - ] + } Connections { function onMutedChanged(): void { @@ -122,7 +106,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { monitor: root.monitor diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index b829409b8..32d6fea2a 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -8,47 +8,31 @@ Item { id: root required property DrawerVisibilities visibilities - required property var panels + required property bool sidebarVisible readonly property real nonAnimWidth: content.implicitWidth - visible: anchors.rightMargin > -implicitWidth - 1 - anchors.rightMargin: -implicitWidth - 1 - implicitWidth: content.implicitWidth - implicitHeight: content.implicitHeight + readonly property bool shouldBeActive: visibilities.session && Config.session.enabled + property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: !shouldBeActive && sidebarVisible ? 14 : 0 // TODO: there is clearly something wrong with the rect to rect edge sink - states: State { - name: "visible" - when: root.visibilities.session && Config.session.enabled + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale + implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open - PropertyChanges { - root.anchors.rightMargin: 0 + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "rightMargin" - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + Behavior on sidebarOffset { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitWidth" - // easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - // } - // } - - - ] + } Loader { id: content @@ -56,7 +40,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 67929bd7e..4a777ca39 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -8,48 +8,22 @@ Item { id: root required property DrawerVisibilities visibilities - required property var panels readonly property Props props: Props {} - visible: anchors.rightMargin > -implicitWidth - 5 - anchors.rightMargin: -implicitWidth - 5 - implicitWidth: Config.sidebar.sizes.width + readonly property bool shouldBeActive: visibilities.sidebar && Config.sidebar.enabled + property real offsetScale: shouldBeActive ? 0 : 1 - states: State { - name: "visible" - when: root.visibilities.sidebar && Config.sidebar.enabled + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5) * offsetScale + implicitWidth: Config.sidebar.sizes.width - PropertyChanges { - root.anchors.rightMargin: 0 + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - transitions: [ - Transition { - // from: "" - // to: "visible" - - Anim { - target: root.anchors - property: "rightMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - // Transition { - // from: "visible" - // to: "" - - // Anim { - // target: root - // property: "implicitWidth" - // easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - // } - // } - - - ] - Loader { id: content @@ -59,8 +33,7 @@ Item { anchors.margins: Appearance.padding.large anchors.bottomMargin: 0 - active: true - Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2 From 148417baa118351b5a5f7c22ee43f520553b642d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chlo=C3=A9=20Legu=C3=A9?= Date: Thu, 26 Mar 2026 23:46:13 -0400 Subject: [PATCH 173/409] fix: weather service using UTC instead of locale timezone (#1343) --- services/Weather.qml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/services/Weather.qml b/services/Weather.qml index d226365fd..b74ae55f3 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -128,7 +128,7 @@ Singleton { const forecastList = []; for (let i = 0; i < json.daily.time.length; i++) forecastList.push({ - date: json.daily.time[i], + date: json.daily.time[i].replace(/-/g, "/"), maxTempC: Math.round(json.daily.temperature_2m_max[i]), maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), minTempC: Math.round(json.daily.temperature_2m_min[i]), @@ -141,7 +141,8 @@ Singleton { const hourlyList = []; const now = new Date(); for (let i = 0; i < json.hourly.time.length; i++) { - const time = new Date(json.hourly.time[i]); + const time = new Date(json.hourly.time[i].replace("T", " ")); + if (time < now) continue; From e8ffd51442fa5e190a6c3972c92b03ed9c2a1fc2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:21:04 +1100 Subject: [PATCH 174/409] fix: window mask regions during overshoot The window mask regions followed the panel positions exactly, so when they overshoot due to the open anim there will be a gap at the top. --- modules/drawers/Drawers.qml | 28 ++----------- modules/drawers/Regions.qml | 80 +++++++++++++++++++++++++++++++++++++ 2 files changed, 84 insertions(+), 24 deletions(-) create mode 100644 modules/drawers/Regions.qml diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index f4f4fc71f..c1d684f21 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -58,14 +58,10 @@ Variants { WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - mask: Region { - x: bar.clampedWidth + win.dragMaskPadding - y: Config.border.clampedThickness + win.dragMaskPadding - width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 - height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 - intersection: Intersection.Xor - - regions: regions.instances // qmllint disable stale-property-read + mask: Regions { + bar: bar + panels: panels + win: win } anchors.top: true @@ -73,22 +69,6 @@ Variants { anchors.left: true anchors.right: true - Variants { - id: regions - - model: panels.children - - Region { - required property Item modelData - - x: modelData.x + bar.implicitWidth - y: modelData.y + Config.border.thickness - width: modelData.width - height: modelData.height - intersection: Intersection.Subtract - } - } - HyprlandFocusGrab { id: focusGrab diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml new file mode 100644 index 000000000..c646809fd --- /dev/null +++ b/modules/drawers/Regions.qml @@ -0,0 +1,80 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.config +import qs.modules.bar as Bar + +Region { + id: root + + required property Bar.BarWrapper bar + required property Panels panels + required property var win + + x: bar.clampedWidth + win.dragMaskPadding + y: Config.border.clampedThickness + win.dragMaskPadding + width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 + height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 + intersection: Intersection.Xor + + R { + panel: root.panels.dashboard + y: 0 + height: panel.height * (1 - root.panels.dashboard.offsetScale) + Config.border.thickness + } + + R { + panel: root.panels.launcher + y: root.win.height - height + height: panel.height * (1 - root.panels.launcher.offsetScale) + Config.border.thickness + } + + R { + id: sessionRegion + + panel: root.panels.sessionWrapper + x: root.win.width - width + width: panel.width * (1 - root.panels.session.offsetScale) + Config.border.thickness + sidebarRegion.width + } + + R { + id: sidebarRegion + + panel: root.panels.sidebar + x: root.win.width - width + width: panel.width * (1 - root.panels.sidebar.offsetScale) + Config.border.thickness + } + + R { + panel: root.panels.osdWrapper + x: root.win.width - width + width: panel.width * (1 - root.panels.osd.offsetScale) + Config.border.thickness + sessionRegion.width + } + + R { + panel: root.panels.notifications + y: 0 + height: panel.height + Config.border.thickness + } + + R { + panel: root.panels.utilities + y: root.win.height - height + height: panel.height * (1 - root.panels.utilities.offsetScale) + Config.border.thickness + } + + R { + panel: root.panels.popouts + } + + component R: Region { + required property Item panel + + x: panel.x + root.bar.implicitWidth + y: panel.y + Config.border.thickness + width: panel.width + height: panel.height + intersection: Intersection.Subtract + } +} From 2c5e433b73c2001c6c7d084757b8e2f9a4ef715f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 15:21:32 +1100 Subject: [PATCH 175/409] fix: clean up notif wrapper and format --- modules/launcher/Wrapper.qml | 2 +- modules/notifications/Wrapper.qml | 21 +-------------------- 2 files changed, 2 insertions(+), 21 deletions(-) diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 6c9e09f31..472667e01 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.modules.launcher.services import qs.components import qs.config +import qs.modules.launcher.services Item { id: root diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index c0756b9a7..97417e9cf 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -1,6 +1,5 @@ import QtQuick import qs.components -import qs.config Item { id: root @@ -10,29 +9,11 @@ Item { property alias osdPanel: content.osdPanel property alias sessionPanel: content.sessionPanel - visible: anchors.topMargin > -implicitHeight - 5 + visible: height > 0 anchors.topMargin: -5 implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) implicitHeight: content.implicitHeight - states: State { - name: "hidden" - // when: root.visibilities.sidebar && Config.sidebar.enabled - - PropertyChanges { - root.anchors.topMargin: -implicitHeight - 5 - } - } - - transitions: Transition { - Anim { - target: root.anchors - property: "topMargin" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Content { id: content From 49695f8fd0163041e125c7ac72d0c9740676e9fa Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Fri, 27 Mar 2026 08:47:05 +0100 Subject: [PATCH 176/409] audio: add default output sink cycle ipc (#1200) * [CI] chore: update flake * [CI] chore: update flake * [CI] chore: update flake * [CI] chore: update flake * audio: added default audio output cycle IPC * fix format --------- Co-authored-by: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- services/Audio.qml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/services/Audio.qml b/services/Audio.qml index 100f00101..e2d936a04 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -2,6 +2,7 @@ pragma Singleton import QtQuick import Quickshell +import Quickshell.Io import Quickshell.Services.Pipewire import Caelestia import Caelestia.Services @@ -67,6 +68,15 @@ Singleton { Pipewire.preferredDefaultAudioSource = newSource; } + function cycleNextAudioOutput(): void { + if (sinks.length === 0) + return; + + const currentIndex = sinks.findIndex(s => s === sink); + const nextIndex = (currentIndex + 1) % sinks.length; + setAudioSink(sinks[nextIndex]); + } + function setStreamVolume(stream: PwNode, newVolume: real): void { if (stream?.ready && stream?.audio) { stream.audio.muted = false; @@ -162,4 +172,12 @@ Singleton { BeatTracker { id: beatTracker } + + IpcHandler { + function cycleOutput(): void { + root.cycleNextAudioOutput(); + } + + target: "audio" + } } From f2d023b13141541046faf6aadd9ca8d7d3e2382e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 18:56:55 +1100 Subject: [PATCH 177/409] fix: osd hover area + popup notif osd/session dodging --- modules/drawers/Drawers.qml | 2 ++ modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 8 ++++---- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index c1d684f21..5211fdcf0 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -156,6 +156,7 @@ Variants { bar: bar deformAmount: 0.25 x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth + implicitWidth: panels.session.width } PanelBg { @@ -175,6 +176,7 @@ Variants { bar: bar deformAmount: 0.3 x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth + implicitWidth: panels.osd.width } PanelBg { diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index c906b9d1b..fbf8ccadd 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -102,7 +102,7 @@ CustomMouseArea { visibilities.bar = false; } - if (panels.sidebar.anchors.rightMargin === -panels.sidebar.implicitWidth - 5) { + if (panels.sidebar.offsetScale === 1) { // Show osd on hover const showOsd = inRightPanel(panels.osdWrapper, x, y); diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 151401038..01144d09c 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -44,7 +44,7 @@ Item { anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale) clip: sidebar.visible || session.visible - implicitWidth: osd.implicitWidth + implicitWidth: osd.implicitWidth * (1 - osd.offsetScale) implicitHeight: osd.implicitHeight Osd.Wrapper { @@ -64,8 +64,8 @@ Item { visibilities: root.visibilities sidebarPanel: sidebar - osdPanel: osd - sessionPanel: session + osdPanel: osdWrapper + sessionPanel: sessionWrapper anchors.top: parent.top anchors.right: parent.right @@ -79,7 +79,7 @@ Item { anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale) clip: sidebar.visible - implicitWidth: session.implicitWidth + implicitWidth: session.implicitWidth * (1 - session.offsetScale) implicitHeight: session.implicitHeight Session.Wrapper { From 14437cc1edf4b6417536227eb9c89be5dd3a1b7b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:03:55 +1100 Subject: [PATCH 178/409] fix: remove osd and session wrapper hacks The panels are ignored if they are closed, so they don't produce bulges. This does remove the sink effect on overshoot, but that didn't work anyways. (also that change was part of the previous commit) --- modules/osd/Wrapper.qml | 10 +--------- modules/session/Wrapper.qml | 10 +--------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 39215cd87..db72c3e5b 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -17,7 +17,6 @@ Item { readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) property real offsetScale: shouldBeActive ? 0 : 1 - property real sidebarOffset: !shouldBeActive && sidebarOrSessionVisible ? 16 : 0 property real volume property bool muted @@ -39,7 +38,7 @@ Item { } visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale + anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight @@ -50,13 +49,6 @@ Item { } } - Behavior on sidebarOffset { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Connections { function onMutedChanged(): void { root.show(); diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 32d6fea2a..ab929c010 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -13,10 +13,9 @@ Item { readonly property bool shouldBeActive: visibilities.session && Config.session.enabled property real offsetScale: shouldBeActive ? 0 : 1 - property real sidebarOffset: !shouldBeActive && sidebarVisible ? 14 : 0 // TODO: there is clearly something wrong with the rect to rect edge sink visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale + anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open @@ -27,13 +26,6 @@ Item { } } - Behavior on sidebarOffset { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Loader { id: content From c4399efbe4f2583dea977018ec6d387744ad3559 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:17:49 +1100 Subject: [PATCH 179/409] fix: launcher anim Async launcher makes anim worse cause height gets recalculated during anim, removing overshoot and slowing down curve --- modules/launcher/Wrapper.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 472667e01..0c9871aa0 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -51,7 +51,6 @@ Item { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - asynchronous: true active: root.shouldBeActive || root.visible sourceComponent: Content { From ff574737818112487333b0ad4f1fdd0b1bbdcaa5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 19:23:54 +1100 Subject: [PATCH 180/409] fix: account for bottom panel offset when checking hover Basically only affects utilities panel cause it's in the corner --- modules/drawers/Interactions.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fbf8ccadd..15958ea1e 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -44,7 +44,8 @@ CustomMouseArea { } function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { - return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panel.height) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); + const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property + return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); } function onWheel(event: WheelEvent): void { From 0f5d2ddc16046437f229117804da5d5f6b7c18b3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:54:52 +1100 Subject: [PATCH 181/409] feat: fade in/out drawers --- modules/dashboard/Wrapper.qml | 1 + modules/launcher/Wrapper.qml | 1 + modules/osd/Wrapper.qml | 2 ++ modules/session/Wrapper.qml | 1 + modules/sidebar/Wrapper.qml | 1 + modules/utilities/Wrapper.qml | 1 + 6 files changed, 7 insertions(+) diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index cb87bf6fc..d411d4984 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -36,6 +36,7 @@ Item { anchors.topMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open + opacity: 1 - offsetScale Behavior on offsetScale { Anim { diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 0c9871aa0..5dfffeaef 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -35,6 +35,7 @@ Item { anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open + opacity: 1 - offsetScale Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index db72c3e5b..585fb959d 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -41,6 +41,7 @@ Item { anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight + opacity: 1 - offsetScale Behavior on offsetScale { Anim { @@ -98,6 +99,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left + asynchronous: true active: root.shouldBeActive || root.visible sourceComponent: Content { diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index ab929c010..e48fff606 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -18,6 +18,7 @@ Item { anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open + opacity: 1 - offsetScale Behavior on offsetScale { Anim { diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 4a777ca39..eefd11a0f 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -16,6 +16,7 @@ Item { visible: offsetScale < 1 anchors.rightMargin: (-implicitWidth - 5) * offsetScale implicitWidth: Config.sidebar.sizes.width + opacity: 1 - offsetScale Behavior on offsetScale { Anim { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 048de1b4c..b0aec96e3 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -27,6 +27,7 @@ Item { anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight + content.anchors.margins * 2 implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width + opacity: 1 - offsetScale Behavior on offsetScale { Anim { From e8555061aba2708198e43d2eeb09038500271436 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 20:58:00 +1100 Subject: [PATCH 182/409] fix: sidebar bulge at connection with utilities Basically make sidebar and utilities exclude each other from merging and manually animate corner radius Also format c++ --- modules/drawers/Drawers.qml | 17 +++ plugin/src/Caelestia/Blobs/blobgroup.cpp | 13 +- plugin/src/Caelestia/Blobs/blobgroup.hpp | 3 +- .../src/Caelestia/Blobs/blobinvertedrect.cpp | 49 ++++--- .../src/Caelestia/Blobs/blobinvertedrect.hpp | 12 +- plugin/src/Caelestia/Blobs/blobmaterial.cpp | 10 +- plugin/src/Caelestia/Blobs/blobmaterial.hpp | 11 +- plugin/src/Caelestia/Blobs/blobrect.cpp | 138 +++++++++++++++--- plugin/src/Caelestia/Blobs/blobrect.hpp | 55 ++++++- plugin/src/Caelestia/Blobs/blobshape.cpp | 119 +++++++-------- plugin/src/Caelestia/Blobs/blobshape.hpp | 10 +- plugin/src/Caelestia/Blobs/shaders/blob.frag | 7 +- 12 files changed, 286 insertions(+), 158 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 5211fdcf0..23be6f038 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -166,6 +166,15 @@ Variants { panel: panels.sidebar bar: bar deformAmount: 0 + height: panel.height + 1 + exclude: [utilsBg] + bottomLeftRadius: panels.sidebar.visible ? 0 : radius + + Behavior on bottomLeftRadius { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } } PanelBg { @@ -193,6 +202,14 @@ Variants { blobGroup: blobGroup panel: panels.utilities bar: bar + exclude: [sidebarBg] + topLeftRadius: panels.sidebar.visible ? 0 : radius + + Behavior on topLeftRadius { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + } + } } PanelBg { diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp index ab4a31e65..a4703c860 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.cpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -74,20 +74,15 @@ void BlobGroup::markShapeDirty(BlobShape* source) { // Use cached padded rects to find spatial neighbors const float pad = static_cast(m_smoothing) * 2.0f; - const QRectF srcRect( - static_cast(source->m_cachedPaddedX - pad), - static_cast(source->m_cachedPaddedY - pad), - static_cast(source->m_cachedPaddedW + pad * 2.0f), + const QRectF srcRect(static_cast(source->m_cachedPaddedX - pad), + static_cast(source->m_cachedPaddedY - pad), static_cast(source->m_cachedPaddedW + pad * 2.0f), static_cast(source->m_cachedPaddedH + pad * 2.0f)); for (auto* shape : std::as_const(m_shapes)) { if (shape == source) continue; - const QRectF otherRect( - static_cast(shape->m_cachedPaddedX), - static_cast(shape->m_cachedPaddedY), - static_cast(shape->m_cachedPaddedW), - static_cast(shape->m_cachedPaddedH)); + const QRectF otherRect(static_cast(shape->m_cachedPaddedX), static_cast(shape->m_cachedPaddedY), + static_cast(shape->m_cachedPaddedW), static_cast(shape->m_cachedPaddedH)); if (srcRect.intersects(otherRect)) { shape->polish(); shape->update(); diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp index b2e83edb2..e09125a96 100644 --- a/plugin/src/Caelestia/Blobs/blobgroup.hpp +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -11,8 +11,7 @@ class BlobInvertedRect; class BlobGroup : public QObject { Q_OBJECT QML_ELEMENT - Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY - smoothingChanged) + Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) public: diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp index 3e392b4af..46ee73a4a 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -13,21 +13,36 @@ BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) static void setFrameIndices(quint16* idx) { // Top strip: 0-1-4, 1-5-4 - idx[0] = 0; idx[1] = 1; idx[2] = 4; - idx[3] = 1; idx[4] = 5; idx[5] = 4; + idx[0] = 0; + idx[1] = 1; + idx[2] = 4; + idx[3] = 1; + idx[4] = 5; + idx[5] = 4; // Right strip: 1-2-5, 2-6-5 - idx[6] = 1; idx[7] = 2; idx[8] = 5; - idx[9] = 2; idx[10] = 6; idx[11] = 5; + idx[6] = 1; + idx[7] = 2; + idx[8] = 5; + idx[9] = 2; + idx[10] = 6; + idx[11] = 5; // Bottom strip: 2-3-6, 3-7-6 - idx[12] = 2; idx[13] = 3; idx[14] = 6; - idx[15] = 3; idx[16] = 7; idx[17] = 6; + idx[12] = 2; + idx[13] = 3; + idx[14] = 6; + idx[15] = 3; + idx[16] = 7; + idx[17] = 6; // Left strip: 3-0-7, 0-4-7 - idx[18] = 3; idx[19] = 0; idx[20] = 7; - idx[21] = 0; idx[22] = 4; idx[23] = 7; + idx[18] = 3; + idx[19] = 0; + idx[20] = 7; + idx[21] = 0; + idx[22] = 4; + idx[23] = 7; } -QSGNode* BlobInvertedRect::updatePaintNode( - QSGNode* oldNode, UpdatePaintNodeData*) { +QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { if (!m_group) { delete oldNode; return nullptr; @@ -55,9 +70,8 @@ QSGNode* BlobInvertedRect::updatePaintNode( delete oldNode; node = new QSGGeometryNode; - auto* geometry = new QSGGeometry( - QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, - QSGGeometry::UnsignedShortType); + auto* geometry = + new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType); geometry->setDrawingMode(QSGGeometry::DrawTriangles); node->setGeometry(geometry); node->setFlag(QSGNode::OwnsGeometry); @@ -105,13 +119,10 @@ QSGNode* BlobInvertedRect::updatePaintNode( material->m_color = m_group->color(); material->m_hasInverted = m_cachedHasInverted ? 1 : 0; material->m_invertedRadius = m_cachedInvertedRadius; - memcpy(material->m_invertedOuter, m_cachedInvertedOuter, - sizeof(m_cachedInvertedOuter)); - memcpy(material->m_invertedInner, m_cachedInvertedInner, - sizeof(m_cachedInvertedInner)); + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); - const int count = - static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); material->m_rectCount = count; for (int i = 0; i < count; ++i) material->m_rects[i] = m_cachedRects[i]; diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp index 207244ded..f7fa6c0a5 100644 --- a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -7,14 +7,10 @@ class BlobInvertedRect : public BlobShape { Q_OBJECT QML_ELEMENT - Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY - borderLeftChanged) - Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY - borderRightChanged) - Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY - borderTopChanged) - Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY - borderBottomChanged) + Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged) + Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged) + Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged) + Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged) public: explicit BlobInvertedRect(QQuickItem* parent = nullptr); diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp index f2ead2ed2..f71ad1bde 100644 --- a/plugin/src/Caelestia/Blobs/blobmaterial.cpp +++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp @@ -7,8 +7,7 @@ QSGMaterialType* BlobMaterial::type() const { return &s_type; } -QSGMaterialShader* BlobMaterial::createShader( - QSGRendererInterface::RenderMode) const { +QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const { return new BlobMaterialShader; } @@ -25,8 +24,7 @@ BlobMaterialShader::BlobMaterialShader() { setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); } -bool BlobMaterialShader::updateUniformData( - RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { +bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { Q_UNUSED(oldMaterial); auto* mat = static_cast(newMaterial); QByteArray* buf = state.uniformData(); @@ -85,13 +83,13 @@ bool BlobMaterialShader::updateUniformData( const auto& r = mat->m_rects[i]; const int base = 160 + i * 80; const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; - const float d1[4] = { r.radius, r.offsetX, r.offsetY, r.minEig }; + const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig }; const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; memcpy(buf->data() + base, d0, 16); memcpy(buf->data() + base + 16, d1, 16); memcpy(buf->data() + base + 32, r.invDeform, 16); memcpy(buf->data() + base + 48, d3, 16); - memcpy(buf->data() + base + 64, r.cornerFill, 16); + memcpy(buf->data() + base + 64, r.radius, 16); } return true; diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp index ef80fea1a..85dbedaa0 100644 --- a/plugin/src/Caelestia/Blobs/blobmaterial.hpp +++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp @@ -6,22 +6,20 @@ struct BlobRectData { float cx = 0, cy = 0, hw = 0, hh = 0; - float radius = 0; float offsetX = 0, offsetY = 0; float minEig = 1.0f; // Inverse of 2x2 deformation matrix, column-major for GLSL float invDeform[4] = { 1, 0, 0, 1 }; // Screen-space AABB half-extents of the deformed rect float screenHalfX = 0, screenHalfY = 0; - // Pre-computed corner fill factors (tr, br, bl, tl) - float cornerFill[4] = { 1, 1, 1, 1 }; + // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU + float radius[4] = { 0, 0, 0, 0 }; }; class BlobMaterial : public QSGMaterial { public: QSGMaterialType* type() const override; - QSGMaterialShader* createShader( - QSGRendererInterface::RenderMode) const override; + QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override; int compare(const QSGMaterial* other) const override; float m_paddedX = 0; @@ -42,6 +40,5 @@ class BlobMaterial : public QSGMaterial { class BlobMaterialShader : public QSGMaterialShader { public: BlobMaterialShader(); - bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, - QSGMaterial* oldMaterial) override; + bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override; }; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index ff021203f..efc1e7f08 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -17,10 +17,8 @@ void BlobRect::updatePolish() { if (m_physicsActive) { // Check if deformation is visually imperceptible - float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) - + std::abs(m_dm11 - 1.0f); - float totalVel = - std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); + float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); if (totalDelta < 0.004f && totalVel < 0.05f) { // Snap to rest, no visible deformation @@ -62,10 +60,8 @@ void BlobRect::updatePhysics() { return; } - const float velX = - static_cast(scenePos.x() - m_prevScenePos.x()) / dt; - const float velY = - static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + const float velX = static_cast(scenePos.x() - m_prevScenePos.x()) / dt; + const float velY = static_cast(scenePos.y() - m_prevScenePos.y()) / dt; m_prevScenePos = scenePos; const float speed = std::sqrt(velX * velX + velY * velY); @@ -86,8 +82,7 @@ void BlobRect::updatePhysics() { float target11 = 1.0f; if (speed > 5.0f) { - const float targetStretch = - 1.0f + std::min(speed * kStretchFactor, kMaxStretch); + const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); const float targetCompress = 1.0f / targetStretch; const float cosA = velX / speed; @@ -105,35 +100,132 @@ void BlobRect::updatePhysics() { const float kStiffness = static_cast(m_stiffness); const float kDamping = static_cast(m_damping); - const float accel00 = - -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; + const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; m_dmVel00 += accel00 * dt; m_dm00 += m_dmVel00 * dt; - const float accel01 = - -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; + const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; m_dmVel01 += accel01 * dt; m_dm01 += m_dmVel01 * dt; - const float accel11 = - -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; + const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; m_dmVel11 += accel11 * dt; m_dm11 += m_dmVel11 * dt; - m_deformMatrix = QMatrix4x4( - m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); updateCenteredDeformMatrix(); checkAtRest(speed); } +void BlobRect::setTopLeftRadius(qreal r) { + if (!qFuzzyCompare(m_topLeftRadius, r)) { + m_topLeftRadius = r; + emit topLeftRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setTopRightRadius(qreal r) { + if (!qFuzzyCompare(m_topRightRadius, r)) { + m_topRightRadius = r; + emit topRightRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setBottomLeftRadius(qreal r) { + if (!qFuzzyCompare(m_bottomLeftRadius, r)) { + m_bottomLeftRadius = r; + emit bottomLeftRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setBottomRightRadius(qreal r) { + if (!qFuzzyCompare(m_bottomRightRadius, r)) { + m_bottomRightRadius = r; + emit bottomRightRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::cornerRadii(float out[4]) const { + const auto base = static_cast(m_radius); + out[0] = m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base; + out[1] = m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base; + out[2] = m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base; + out[3] = m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base; +} + +bool BlobRect::isExcluded(const BlobShape* other) const { + for (const auto& ptr : m_exclude) { + if (ptr == other) + return true; + } + return false; +} + +QQmlListProperty BlobRect::exclude() { + return QQmlListProperty( + this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); +} + +void BlobRect::excludeAppend(QQmlListProperty* prop, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_exclude.append(rect); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +qsizetype BlobRect::excludeCount(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + return self->m_exclude.size(); +} + +BlobRect* BlobRect::excludeAt(QQmlListProperty* prop, qsizetype index) { + auto* self = static_cast(prop->object); + return self->m_exclude.at(index); +} + +void BlobRect::excludeClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) + return; + self->m_exclude.clear(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect::excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_exclude[index] = rect; + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect::excludeRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) + return; + self->m_exclude.removeLast(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + void BlobRect::checkAtRest(float speed) { constexpr float kEpsilon = 0.002f; - const bool atRest = - std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && - std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && - std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && - speed < 5.0f; + const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && + std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && + std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f; if (atRest) { m_dm00 = 1.0f; diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp index 86d4666d3..d2d6ad453 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.hpp +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -3,17 +3,22 @@ #include "blobshape.hpp" #include +#include #include +#include class BlobRect : public BlobShape { Q_OBJECT QML_ELEMENT - Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY - stiffnessChanged) + Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged) + Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) + Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) + Q_PROPERTY(QQmlListProperty exclude READ exclude NOTIFY excludeChanged) + Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) + Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) + Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) Q_PROPERTY( - qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) - Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY - deformScaleChanged) + qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged) public: explicit BlobRect(QQuickItem* parent = nullptr); @@ -46,10 +51,36 @@ class BlobRect : public BlobShape { } } + QQmlListProperty exclude(); + + bool isExcluded(const BlobShape* other) const override; + void cornerRadii(float out[4]) const override; + + qreal topLeftRadius() const { return m_topLeftRadius; } + + void setTopLeftRadius(qreal r); + + qreal topRightRadius() const { return m_topRightRadius; } + + void setTopRightRadius(qreal r); + + qreal bottomLeftRadius() const { return m_bottomLeftRadius; } + + void setBottomLeftRadius(qreal r); + + qreal bottomRightRadius() const { return m_bottomRightRadius; } + + void setBottomRightRadius(qreal r); + signals: void stiffnessChanged(); void dampingChanged(); void deformScaleChanged(); + void excludeChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void bottomRightRadiusChanged(); protected: void updatePolish() override; @@ -78,4 +109,18 @@ class BlobRect : public BlobShape { qreal m_stiffness = 200.0; qreal m_damping = 16.0; qreal m_deformScale = 0.0005; + + qreal m_topLeftRadius = -1; + qreal m_topRightRadius = -1; + qreal m_bottomLeftRadius = -1; + qreal m_bottomRightRadius = -1; + + QList> m_exclude; + + static void excludeAppend(QQmlListProperty* prop, BlobRect* rect); + static qsizetype excludeCount(QQmlListProperty* prop); + static BlobRect* excludeAt(QQmlListProperty* prop, qsizetype index); + static void excludeClear(QQmlListProperty* prop); + static void excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); + static void excludeRemoveLast(QQmlListProperty* prop); }; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index a7938a50f..b048c57ed 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -19,8 +19,7 @@ static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { return std::max(extraX, extraY); } -static float cpuSdBox(float px, float py, float cx, float cy, float hw, - float hh) { +static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) { const float dx = std::abs(px - cx) - hw; const float dy = std::abs(py - cy) - hh; const float mdx = std::max(dx, 0.0f); @@ -66,8 +65,7 @@ void BlobShape::componentComplete() { registerWithGroup(); } -void BlobShape::geometryChange( - const QRectF& newGeometry, const QRectF& oldGeometry) { +void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { QQuickItem::geometryChange(newGeometry, oldGeometry); updateCenteredDeformMatrix(); if (m_group) { @@ -92,6 +90,14 @@ void BlobShape::updateCenteredDeformMatrix() { emit deformMatrixChanged(); } +void BlobShape::cornerRadii(float out[4]) const { + const auto r = static_cast(m_radius); + out[0] = r; + out[1] = r; + out[2] = r; + out[3] = r; +} + void BlobShape::registerWithGroup() { if (m_group) m_group->addShape(this); @@ -127,19 +133,15 @@ void BlobShape::updatePolish() { m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; - m_localPaddedRect = QRectF(static_cast(-totalPad), - static_cast(-totalPad), - width() + 2.0 * static_cast(totalPad), - height() + 2.0 * static_cast(totalPad)); + m_localPaddedRect = QRectF(static_cast(-totalPad), static_cast(-totalPad), + width() + 2.0 * static_cast(totalPad), height() + 2.0 * static_cast(totalPad)); } // Filter nearby normal rects m_cachedRects.clear(); m_cachedMyIndex = -2; - const QRectF myPadded(static_cast(m_cachedPaddedX), - static_cast(m_cachedPaddedY), - static_cast(m_cachedPaddedW), - static_cast(m_cachedPaddedH)); + const QRectF myPadded(static_cast(m_cachedPaddedX), static_cast(m_cachedPaddedY), + static_cast(m_cachedPaddedW), static_cast(m_cachedPaddedH)); for (BlobShape* other : m_group->shapes()) { if (other->isInvertedRect()) @@ -149,6 +151,9 @@ void BlobShape::updatePolish() { if (other->width() <= 0 || other->height() <= 0) continue; + if (isExcluded(other)) + continue; + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); bool include = false; @@ -157,12 +162,9 @@ void BlobShape::updatePolish() { } else { const float otherHW = static_cast(other->width()) * 0.5f; const float otherHH = static_cast(other->height()) * 0.5f; - const float otherPad = - pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); - const QRectF otherPadded( - otherScene.x() - static_cast(otherPad), - otherScene.y() - static_cast(otherPad), - other->width() + 2.0 * static_cast(otherPad), + const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); + const QRectF otherPadded(otherScene.x() - static_cast(otherPad), + otherScene.y() - static_cast(otherPad), other->width() + 2.0 * static_cast(otherPad), other->height() + 2.0 * static_cast(otherPad)); include = myPadded.intersects(otherPadded); } @@ -180,14 +182,13 @@ void BlobShape::updatePolish() { r.cy = static_cast(otherScene.y() + other->height() / 2.0); r.hw = static_cast(other->width() / 2.0); r.hh = static_cast(other->height() / 2.0); - r.radius = static_cast(other->radius()); + other->cornerRadii(r.radius); r.offsetX = dm(0, 3); r.offsetY = dm(1, 3); // Pre-compute inverse deformation matrix const float det = a * d - c * b; - const float invDet = - std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; + const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; r.invDeform[0] = d * invDet; r.invDeform[1] = -b * invDet; r.invDeform[2] = -c * invDet; @@ -218,25 +219,15 @@ void BlobShape::updatePolish() { auto* inv = m_group->invertedRect(); if (inv) { const QPointF invScene = inv->mapToScene(QPointF(0, 0)); - const float outerCX = - static_cast(invScene.x() + inv->width() / 2.0); - const float outerCY = - static_cast(invScene.y() + inv->height() / 2.0); + const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); + const float outerCY = static_cast(invScene.y() + inv->height() / 2.0); const float outerHW = static_cast(inv->width() / 2.0); const float outerHH = static_cast(inv->height() / 2.0); - const float innerCX = - outerCX + - static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); - const float innerCY = - outerCY + - static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); - const float innerHW = - outerHW - - static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); - const float innerHH = - outerHH - - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); + const float innerCX = outerCX + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); + const float innerCY = outerCY + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); + const float innerHW = outerHW - static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); + const float innerHH = outerHH - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); // Check if this rect is near the border (within 2x smoothing of inner edge) bool nearBorder = isInvertedRect(); @@ -247,10 +238,8 @@ void BlobShape::updatePolish() { const float myHW = m_cachedPaddedW * 0.5f; const float myHH = m_cachedPaddedH * 0.5f; // Near border if any edge of padded rect is within margin of inner edge - nearBorder = (myCX - myHW < innerCX - innerHW + margin) || - (myCX + myHW > innerCX + innerHW - margin) || - (myCY - myHH < innerCY - innerHH + margin) || - (myCY + myHH > innerCY + innerHH - margin); + nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) || + (myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin); } if (nearBorder) { @@ -269,8 +258,9 @@ void BlobShape::updatePolish() { } } - // Pre-compute corner fill factors (moves O(N²) work from GPU to CPU) + // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) const float smoothFactor = pad; + constexpr float minR = 2.0f; const auto rectCount = m_cachedRects.size(); for (qsizetype i = 0; i < rectCount; ++i) { auto& ri = m_cachedRects[i]; @@ -285,14 +275,10 @@ void BlobShape::updatePolish() { if (j == i) continue; const auto& rj = m_cachedRects[j]; - fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); - fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); - fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, - cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); } if (m_cachedHasInverted) { @@ -300,20 +286,17 @@ void BlobShape::updatePolish() { const float icy = m_cachedInvertedInner[1]; const float ihw = m_cachedInvertedInner[2]; const float ihh = m_cachedInvertedInner[3]; - fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); - fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); - fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); - fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, - -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); } - ri.cornerFill[0] = fTr; - ri.cornerFill[1] = fBr; - ri.cornerFill[2] = fBl; - ri.cornerFill[3] = fTl; + // Combine base radii with fill factors into effective per-corner radii + ri.radius[0] = std::max(ri.radius[0] * fTr, minR); + ri.radius[1] = std::max(ri.radius[1] * fBr, minR); + ri.radius[2] = std::max(ri.radius[2] * fBl, minR); + ri.radius[3] = std::max(ri.radius[3] * fTl, minR); } } @@ -327,8 +310,7 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { if (!node) { node = new QSGGeometryNode; - auto* geometry = new QSGGeometry( - QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); node->setGeometry(geometry); node->setFlag(QSGNode::OwnsGeometry); @@ -366,13 +348,10 @@ QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { material->m_color = m_group->color(); material->m_hasInverted = m_cachedHasInverted ? 1 : 0; material->m_invertedRadius = m_cachedInvertedRadius; - memcpy(material->m_invertedOuter, m_cachedInvertedOuter, - sizeof(m_cachedInvertedOuter)); - memcpy(material->m_invertedInner, m_cachedInvertedInner, - sizeof(m_cachedInvertedInner)); + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); - const int count = - static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); material->m_rectCount = count; for (int i = 0; i < count; ++i) material->m_rects[i] = m_cachedRects[i]; diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index 8c6f11655..6383b644c 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -12,8 +12,7 @@ class BlobShape : public QQuickItem { Q_OBJECT Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) - Q_PROPERTY( - QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) friend class BlobGroup; @@ -38,13 +37,16 @@ class BlobShape : public QQuickItem { protected: void componentComplete() override; - void geometryChange( - const QRectF& newGeometry, const QRectF& oldGeometry) override; + void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; void updatePolish() override; QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; virtual bool isInvertedRect() const { return false; } + virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } + + virtual void cornerRadii(float out[4]) const; + virtual void updatePhysics() {} virtual void registerWithGroup(); diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index f6387a2f8..808cf3091 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -80,7 +80,7 @@ void main() { vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 - vec4 fills = rectData[i * 5 + 4]; // f_tr, f_br, f_bl, f_tl + vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) // Offset center for asymmetric deformation vec2 center = rect.xy + props.yz; @@ -94,10 +94,7 @@ void main() { mat2 invDeform = mat2(invDm.xy, invDm.zw); vec2 transformedPixel = center + invDeform * (pixel - center); - // Use pre-computed corner fill factors - float br = props.x; - float minR = 2.0; - vec4 radii = max(br * fills, vec4(minR)); + // Use pre-computed effective per-corner radii float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); // Use pre-computed minimum eigenvalue for SDF correction From b739e74247057e6a65ec7d3de639f9161067ba06 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 27 Mar 2026 21:03:39 +1100 Subject: [PATCH 183/409] ci: add qt shadertools dep to ci image --- .github/workflows/update-image.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml index 23006dd0c..74fe5435c 100644 --- a/.github/workflows/update-image.yml +++ b/.github/workflows/update-image.yml @@ -23,7 +23,7 @@ jobs: run: | cat > /tmp/Dockerfile <> /etc/sudoers && \ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ From 611f8de4001ad8ec886e2e79ebf9c3c232af2654 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:49:48 +1100 Subject: [PATCH 184/409] fix: sidebar close anim & gap w/ utilities Don't exclude each other when not flush to fix close overshoot not looking right Also increase sidebar height by 2 instead of 1 to fix gap --- modules/drawers/Drawers.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 23be6f038..2e1598d45 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -166,8 +166,8 @@ Variants { panel: panels.sidebar bar: bar deformAmount: 0 - height: panel.height + 1 - exclude: [utilsBg] + height: panel.height + 2 + exclude: panels.sidebar.offsetScale > 0 ? [] : [utilsBg] bottomLeftRadius: panels.sidebar.visible ? 0 : radius Behavior on bottomLeftRadius { @@ -202,7 +202,7 @@ Variants { blobGroup: blobGroup panel: panels.utilities bar: bar - exclude: [sidebarBg] + exclude: panels.sidebar.offsetScale > 0 ? [] : [sidebarBg] topLeftRadius: panels.sidebar.visible ? 0 : radius Behavior on topLeftRadius { From 42a1f34ddbe0f0e35e0c2d54d94eeaddf2956001 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:17:36 +1100 Subject: [PATCH 185/409] chore: fix property-override linter warnings --- components/controls/SplitButtonRow.qml | 1 - components/controls/StyledInputField.qml | 3 +- components/controls/SwitchRow.qml | 1 - modules/background/DesktopClock.qml | 36 ++++++++++++------------ modules/bar/Bar.qml | 2 +- modules/bar/popouts/Wrapper.qml | 2 +- modules/dashboard/Content.qml | 16 +++++------ modules/dashboard/Dash.qml | 5 ++-- modules/dashboard/Tabs.qml | 12 ++++---- modules/dashboard/Wrapper.qml | 2 +- modules/dashboard/dash/Calendar.qml | 18 ++++++------ modules/dashboard/dash/User.qml | 1 - services/Nmcli.qml | 14 ++++----- 13 files changed, 54 insertions(+), 59 deletions(-) diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index b4f9cf0c3..a58e46d45 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -12,7 +12,6 @@ StyledRect { required property string label property int expandedZ: 100 - property bool enabled: true property alias menuItems: splitButton.menuItems property alias active: splitButton.active diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index f56f79fb3..c7b66dda4 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -13,8 +13,7 @@ Item { property var validator: null property bool readOnly: false property int horizontalAlignment: TextInput.AlignHCenter - property int implicitWidth: 70 - property bool enabled: true + implicitWidth: 70 // Expose activeFocus through alias to avoid FINAL property override readonly property alias hasFocus: inputField.activeFocus diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 6c82d72cc..076080b64 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -10,7 +10,6 @@ StyledRect { required property string label required property bool checked - property bool enabled: true property var onToggled: function (checked) {} Layout.fillWidth: true diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 86f9c623f..67f77e44e 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -14,7 +14,7 @@ Item { required property real absX required property real absY - property real scale: Config.background.desktopClock.scale + property real clockScale: Config.background.desktopClock.scale readonly property bool bgEnabled: Config.background.desktopClock.background.enabled readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled readonly property bool invertColors: Config.background.desktopClock.invertColors @@ -23,8 +23,8 @@ Item { readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary - implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.scale) - implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.scale) + implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.clockScale) + implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.clockScale) Item { id: clockContainer @@ -63,7 +63,7 @@ Item { visible: root.bgEnabled anchors.fill: parent - radius: Appearance.rounding.large * root.scale + radius: Appearance.rounding.large * root.clockScale opacity: Config.background.desktopClock.background.opacity color: Colours.palette.m3surface @@ -74,29 +74,29 @@ Item { id: layout anchors.centerIn: parent - spacing: Appearance.spacing.larger * root.scale + spacing: Appearance.spacing.larger * root.clockScale RowLayout { spacing: Appearance.spacing.small StyledText { text: Time.hourStr - font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale + font.pointSize: Appearance.font.size.extraLarge * 3 * root.clockScale font.weight: Font.Bold color: root.safePrimary } StyledText { text: ":" - font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale + font.pointSize: Appearance.font.size.extraLarge * 3 * root.clockScale color: root.safeTertiary opacity: 0.8 - Layout.topMargin: -Appearance.padding.large * 1.5 * root.scale + Layout.topMargin: -Appearance.padding.large * 1.5 * root.clockScale } StyledText { text: Time.minuteStr - font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale + font.pointSize: Appearance.font.size.extraLarge * 3 * root.clockScale font.weight: Font.Bold color: root.safeSecondary } @@ -104,14 +104,14 @@ Item { Loader { asynchronous: true Layout.alignment: Qt.AlignTop - Layout.topMargin: Appearance.padding.large * 1.4 * root.scale + Layout.topMargin: Appearance.padding.large * 1.4 * root.clockScale active: Config.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr - font.pointSize: Appearance.font.size.large * root.scale + font.pointSize: Appearance.font.size.large * root.clockScale color: root.safeSecondary } } @@ -119,9 +119,9 @@ Item { StyledRect { Layout.fillHeight: true - Layout.preferredWidth: 4 * root.scale - Layout.topMargin: Appearance.spacing.larger * root.scale - Layout.bottomMargin: Appearance.spacing.larger * root.scale + Layout.preferredWidth: 4 * root.clockScale + Layout.topMargin: Appearance.spacing.larger * root.clockScale + Layout.bottomMargin: Appearance.spacing.larger * root.clockScale radius: Appearance.rounding.full color: root.safePrimary opacity: 0.8 @@ -132,7 +132,7 @@ Item { StyledText { text: Time.format("MMMM").toUpperCase() - font.pointSize: Appearance.font.size.large * root.scale + font.pointSize: Appearance.font.size.large * root.clockScale font.letterSpacing: 4 font.weight: Font.Bold color: root.safeSecondary @@ -140,7 +140,7 @@ Item { StyledText { text: Time.format("dd") - font.pointSize: Appearance.font.size.extraLarge * root.scale + font.pointSize: Appearance.font.size.extraLarge * root.clockScale font.letterSpacing: 2 font.weight: Font.Medium color: root.safePrimary @@ -148,7 +148,7 @@ Item { StyledText { text: Time.format("dddd") - font.pointSize: Appearance.font.size.larger * root.scale + font.pointSize: Appearance.font.size.larger * root.clockScale font.letterSpacing: 2 color: root.safeSecondary } @@ -156,7 +156,7 @@ Item { } } - Behavior on scale { + Behavior on clockScale { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index b82a17c74..38ed564b5 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -171,7 +171,7 @@ ColumnLayout { } component WrappedLoader: Loader { - required property bool enabled + required enabled required property string id required property int index diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 06bec9b0b..3a9074332 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -22,7 +22,7 @@ Item { property alias currentName: popoutState.currentName property real currentCenter property alias hasCurrent: popoutState.hasCurrent - readonly property PopoutState state: popoutState + readonly property PopoutState popState: popoutState property string detachedMode property string queuedMode diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 5b3bbbe6b..957389f86 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -21,7 +21,7 @@ Item { } return false; } - required property DashboardState state + required property DashboardState dashState required property FileDialog facePicker readonly property var dashboardTabs: { @@ -70,7 +70,7 @@ Item { anchors.margins: Appearance.padding.large nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 - state: root.state + dashState: root.dashState tabs: root.dashboardTabs } @@ -89,7 +89,7 @@ Item { Flickable { id: view - readonly property int currentIndex: root.state.currentTab + readonly property int currentIndex: root.dashState.currentTab readonly property Item currentItem: { repeater.count; // Trigger update on count change return repeater.itemAt(currentIndex); @@ -112,9 +112,9 @@ Item { const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 2) - root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); + root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 2) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); } onDragEnded: { @@ -123,9 +123,9 @@ Item { const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 10) - root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); + root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 10) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); else contentX = Qt.binding(() => currentItem?.x ?? 0); } @@ -166,7 +166,7 @@ Item { Dash { visibilities: root.visibilities - state: root.state + dashState: root.dashState facePicker: root.facePicker } } diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index c0657f58e..59620d88a 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -9,7 +9,7 @@ GridLayout { id: root required property DrawerVisibilities visibilities - required property DashboardState state + required property DashboardState dashState required property FileDialog facePicker rowSpacing: Appearance.spacing.normal @@ -27,7 +27,6 @@ GridLayout { id: user visibilities: root.visibilities - state: root.state facePicker: root.facePicker } } @@ -67,7 +66,7 @@ GridLayout { Calendar { id: calendar - state: root.state + dashState: root.dashState } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index adeab928d..7d8ff051c 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -13,7 +13,7 @@ Item { id: root required property real nonAnimWidth - required property DashboardState state + required property DashboardState dashState required property var tabs readonly property alias count: bar.count @@ -27,10 +27,10 @@ Item { anchors.right: parent.right anchors.top: parent.top - currentIndex: root.state.currentTab + currentIndex: root.dashState.currentTab background: null - onCurrentIndexChanged: root.state.currentTab = currentIndex + onCurrentIndexChanged: root.dashState.currentTab = currentIndex Repeater { model: ScriptModel { @@ -113,9 +113,9 @@ Item { function onWheel(event: WheelEvent): void { if (event.angleDelta.y < 0) - root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); + root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, bar.count - 1); else if (event.angleDelta.y > 0) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); } implicitWidth: Math.max(icon.width, label.width) @@ -124,7 +124,7 @@ Item { cursorShape: Qt.PointingHandCursor onPressed: event => { - root.state.currentTab = tab.TabBar.index; + root.dashState.currentTab = tab.TabBar.index; const stateY = stateWrapper.y; rippleAnim.x = event.x; diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 596c21b75..a7449689c 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -96,7 +96,7 @@ Item { sourceComponent: Content { visibilities: root.visibilities - state: root.dashState + dashState: root.dashState facePicker: root.facePicker } } diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index bed3c4d34..64e43f5c0 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -12,16 +12,16 @@ import qs.config CustomMouseArea { id: root - required property var state + required property DashboardState dashState - readonly property int currMonth: state.currentDate.getMonth() - readonly property int currYear: state.currentDate.getFullYear() + readonly property int currMonth: dashState.currentDate.getMonth() + readonly property int currYear: dashState.currentDate.getFullYear() function onWheel(event: WheelEvent): void { if (event.angleDelta.y > 0) - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1); else if (event.angleDelta.y < 0) - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } anchors.left: parent.left @@ -29,7 +29,7 @@ CustomMouseArea { implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 acceptedButtons: Qt.MiddleButton - onClicked: root.state.currentDate = new Date() + onClicked: root.dashState.currentDate = new Date() ColumnLayout { id: inner @@ -52,7 +52,7 @@ CustomMouseArea { id: prevMonthStateLayer function onClicked(): void { - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1); } radius: Appearance.rounding.full @@ -77,7 +77,7 @@ CustomMouseArea { StateLayer { function onClicked(): void { - root.state.currentDate = new Date(); + root.dashState.currentDate = new Date(); } anchors.fill: monthYearDisplay @@ -112,7 +112,7 @@ CustomMouseArea { id: nextMonthStateLayer function onClicked(): void { - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } radius: Appearance.rounding.full diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 5fb71effa..7dc47f748 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -11,7 +11,6 @@ Row { id: root required property DrawerVisibilities visibilities - required property DashboardState state required property FileDialog facePicker padding: Appearance.padding.large diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 7af9513bb..dc783f8af 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -168,7 +168,7 @@ Singleton { function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); - proc.command = ["nmcli", ...args]; + proc.cmdArgs = ["nmcli", ...args]; proc.callback = callback; activeProcesses.push(proc); @@ -181,7 +181,7 @@ Singleton { }); Qt.callLater(() => { - proc.exec(proc.command); + proc.exec(proc.cmdArgs); }); } @@ -840,7 +840,7 @@ Singleton { return false; } - if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { + if (!isConnectionCommand(proc.cmdArgs) || !root.pendingConnection || !root.pendingConnection.callback) { return false; } @@ -1117,7 +1117,7 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { + if (root.isConnectionCommand(proc.cmdArgs)) { const needsPassword = root.detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection) { @@ -1204,7 +1204,7 @@ Singleton { if (proc && proc.stderr && proc.stderr.text) { const error = proc.stderr.text.trim(); if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { + if (root.isConnectionCommand(proc.cmdArgs)) { const needsPassword = root.detectPasswordRequired(error); if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { @@ -1281,7 +1281,7 @@ Singleton { id: proc property var callback: null - property list command: [] + property list cmdArgs: [] property bool callbackCalled: false property int exitCode: 0 @@ -1321,7 +1321,7 @@ Singleton { const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; const success = exitCode === 0; - const cmdIsConnection = isConnectionCommand(proc.command); + const cmdIsConnection = isConnectionCommand(proc.cmdArgs); if (root.handlePasswordRequired(proc, error, output, exitCode)) { processFinished(); From be53c42f0dd773096612948d2877f662f6e97670 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 19:51:37 +1100 Subject: [PATCH 186/409] chore: fix format --- components/controls/StyledInputField.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index c7b66dda4..fb312128c 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -13,14 +13,15 @@ Item { property var validator: null property bool readOnly: false property int horizontalAlignment: TextInput.AlignHCenter - implicitWidth: 70 // Expose activeFocus through alias to avoid FINAL property override readonly property alias hasFocus: inputField.activeFocus signal textEdited(string text) + signal editingFinished + implicitWidth: 70 implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 StyledRect { From 04efa4d39da737b6662cb3a574b5b58068ce6022 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:06:49 +1100 Subject: [PATCH 187/409] feat: make utils anim to sidebar width --- modules/drawers/Drawers.qml | 23 ++++++---------------- modules/utilities/Wrapper.qml | 36 +++++++++++++++++++++++++++++++++-- 2 files changed, 40 insertions(+), 19 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 2e1598d45..4d37a530c 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -165,16 +165,10 @@ Variants { blobGroup: blobGroup panel: panels.sidebar bar: bar - deformAmount: 0 + deformAmount: 0.05 height: panel.height + 2 - exclude: panels.sidebar.offsetScale > 0 ? [] : [utilsBg] - bottomLeftRadius: panels.sidebar.visible ? 0 : radius - - Behavior on bottomLeftRadius { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - } - } + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] + bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius } PanelBg { @@ -202,14 +196,9 @@ Variants { blobGroup: blobGroup panel: panels.utilities bar: bar - exclude: panels.sidebar.offsetScale > 0 ? [] : [sidebarBg] - topLeftRadius: panels.sidebar.visible ? 0 : radius - - Behavior on topLeftRadius { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - } - } + deformAmount: panels.sidebar.visible ? 0.1 : 0.15 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] + topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius } PanelBg { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index b0aec96e3..23199aa2e 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -5,12 +5,13 @@ import Quickshell import qs.components import qs.config import qs.modules.bar.popouts as BarPopouts +import qs.modules.sidebar as Sidebar Item { id: root required property DrawerVisibilities visibilities - required property Item sidebar + required property Sidebar.Wrapper sidebar required property BarPopouts.Wrapper popouts readonly property PersistentProperties props: PersistentProperties { @@ -22,13 +23,44 @@ Item { } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarLerp visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight + content.anchors.margins * 2 - implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width + implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp) opacity: 1 - offsetScale + states: State { + name: "attachedToSidebar" + when: root.visibilities.sidebar + + PropertyChanges { + root.sidebarLerp: 1 + } + } + + transitions: [ + Transition { + from: "" + + Anim { + property: "sidebarLerp" + duration: Appearance.anim.durations.expressiveDefaultSpatial / 2 + easing.bezierCurve: Appearance.anim.curves.standardAccel + } + }, + Transition { + to: "" + + Anim { + property: "sidebarLerp" + duration: Appearance.anim.durations.expressiveDefaultSpatial / 2 + easing.bezierCurve: Appearance.anim.curves.standardDecel + } + } + ] + Behavior on offsetScale { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial From 6cdecb88259cdc1a4140de8fb3d022b156dab5eb Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:28:21 +1100 Subject: [PATCH 188/409] fix: sidebar gap with utils due to deform Also better sync utils width with sidebar width --- modules/drawers/Drawers.qml | 4 +++- modules/utilities/Wrapper.qml | 3 ++- plugin/src/Caelestia/Blobs/blobrect.cpp | 3 +++ plugin/src/Caelestia/Blobs/blobshape.cpp | 6 ++++-- plugin/src/Caelestia/Blobs/blobshape.hpp | 3 +++ 5 files changed, 15 insertions(+), 4 deletions(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 4d37a530c..b18ecc849 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -166,7 +166,7 @@ Variants { panel: panels.sidebar bar: bar deformAmount: 0.05 - height: panel.height + 2 + implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius } @@ -247,6 +247,8 @@ Variants { visibilities: visibilities bar: bar + utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 + dashboard.transform: Matrix4x4 { matrix: dashBg.deformMatrix } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 23199aa2e..107f1f933 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -13,6 +13,7 @@ Item { required property DrawerVisibilities visibilities required property Sidebar.Wrapper sidebar required property BarPopouts.Wrapper popouts + property real horizontalStretch readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false @@ -28,7 +29,7 @@ Item { visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight + content.anchors.margins * 2 - implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp) + implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp) opacity: 1 - offsetScale states: State { diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index efc1e7f08..fa7b4b1ed 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -27,6 +27,7 @@ void BlobRect::updatePolish() { m_dm11 = 1.0f; m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; m_deformMatrix = QMatrix4x4(); + emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); m_physicsActive = false; } else { @@ -113,6 +114,7 @@ void BlobRect::updatePhysics() { m_dm11 += m_dmVel11 * dt; m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); checkAtRest(speed); @@ -235,6 +237,7 @@ void BlobRect::checkAtRest(float speed) { m_dmVel01 = 0.0f; m_dmVel11 = 0.0f; m_deformMatrix = QMatrix4x4(); // identity + emit rawDeformMatrixChanged(); updateCenteredDeformMatrix(); m_physicsActive = false; } diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index b048c57ed..966621500 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -86,8 +86,10 @@ void BlobShape::updateCenteredDeformMatrix() { result.translate(cx, cy); result *= m_deformMatrix; result.translate(-cx, -cy); - m_centeredDeformMatrix = result; - emit deformMatrixChanged(); + if (m_centeredDeformMatrix != result) { + m_centeredDeformMatrix = result; + emit deformMatrixChanged(); + } } void BlobShape::cornerRadii(float out[4]) const { diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index 6383b644c..9165579cc 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -13,6 +13,7 @@ class BlobShape : public QQuickItem { Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged) friend class BlobGroup; @@ -29,11 +30,13 @@ class BlobShape : public QQuickItem { void setRadius(qreal r); QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } signals: void groupChanged(); void radiusChanged(); void deformMatrixChanged(); + void rawDeformMatrixChanged(); protected: void componentComplete() override; From 99dec4ea6bccc962de80ae4c43fed21b56176299 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:31:28 +1100 Subject: [PATCH 189/409] chore: fix format --- modules/utilities/Wrapper.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 107f1f933..cb4bcaa01 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -4,8 +4,8 @@ import QtQuick import Quickshell import qs.components import qs.config -import qs.modules.bar.popouts as BarPopouts import qs.modules.sidebar as Sidebar +import qs.modules.bar.popouts as BarPopouts Item { id: root From 4ed544948577179a8c61b33799300c4b8b73baff Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:35:31 +1100 Subject: [PATCH 190/409] fix: animate background colour --- modules/drawers/Drawers.qml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index b18ecc849..c775aed5a 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -117,6 +117,10 @@ Variants { id: blobGroup color: Colours.palette.m3surface + + Behavior on color { + CAnim {} + } } BlobInvertedRect { From c670302fc1038295853b1c9db1dfabfcde5a6a30 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:36:00 +1100 Subject: [PATCH 191/409] chore: format c++ --- plugin/src/Caelestia/Blobs/blobshape.hpp | 1 + 1 file changed, 1 insertion(+) diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index 9165579cc..c9d985040 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -30,6 +30,7 @@ class BlobShape : public QQuickItem { void setRadius(qreal r); QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } signals: From 93379449ee5683115201e2ef8c376be826acd5e4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 21:44:14 +1100 Subject: [PATCH 192/409] ci: use 2 separate jobs for format --- .github/workflows/check-format.yml | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index 2f25cb1b3..b26cb9498 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -7,9 +7,8 @@ on: pull_request: jobs: - check-format: + check-qml: runs-on: ubuntu-latest - container: image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest @@ -24,6 +23,14 @@ jobs: end python3 scripts/qml-lint-conventions.py + check-cpp: + runs-on: ubuntu-latest + container: + image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest + + steps: + - uses: actions/checkout@v6 + - name: Check C++ format shell: fish {0} run: | From c64119a22b9af16dc696522b8342ce168c878256 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas?= <67807483+Mestane@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:58:16 +0300 Subject: [PATCH 193/409] fix: YouTube thumbnail fallback for missing mpris:artUrl (#1347) * fix: YouTube thumbnail fallback for missing mpris:artUrl * fix --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- modules/dashboard/Media.qml | 2 +- modules/dashboard/dash/Media.qml | 2 +- modules/lock/Media.qml | 2 +- services/Players.qml | 15 +++++++++++++++ 4 files changed, 18 insertions(+), 3 deletions(-) diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 490461851..4c838a5f5 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -181,7 +181,7 @@ Item { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type + source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 1a4f0c938..4f50531c4 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -106,7 +106,7 @@ Item { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type + source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop sourceSize.width: width diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index cabe60b9a..9d2a03677 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -18,7 +18,7 @@ Item { Image { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" // qmllint disable incompatible-type + source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop diff --git a/services/Players.qml b/services/Players.qml index d51b5df38..526b00446 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -20,6 +20,21 @@ Singleton { return alias?.to ?? player.identity; } + function getArtUrl(player: MprisPlayer): string { + if (!player) + return ""; + if (player.trackArtUrl) + return player.trackArtUrl; + + const url = player.metadata["xesam:url"] ?? ""; + if (url.startsWith("https://www.youtube.com/watch")) { + // Fallback for youtube + const id = url.match(/[?&]v=([\w-]{11})/)?.[1]; + return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : ""; + } + return ""; + } + Connections { function onPostTrackChanged() { if (!Config.utilities.toasts.nowPlaying) { From e32e1fd81cd8f85c25e460cab3994929c3b8538e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 28 Mar 2026 22:22:22 +1100 Subject: [PATCH 194/409] feat: disable reloads for installed shell --- CMakeLists.txt | 8 +++++++- shell.qml | 2 ++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b95855b5..f9d762bd7 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -63,5 +63,11 @@ if("shell" IN_LIST ENABLE_MODULES) foreach(dir assets components config modules services utils) install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") endforeach() - install(FILES shell.qml LICENSE DESTINATION "${INSTALL_QSCONFDIR}") + + file(READ shell.qml SHELL_QML) + string(REPLACE "settings.watchFiles: true" "settings.watchFiles: false" SHELL_QML "${SHELL_QML}") + file(WRITE "${CMAKE_BINARY_DIR}/qml/shell.qml" "${SHELL_QML}") + install(FILES "${CMAKE_BINARY_DIR}/qml/shell.qml" DESTINATION "${INSTALL_QSCONFDIR}") + + install(FILES LICENSE DESTINATION "${INSTALL_QSCONFDIR}") endif() diff --git a/shell.qml b/shell.qml index 3ce777699..7ef1014d7 100644 --- a/shell.qml +++ b/shell.qml @@ -10,6 +10,8 @@ import "modules/lock" import Quickshell ShellRoot { + settings.watchFiles: true + Background {} Drawers {} AreaPicker {} From 793cc8234cacc49b74fe087f3d88572c5cb03673 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 29 Mar 2026 01:24:52 +1100 Subject: [PATCH 195/409] feat: set hypr blur configs dynamically --- services/Colours.qml | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/services/Colours.qml b/services/Colours.qml index 469118167..8a26e1195 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -79,6 +79,21 @@ Singleton { Quickshell.execDetached(["caelestia", "scheme", "set", "--notify", "-m", mode]); } + function reloadHyprRules(): void { + const str = "keyword layerrule %1 %2, match:namespace caelestia-drawers"; + Hypr.extras.batchMessage([str.arg("blur").arg(transparency.enabled ? 1 : 0), str.arg("ignore_alpha").arg(transparency.base - 0.03)]); + } + + Component.onCompleted: debounceTimer.triggered() + + Connections { + function onConfigReloaded(): void { + root.reloadHyprRules(); + } + + target: Hypr + } + FileView { path: `${Paths.state}/scheme.json` watchChanges: true @@ -92,10 +107,20 @@ Singleton { source: Wallpapers.current } + Timer { + id: debounceTimer + + interval: 300 + onTriggered: root.reloadHyprRules() + } + component Transparency: QtObject { readonly property bool enabled: Appearance.transparency.enabled - readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0) + readonly property real base: Math.max(0, Math.min(1, Appearance.transparency.base - (root.light ? 0.1 : 0))) readonly property real layers: Appearance.transparency.layers + + onEnabledChanged: debounceTimer.restart() + onBaseChanged: debounceTimer.restart() } component M3TPalette: QtObject { From 980dd0b2090e9ab6b6a191090c498bf347284100 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 29 Mar 2026 02:13:12 +1100 Subject: [PATCH 196/409] fix: nix duplicate property set --- nix/default.nix | 2 -- 1 file changed, 2 deletions(-) diff --git a/nix/default.nix b/nix/default.nix index 67747b2d9..3a153dd0c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -126,8 +126,6 @@ in prePatch = '' substituteInPlace assets/pam.d/fprint \ --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so - substituteInPlace shell.qml \ - --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' ''; postInstall = '' From b5f761666db40b71b15291956fc3ba3e283bd781 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Chlo=C3=A9=20Legu=C3=A9?= Date: Sat, 28 Mar 2026 11:56:38 -0400 Subject: [PATCH 197/409] fix: weather not using system timezone (#1346) * adjusted weather.qml to ensure that it tracks system local time and date while ignoring API times if using VPN * adjusted weather.qml to ensure that it tracks system local time and date while ignoring API times if using VPN --------- Co-authored-by: Chris <102560999+ItsABigIgloo@users.noreply.github.com> Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- services/Weather.qml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/services/Weather.qml b/services/Weather.qml index b74ae55f3..40c4d895b 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -121,8 +121,8 @@ Singleton { humidity: json.current.relative_humidity_2m, windSpeed: json.current.wind_speed_10m, isDay: json.current.is_day, - sunrise: json.daily.sunrise[0], - sunset: json.daily.sunset[0] + sunrise: json.daily.sunrise[0].replace("T", " "), + sunset: json.daily.sunset[0].replace("T", " ") }; const forecastList = []; @@ -218,7 +218,6 @@ Singleton { target: Config.services } - // Refresh current location hourly Timer { interval: 3600000 // 1 hour running: true From c588794b20242b23ecdfb6281c68cc265db6802b Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Sun, 29 Mar 2026 06:22:00 +0200 Subject: [PATCH 198/409] feat: fullscreen notification & toasts overlay (#1276) * reworked fullscreen mode as transforming shell * controlcenter configuring of toasts & notifs * fix animation on fs switch * border rounding & shadow animation * change controlcenter notifications layout * stay on WlrLayer.Overlay, use Anim * ci: update action versions * qmlformat * format take two * third time's the charm * fix: special workspace fullscreen * fix: bar width border.thickness on non-persistent behaviour * merge fix * stop layout shift on close * last few conventions sorted * linting * maximized state to fullscreen --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- components/controls/Menu.qml | 1 + config/Config.qml | 5 +- config/NotifsConfig.qml | 1 + config/UtilitiesConfig.qml | 1 + modules/Shortcuts.qml | 2 +- modules/bar/Bar.qml | 6 + modules/bar/BarWrapper.qml | 9 +- .../components/workspaces/ActiveIndicator.qml | 1 + .../bar/components/workspaces/Workspaces.qml | 3 + modules/controlcenter/PaneRegistry.qml | 12 +- modules/controlcenter/Panes.qml | 1 + .../notifications/NotificationsPane.qml | 427 ++++++++++++++++++ modules/drawers/Backgrounds.qml | 8 +- modules/drawers/Border.qml | 7 +- modules/drawers/Drawers.qml | 57 ++- modules/drawers/Exclusions.qml | 4 +- modules/drawers/Interactions.qml | 9 +- modules/drawers/Panels.qml | 13 +- modules/notifications/Background.qml | 9 +- modules/osd/Background.qml | 3 +- modules/sidebar/Background.qml | 4 +- modules/utilities/Background.qml | 3 +- modules/utilities/toasts/Toasts.qml | 13 + services/Notifs.qml | 18 +- 24 files changed, 582 insertions(+), 35 deletions(-) create mode 100644 modules/controlcenter/notifications/NotificationsPane.qml diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index b60a36399..320f318cc 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -55,6 +55,7 @@ Elevation { function onClicked(): void { root.itemSelected(item.modelData); root.active = item.modelData; + item.modelData.clicked(); root.expanded = false; } diff --git a/config/Config.qml b/config/Config.qml index 91454b598..bcdcff19c 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -267,11 +267,13 @@ Singleton { function serializeNotifs(): var { return { expire: notifs.expire, + fullscreen: notifs.fullscreen, defaultExpireTimeout: notifs.defaultExpireTimeout, clearThreshold: notifs.clearThreshold, expandThreshold: notifs.expandThreshold, actionOnClick: notifs.actionOnClick, - groupPreviewNum: notifs.groupPreviewNum + groupPreviewNum: notifs.groupPreviewNum, + openExpanded: notifs.openExpanded }; } @@ -323,6 +325,7 @@ Singleton { maxToasts: utilities.maxToasts, toasts: { configLoaded: utilities.toasts.configLoaded, + fullscreen: utilities.toasts.fullscreen, chargingChanged: utilities.toasts.chargingChanged, gameModeChanged: utilities.toasts.gameModeChanged, dndChanged: utilities.toasts.dndChanged, diff --git a/config/NotifsConfig.qml b/config/NotifsConfig.qml index fa2db494e..bd54b94b7 100644 --- a/config/NotifsConfig.qml +++ b/config/NotifsConfig.qml @@ -2,6 +2,7 @@ import Quickshell.Io JsonObject { property bool expire: true + property string fullscreen: "on" property int defaultExpireTimeout: 5000 property real clearThreshold: 0.3 property int expandThreshold: 20 diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index 017ab4ad8..c97e9f603 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -46,6 +46,7 @@ JsonObject { component Toasts: JsonObject { property bool configLoaded: true + property string fullscreen: "off" property bool chargingChanged: true property bool gameModeChanged: true property bool dndChanged: true diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index d73e63511..b4633e8f5 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -9,7 +9,7 @@ Scope { id: root property bool launcherInterrupted - readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false + readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false // qmllint disable unresolved-type CustomShortcut { diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 38ed564b5..3b99a44bc 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -16,6 +16,7 @@ ColumnLayout { required property ShellScreen screen required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts + required property bool fullscreen readonly property int vPadding: Appearance.padding.large function closeTray(): void { @@ -128,6 +129,7 @@ ColumnLayout { delegate: WrappedLoader { sourceComponent: Workspaces { screen: root.screen + fullscreen: root.fullscreen } } } @@ -135,6 +137,7 @@ ColumnLayout { roleValue: "activeWindow" delegate: WrappedLoader { Layout.fillWidth: true + visible: !root.fullscreen sourceComponent: ActiveWindow { bar: root monitor: Brightness.getMonitorForScreen(root.screen) @@ -144,18 +147,21 @@ ColumnLayout { DelegateChoice { roleValue: "tray" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: Tray {} } } DelegateChoice { roleValue: "clock" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: Clock {} } } DelegateChoice { roleValue: "statusIcons" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: StatusIcons {} } } diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index df29f0af1..f29e23f93 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -13,12 +13,13 @@ Item { required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts required property bool disabled + required property bool fullscreen readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness - readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered) + readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) property bool isHovered function closeTray(): void { @@ -33,8 +34,9 @@ Item { (content.item as Bar)?.handleWheel(y, angleDelta); } - visible: width > Config.border.thickness - implicitWidth: Config.border.thickness + clip: true + visible: width > 0 + implicitWidth: fullscreen ? 0 : Config.border.thickness states: State { name: "visible" @@ -83,6 +85,7 @@ Item { screen: root.screen visibilities: root.visibilities popouts: root.popouts // qmllint disable incompatible-type + fullscreen: root.fullscreen } } } diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index e8a52d4d4..d10086fd7 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -10,6 +10,7 @@ StyledRect { required property int activeWsId required property Repeater workspaces required property Item mask + required property bool fullscreen readonly property int currentWsIdx: { let i = activeWsId - 1; diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index f205dfac0..bbbfdfbcc 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -12,6 +12,7 @@ StyledClippingRect { id: root required property ShellScreen screen + required property bool fullscreen readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId @@ -36,6 +37,7 @@ StyledClippingRect { anchors.fill: parent scale: root.onSpecial ? 0.8 : 1 opacity: root.onSpecial ? 0.5 : 1 + visible: !root.fullscreen layer.enabled: root.blur > 0 layer.effect: MultiEffect { @@ -86,6 +88,7 @@ StyledClippingRect { activeWsId: root.activeWsId workspaces: workspaces mask: layout + fullscreen: root.fullscreen } } diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml index ca48551fc..4d85969bb 100644 --- a/modules/controlcenter/PaneRegistry.qml +++ b/modules/controlcenter/PaneRegistry.qml @@ -36,6 +36,12 @@ QtObject { readonly property string icon: "task_alt" readonly property string component: "taskbar/TaskbarPane.qml" }, + QtObject { + readonly property string id: "notifications" + readonly property string label: "notifications" + readonly property string icon: "notifications" + readonly property string component: "notifications/NotificationsPane.qml" + }, QtObject { readonly property string id: "launcher" readonly property string label: "launcher" @@ -60,7 +66,7 @@ QtObject { return result; } - function getByIndex(index: int): QtObject { + function getByIndex(index: int): var { if (index >= 0 && index < panes.length) { return panes[index]; } @@ -76,12 +82,12 @@ QtObject { return -1; } - function getByLabel(label: string): QtObject { + function getByLabel(label: string): var { const index = getIndexByLabel(label); return getByIndex(index); } - function getById(id: string): QtObject { + function getById(id: string): var { for (let i = 0; i < panes.length; i++) { if (panes[i].id === id) { return panes[i]; diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5660ea7d3..6a5364100 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -5,6 +5,7 @@ import "network" import "audio" import "appearance" import "taskbar" +import "notifications" import "launcher" import "dashboard" import QtQuick diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml new file mode 100644 index 000000000..41920f69f --- /dev/null +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -0,0 +1,427 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config + +Item { + id: root + + required property Session session + + property bool notificationsExpire: Config.notifs.expire ?? true + property string notificationsFullscreen: Config.notifs.fullscreen ?? "on" + property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false + property int notificationsDefaultExpireTimeout: Config.notifs.defaultExpireTimeout ?? 5000 + property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3 + + property int maxToasts: Config.utilities.maxToasts ?? 4 + property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off" + property bool chargingChanged: Config.utilities.toasts.chargingChanged ?? true + property bool gameModeChanged: Config.utilities.toasts.gameModeChanged ?? true + property bool dndChanged: Config.utilities.toasts.dndChanged ?? true + property bool audioOutputChanged: Config.utilities.toasts.audioOutputChanged ?? true + property bool audioInputChanged: Config.utilities.toasts.audioInputChanged ?? true + property bool capsLockChanged: Config.utilities.toasts.capsLockChanged ?? true + property bool numLockChanged: Config.utilities.toasts.numLockChanged ?? true + property bool kbLayoutChanged: Config.utilities.toasts.kbLayoutChanged ?? true + property bool vpnChanged: Config.utilities.toasts.vpnChanged ?? true + property bool nowPlaying: Config.utilities.toasts.nowPlaying ?? false + + function saveConfig(): void { + Config.notifs.expire = root.notificationsExpire; + Config.notifs.fullscreen = root.notificationsFullscreen; + Config.notifs.openExpanded = root.notificationsOpenExpanded; + Config.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; + Config.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; + + Config.utilities.maxToasts = root.maxToasts; + Config.utilities.toasts.fullscreen = root.toastsFullscreen; + Config.utilities.toasts.chargingChanged = root.chargingChanged; + Config.utilities.toasts.gameModeChanged = root.gameModeChanged; + Config.utilities.toasts.dndChanged = root.dndChanged; + Config.utilities.toasts.audioOutputChanged = root.audioOutputChanged; + Config.utilities.toasts.audioInputChanged = root.audioInputChanged; + Config.utilities.toasts.capsLockChanged = root.capsLockChanged; + Config.utilities.toasts.numLockChanged = root.numLockChanged; + Config.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; + Config.utilities.toasts.vpnChanged = root.vpnChanged; + Config.utilities.toasts.nowPlaying = root.nowPlaying; + + Config.save(); + } + + anchors.fill: parent + + ClippingRectangle { + id: notificationsClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal + + color: "transparent" + radius: notificationsBorder.innerRadius + + Loader { + id: notificationsLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + sourceComponent: notificationsContentComponent + } + } + + InnerBorder { + id: notificationsBorder + + leftThickness: 0 + rightThickness: Appearance.padding.normal + } + + Component { + id: notificationsContentComponent + + StyledFlickable { + id: notificationsFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: notificationsLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: notificationsFlickable + } + + RowLayout { + id: notificationsLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + Layout.maximumWidth: 500 + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Notifications") + font.pointSize: Appearance.font.size.normal + } + + SplitButtonRow { + id: notificationsFullscreenSelector + + function syncActiveItem(): void { + active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem; + } + + label: qsTr("Show in fullscreen") + menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem] + + Component.onCompleted: syncActiveItem() + + Connections { + function onNotificationsFullscreenChanged(): void { + notificationsFullscreenSelector.syncActiveItem(); + } + + target: root + } + + MenuItem { + id: notificationsFullscreenOffItem + + text: qsTr("Off") + icon: "notifications_off" + activeText: qsTr("Off") + onClicked: { + root.notificationsFullscreen = "off"; + root.saveConfig(); + } + } + + MenuItem { + id: notificationsFullscreenOnItem + + text: qsTr("On") + icon: "notifications" + activeText: qsTr("On") + onClicked: { + root.notificationsFullscreen = "on"; + root.saveConfig(); + } + } + } + + SwitchRow { + label: qsTr("Expire automatically") + checked: root.notificationsExpire + onToggled: checked => { + root.notificationsExpire = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Open expanded") + checked: root.notificationsOpenExpanded + onToggled: checked => { + root.notificationsOpenExpanded = checked; + root.saveConfig(); + } + } + + SpinBoxRow { + label: qsTr("Default timeout") + value: root.notificationsDefaultExpireTimeout + min: 1000 + max: 60000 + step: 500 + onValueModified: value => { + root.notificationsDefaultExpireTimeout = value; + root.saveConfig(); + } + } + + SpinBoxRow { + label: qsTr("Group preview count") + value: root.notificationsGroupPreviewNum + min: 1 + max: 10 + step: 1 + onValueModified: value => { + root.notificationsGroupPreviewNum = value; + root.saveConfig(); + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Toast settings") + font.pointSize: Appearance.font.size.normal + } + + SplitButtonRow { + id: toastFullscreenSelector + + function syncActiveItem(): void { + if (root.toastsFullscreen === "all") { + active = toastFullscreenAllItem; + return; + } + + if (root.toastsFullscreen === "important") { + active = toastFullscreenImportantItem; + return; + } + + active = toastFullscreenOffItem; + } + + Layout.fillWidth: true + z: expanded ? 100 : 0 + label: qsTr("Show in fullscreen") + menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem] + + Component.onCompleted: syncActiveItem() + + Connections { + function onToastsFullscreenChanged(): void { + toastFullscreenSelector.syncActiveItem(); + } + + target: root + } + + MenuItem { + id: toastFullscreenOffItem + + text: qsTr("Off") + icon: "notifications_off" + activeText: qsTr("Off") + onClicked: { + root.toastsFullscreen = "off"; + root.saveConfig(); + } + } + + MenuItem { + id: toastFullscreenImportantItem + + text: qsTr("Important") + icon: "priority_high" + activeText: qsTr("Important") + onClicked: { + root.toastsFullscreen = "important"; + root.saveConfig(); + } + } + + MenuItem { + id: toastFullscreenAllItem + + text: qsTr("On") + icon: "notifications" + activeText: qsTr("On") + onClicked: { + root.toastsFullscreen = "all"; + root.saveConfig(); + } + } + } + + SpinBoxRow { + Layout.fillWidth: true + label: qsTr("Visible toasts") + value: root.maxToasts + min: 1 + max: 10 + step: 1 + onValueModified: value => { + root.maxToasts = value; + root.saveConfig(); + } + } + + GridLayout { + Layout.fillWidth: true + columns: 2 + columnSpacing: Appearance.spacing.normal + rowSpacing: Appearance.spacing.normal + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Charging changes") + checked: root.chargingChanged + onToggled: checked => { + root.chargingChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Game mode changes") + checked: root.gameModeChanged + onToggled: checked => { + root.gameModeChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Do not disturb") + checked: root.dndChanged + onToggled: checked => { + root.dndChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Audio output changes") + checked: root.audioOutputChanged + onToggled: checked => { + root.audioOutputChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Audio input changes") + checked: root.audioInputChanged + onToggled: checked => { + root.audioInputChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Caps lock changes") + checked: root.capsLockChanged + onToggled: checked => { + root.capsLockChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Num lock changes") + checked: root.numLockChanged + onToggled: checked => { + root.numLockChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Keyboard layout changes") + checked: root.kbLayoutChanged + onToggled: checked => { + root.kbLayoutChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("VPN changes") + checked: root.vpnChanged + onToggled: checked => { + root.vpnChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Now playing") + checked: root.nowPlaying + onToggled: checked => { + root.nowPlaying = checked; + root.saveConfig(); + } + } + } + } + } + } + } + } +} diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index b79cd9195..7592411c3 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -15,14 +15,17 @@ Shape { required property Panels panels required property Item bar + required property real borderThickness + required property real borderRounding anchors.fill: parent - anchors.margins: Config.border.thickness + anchors.margins: root.borderThickness anchors.leftMargin: bar.implicitWidth preferredRendererType: Shape.CurveRenderer Osd.Background { wrapper: root.panels.osd // qmllint disable incompatible-type + rounding: Config.border.rounding startX: root.width - root.panels.session.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding @@ -31,6 +34,7 @@ Shape { Notifications.Background { wrapper: root.panels.notifications // qmllint disable incompatible-type sidebar: sidebar + rounding: Config.border.rounding startX: root.width startY: 0 @@ -68,6 +72,7 @@ Shape { Utilities.Background { wrapper: root.panels.utilities // qmllint disable incompatible-type sidebar: sidebar + rounding: root.borderRounding startX: root.width startY: root.height @@ -78,6 +83,7 @@ Shape { wrapper: root.panels.sidebar // qmllint disable incompatible-type panels: root.panels + rounding: root.borderRounding startX: root.width startY: root.panels.notifications.height diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml index 13f92a4b2..a638479df 100644 --- a/modules/drawers/Border.qml +++ b/modules/drawers/Border.qml @@ -4,12 +4,13 @@ import QtQuick import QtQuick.Effects import qs.components import qs.services -import qs.config Item { id: root required property Item bar + required property real borderThickness + required property real borderRounding anchors.fill: parent @@ -36,9 +37,9 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: Config.border.thickness + anchors.margins: root.borderThickness anchors.leftMargin: root.bar.implicitWidth - radius: Config.border.rounding + radius: root.borderRounding } } } diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 864ad5c46..9d3de7764 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -25,18 +25,34 @@ Variants { Exclusions { screen: scope.modelData bar: bar + borderThickness: Config.border.thickness } StyledWindow { id: win - readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false + readonly property var monitor: Hypr.monitorFor(screen) + readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject?.specialWorkspace?.name.length ?? 0) > 0 + readonly property bool hasFullscreen: { + if (hasSpecialWorkspace) { + const specialName = monitor?.lastIpcObject?.specialWorkspace?.name; + if (!specialName) + return false; + const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); + return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + property real borderThickness: hasFullscreen ? 0 : Config.border.thickness + readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness + property real borderRounding: hasFullscreen ? 0 : Config.border.rounding + property real shadowOpacity: hasFullscreen ? 0 : 0.7 readonly property int dragMaskPadding: { if (focusGrab.active || panels.popouts.isDetached) return 0; const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) + if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace.lastIpcObject.windows > 0) return 0; const thresholds = []; @@ -55,6 +71,7 @@ Variants { screen: scope.modelData name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: Region { @@ -72,6 +89,30 @@ Variants { anchors.left: true anchors.right: true + Behavior on borderThickness { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on borderRounding { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on shadowOpacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + Variants { id: regions @@ -81,7 +122,7 @@ Variants { required property Item modelData x: modelData.x + bar.implicitWidth - y: modelData.y + Config.border.thickness + y: modelData.y + win.borderLayoutThickness width: modelData.width height: modelData.height intersection: Intersection.Subtract @@ -120,16 +161,20 @@ Variants { layer.effect: MultiEffect { shadowEnabled: true blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) + shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity)) } Border { bar: bar + borderThickness: win.borderThickness + borderRounding: win.borderRounding } Backgrounds { panels: panels bar: bar + borderThickness: win.borderThickness + borderRounding: win.borderRounding } } @@ -145,6 +190,8 @@ Variants { visibilities: visibilities panels: panels bar: bar + borderThickness: win.borderLayoutThickness + fullscreen: win.hasFullscreen Panels { id: panels @@ -152,6 +199,7 @@ Variants { screen: scope.modelData visibilities: visibilities bar: bar + borderThickness: win.borderLayoutThickness } BarWrapper { @@ -165,6 +213,7 @@ Variants { popouts: panels.popouts disabled: scope.barDisabled + fullscreen: win.hasFullscreen Component.onCompleted: Visibilities.bars.set(scope.modelData, this) } diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index f43afb9a2..87610ee6b 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.components.containers -import qs.config import qs.modules.bar as Bar Scope { @@ -11,6 +10,7 @@ Scope { required property ShellScreen screen required property Bar.BarWrapper bar + required property real borderThickness ExclusionZone { anchors.left: true @@ -32,7 +32,7 @@ Scope { component ExclusionZone: StyledWindow { screen: root.screen name: "border-exclusion" - exclusiveZone: Config.border.thickness + exclusiveZone: root.borderThickness mask: Region {} implicitWidth: 1 implicitHeight: 1 diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fcb128a93..4b670fca2 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -15,6 +15,8 @@ CustomMouseArea { required property DrawerVisibilities visibilities required property Panels panels required property Bar.BarWrapper bar + required property real borderThickness + required property bool fullscreen property point dragStart property bool dashboardShortcutActive @@ -22,7 +24,7 @@ CustomMouseArea { property bool utilitiesShortcutActive function withinPanelHeight(panel: Item, x: real, y: real): bool { - const panelY = Config.border.thickness + panel.y; + const panelY = root.borderThickness + panel.y; return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; } @@ -48,13 +50,16 @@ CustomMouseArea { } function onWheel(event: WheelEvent): void { + if (fullscreen) + return; if (event.x < bar.implicitWidth) { bar.handleWheel(event.y, event.angleDelta); } } anchors.fill: parent - hoverEnabled: true + acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons + hoverEnabled: !fullscreen onPressed: event => dragStart = Qt.point(event.x, event.y) onContainsMouseChanged: { diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index f2531cfa3..04b83c8d0 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -19,6 +19,7 @@ Item { required property ShellScreen screen required property DrawerVisibilities visibilities required property Bar.BarWrapper bar + required property real borderThickness readonly property alias osd: osd readonly property alias notifications: notifications @@ -31,9 +32,17 @@ Item { readonly property alias sidebar: sidebar anchors.fill: parent - anchors.margins: Config.border.thickness + anchors.margins: root.borderThickness anchors.leftMargin: bar.implicitWidth + Behavior on anchors.margins { + Anim {} + } + + Behavior on anchors.leftMargin { + Anim {} + } + Osd.Wrapper { id: osd @@ -100,7 +109,7 @@ Item { if (isDetached) return (root.height - nonAnimHeight) / 2; - const off = currentCenter - Config.border.thickness - nonAnimHeight / 2; + const off = currentCenter - root.borderThickness - nonAnimHeight / 2; const diff = root.height - Math.floor(off + nonAnimHeight); if (diff < 0) return off + diff; diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml index 4d7a5ff76..740cda105 100644 --- a/modules/notifications/Background.qml +++ b/modules/notifications/Background.qml @@ -2,14 +2,13 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper required property var sidebar - readonly property real rounding: Config.border.rounding + required property real rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding @@ -31,14 +30,14 @@ ShapePath { relativeY: root.wrapper.height - root.roundingY * 2 } PathArc { - relativeX: root.sidebar.notifsRoundingX + relativeX: root.rounding relativeY: root.roundingY - radiusX: root.sidebar.notifsRoundingX + radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width + relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width relativeY: 0 } PathArc { diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml index a609f4601..330c703e7 100644 --- a/modules/osd/Background.qml +++ b/modules/osd/Background.qml @@ -2,13 +2,12 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper - readonly property real rounding: Config.border.rounding + required property real rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml index 4cc142628..c7d42123e 100644 --- a/modules/sidebar/Background.qml +++ b/modules/sidebar/Background.qml @@ -2,15 +2,13 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper required property var panels - - readonly property real rounding: Config.border.rounding + required property real rounding readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml index 975461a58..5b58b41e0 100644 --- a/modules/utilities/Background.qml +++ b/modules/utilities/Background.qml @@ -2,14 +2,13 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper required property var sidebar - readonly property real rounding: Config.border.rounding + required property real rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index ac8772f57..1ef072e9e 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Caelestia import qs.components +import qs.services import qs.config Item { @@ -12,6 +13,16 @@ Item { readonly property int spacing: Appearance.spacing.small property bool flag + function shouldShowToast(toast: Toast): bool { + if (!Notifs.hasFullscreen()) + return true; + if (Config.utilities.toasts.fullscreen === "all") + return true; + if (Config.utilities.toasts.fullscreen === "important") + return toast.type === Toast.Warning || toast.type === Toast.Error; + return false; + } + implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 implicitHeight: { let h = -spacing; @@ -31,6 +42,8 @@ Item { const toasts = []; let count = 0; for (const toast of Toaster.toasts) { + if (!root.shouldShowToast(toast)) + continue; toasts.push(toast); if (!toast.closed) { count++; diff --git a/services/Notifs.qml b/services/Notifs.qml index 4e6c0dd6b..92e609be9 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -21,6 +21,22 @@ Singleton { property bool loaded + function hasFullscreen(): bool { + for (const monitor of Hypr.monitors.values) { + if (monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1)) + return true; + } + return false; + } + + function shouldShowPopup(): bool { + if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) + return false; + if (Config.notifs.fullscreen === "off" && hasFullscreen()) + return false; + return true; + } + onDndChanged: { if (!Config.utilities.toasts.dndChanged) return; @@ -79,7 +95,7 @@ Singleton { notif.tracked = true; const comp = notifComp.createObject(root, { - popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), + popup: root.shouldShowPopup(), notification: notif }); root.list = [comp, ...root.list]; From 5c59e4490a43eb38b39133d80c47490f0963f216 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Sun, 29 Mar 2026 06:25:52 +0200 Subject: [PATCH 199/409] feat: VPN fixes & improvements (#1116) * feat: add VPN settings and management UI - Add VPN configuration UI - Update VPN toggle visibility to check enabled providers * controlcenter: VPN modal transitions & cleanup * controlcenter: VPN modal styling * controlcenter: VPN modal scrim * controlcenter: VPN modal padding * controlcenter: VPN modal enter & exit behaviour * vpn: reworked managment & fixes - Switched to primarily use provider status json - Emitting more detailed toasts and errors/states - Authentication prompt button to open in browser - Better resets and provider change * vpn: replace use of qt5compat * fixing providers, status and adding custom option * local qml-lint script lied to me * format * section order * linting --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- config/Config.qml | 20 +- config/UtilitiesConfig.qml | 2 +- modules/controlcenter/network/VpnDetails.qml | 149 ++++++++- modules/controlcenter/network/VpnList.qml | 151 ++++++++- modules/utilities/cards/Toggles.qml | 4 +- services/VPN.qml | 334 ++++++++++++++++++- 6 files changed, 623 insertions(+), 37 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index bcdcff19c..df684b425 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -320,6 +320,24 @@ Singleton { } function serializeUtilities(): var { + const vpnProviders = []; + for (let i = 0; i < utilities.vpn.provider.length; i++) { + const p = utilities.vpn.provider[i]; + const provider = { + displayName: p.displayName, + enabled: p.enabled, + iface: p.iface, + name: p.name + }; + if (p.connectCmd && p.connectCmd.length > 0) { + provider.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + provider.disconnectCmd = p.disconnectCmd; + } + vpnProviders.push(provider); + } + return { enabled: utilities.enabled, maxToasts: utilities.maxToasts, @@ -339,7 +357,7 @@ Singleton { }, vpn: { enabled: utilities.vpn.enabled, - provider: utilities.vpn.provider + provider: vpnProviders }, quickToggles: utilities.quickToggles }; diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index c97e9f603..61cf16786 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -62,6 +62,6 @@ JsonObject { component Vpn: JsonObject { property bool enabled: false - property list provider: ["netbird"] + property list provider: [] } } diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 8d91067c7..4d749d8b9 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -109,10 +109,13 @@ DeviceDetails { inactiveOnColour: Colours.palette.m3onSecondaryContainer onClicked: { + const provider = Config.utilities.vpn.provider[root.vpnProvider.index]; editVpnDialog.editIndex = root.vpnProvider.index; editVpnDialog.providerName = root.vpnProvider.name; editVpnDialog.displayName = root.vpnProvider.displayName; editVpnDialog.interfaceName = root.vpnProvider.interface; + editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : ""; + editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : ""; editVpnDialog.open(); } } @@ -136,6 +139,30 @@ DeviceDetails { } } } + + TextButton { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" + text: qsTr("Open Login Page") + inactiveColour: Colours.palette.m3tertiaryContainer + inactiveOnColour: Colours.palette.m3onTertiaryContainer + + onClicked: { + Qt.openUrlExternally(VPN.status.authUrl); + } + } + + StyledText { + Layout.fillWidth: true + Layout.topMargin: Appearance.spacing.normal + visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" + text: qsTr("Click 'Connect' to generate authentication URL") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } } } }, @@ -176,12 +203,31 @@ DeviceDetails { return qsTr("Disabled"); if (VPN.connecting) return qsTr("Connecting..."); - if (VPN.connected) + + switch (VPN.status.state) { + case "connected": return qsTr("Connected"); - return qsTr("Enabled (Not connected)"); + case "disconnected": + return qsTr("Disconnected"); + case "connecting": + return qsTr("Connecting..."); + case "needs-auth": + return qsTr("Authentication required"); + case "error": + return qsTr("Error"); + default: + return qsTr("Unknown"); + } } } + PropertyRow { + visible: VPN.status.reason !== "" + showTopMargin: true + label: qsTr("Details") + value: VPN.status.reason + } + PropertyRow { showTopMargin: true label: qsTr("Enabled") @@ -200,6 +246,8 @@ DeviceDetails { property string providerName: "" property string displayName: "" property string interfaceName: "" + property string connectCmd: "" + property string disconnectCmd: "" function closeWithAnimation(): void { close(); @@ -349,6 +397,82 @@ DeviceDetails { } } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller / 2 + visible: editVpnDialog.connectCmd.length > 0 + + StyledText { + text: qsTr("Connect Command") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: connectCmdFieldEdit + + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: editVpnDialog.connectCmd + onTextChanged: editVpnDialog.connectCmd = text + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller / 2 + visible: editVpnDialog.disconnectCmd.length > 0 + + StyledText { + text: qsTr("Disconnect Command") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: disconnectCmdFieldEdit + + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: editVpnDialog.disconnectCmd + onTextChanged: editVpnDialog.disconnectCmd = text + } + } + } + RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true @@ -376,12 +500,23 @@ DeviceDetails { for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { if (i === editVpnDialog.editIndex) { - providers.push({ - name: editVpnDialog.providerName, + const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0; + const newProvider = { displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, - interface: editVpnDialog.interfaceName, - enabled: wasEnabled - }); + enabled: wasEnabled, + iface: editVpnDialog.interfaceName, + name: editVpnDialog.providerName, + connectCmd: hasCommands ? editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0) : undefined, + disconnectCmd: hasCommands ? editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0) : undefined + }; + + // Remove undefined properties + if (!hasCommands) { + delete newProvider.connectCmd; + delete newProvider.disconnectCmd; + } + + providers.push(newProvider); } else { providers.push(Config.utilities.vpn.provider[i]); } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 3646841ce..6dabc2cfc 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -159,15 +159,38 @@ ColumnLayout { StyledText { Layout.fillWidth: true text: { - if (modelData.enabled && VPN.connected) + if (!modelData.enabled) + return qsTr("Disabled"); + + if (VPN.connecting) + return qsTr("Connecting..."); + + switch (VPN.status.state) { + case "connected": return qsTr("Connected"); - if (modelData.enabled && VPN.connecting) + case "disconnected": + return qsTr("Enabled"); + case "connecting": return qsTr("Connecting..."); - if (modelData.enabled) + case "needs-auth": + return qsTr("Auth required"); + case "error": + return qsTr("Error"); + default: return qsTr("Enabled"); - return qsTr("Disabled"); + } + } + color: { + if (!modelData.enabled) + return Colours.palette.m3outline; + if (VPN.status.state === "connected") + return Colours.palette.m3primary; + if (VPN.status.state === "error") + return Colours.palette.m3error; + if (VPN.status.state === "needs-auth") + return Colours.palette.m3tertiary; + return Colours.palette.m3onSurface; } - color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline font.pointSize: Appearance.font.size.small font.weight: modelData.enabled && VPN.connected ? 500 : 400 elide: Text.ElideRight @@ -271,6 +294,8 @@ ColumnLayout { property string providerName: "" property string displayName: "" property string interfaceName: "" + property string connectCmd: "" + property string disconnectCmd: "" function showProviderSelection(): void { currentState = "selection"; @@ -286,6 +311,8 @@ ColumnLayout { providerName = providerType; displayName = defaultDisplayName; interfaceName = ""; + connectCmd = ""; + disconnectCmd = ""; if (currentState === "selection") { transitionToForm.start(); @@ -304,6 +331,8 @@ ColumnLayout { providerName = isObject ? (provider.name || "custom") : String(provider); displayName = isObject ? (provider.displayName || providerName) : providerName; interfaceName = isObject ? (provider.interface || "") : ""; + connectCmd = isObject && provider.connectCmd ? provider.connectCmd.join(" ") : ""; + disconnectCmd = isObject && provider.disconnectCmd ? provider.disconnectCmd.join(" ") : ""; currentState = "form"; open(); @@ -491,7 +520,7 @@ ColumnLayout { TextButton { Layout.fillWidth: true - text: qsTr("WireGuard (Custom)") + text: qsTr("WireGuard") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { @@ -499,6 +528,16 @@ ColumnLayout { } } + TextButton { + Layout.fillWidth: true + text: qsTr("Custom") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + vpnDialog.showAddForm("custom", "Custom VPN"); + } + } + TextButton { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true @@ -604,6 +643,82 @@ ColumnLayout { } } + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller / 2 + visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") + + StyledText { + text: qsTr("Connect Command (e.g., wg-quick up wg0)") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: connectCmdField + + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.connectCmd + onTextChanged: vpnDialog.connectCmd = text + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller / 2 + visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") + + StyledText { + text: qsTr("Disconnect Command (e.g., wg-quick down wg0)") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Appearance.rounding.small + border.width: 1 + border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: disconnectCmdField + + anchors.centerIn: parent + width: parent.width - Appearance.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.disconnectCmd + onTextChanged: vpnDialog.disconnectCmd = text + } + } + } + RowLayout { Layout.topMargin: Appearance.spacing.normal Layout.fillWidth: true @@ -620,19 +735,37 @@ ColumnLayout { TextButton { Layout.fillWidth: true text: qsTr("Save") - enabled: vpnDialog.interfaceName.length > 0 + enabled: { + const hasCommands = vpnDialog.connectCmd.length > 0 || vpnDialog.disconnectCmd.length > 0; + if (hasCommands) { + return vpnDialog.interfaceName.length > 0 && vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; + } + return vpnDialog.interfaceName.length > 0; + } inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer onClicked: { const providers = []; + const hasCommands = vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; const newProvider = { - name: vpnDialog.providerName, displayName: vpnDialog.displayName || vpnDialog.interfaceName, - interface: vpnDialog.interfaceName + enabled: false, + interface: vpnDialog.interfaceName, + name: vpnDialog.providerName }; + if (hasCommands) { + newProvider.connectCmd = vpnDialog.connectCmd.split(" ").filter(s => s.length > 0); + newProvider.disconnectCmd = vpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); + } + if (vpnDialog.editIndex >= 0) { + const oldProvider = Config.utilities.vpn.provider[vpnDialog.editIndex]; + if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) { + newProvider.enabled = oldProvider.enabled; + } + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { if (i === vpnDialog.editIndex) { providers.push(newProvider); diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 996b027c3..2b14a9ae0 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -141,8 +141,10 @@ StyledRect { roleValue: "vpn" delegate: Toggle { icon: "vpn_key" - checked: VPN.connected + checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error" enabled: !VPN.connecting + toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" + inactiveOnColour: Colours.palette.m3onSurfaceVariant onClicked: VPN.toggle() } } diff --git a/services/VPN.qml b/services/VPN.qml index 2b25813b2..bfeacea46 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -10,6 +10,12 @@ Singleton { id: root property bool connected: false + property var status: ({ + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }) readonly property bool connecting: connectProc.running || disconnectProc.running readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) @@ -19,7 +25,7 @@ Singleton { } readonly property bool isCustomProvider: typeof providerInput === "object" readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput) - readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : "" + readonly property string interfaceName: isCustomProvider ? (providerInput.iface || "") : "" readonly property var currentConfig: { const name = providerName; const iface = interfaceName; @@ -30,7 +36,7 @@ Singleton { return { connectCmd: custom.connectCmd || defaults.connectCmd, disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd, - interface: custom.interface || defaults.interface, + interface: custom.iface || defaults.interface, displayName: custom.displayName || defaults.displayName }; } @@ -53,7 +59,7 @@ Singleton { displayName: "Warp" }, "netbird": { - connectCmd: ["netbird", "up"], + connectCmd: ["netbird", "up", "--no-browser"], disconnectCmd: ["netbird", "down"], interface: "wt0", displayName: "NetBird" @@ -75,6 +81,10 @@ Singleton { } function connect(): void { + if (status.state === "needs-auth" && status.authUrl) { + emitStatusToast(status); + return; + } if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { connectProc.exec(root.currentConfig.connectCmd); } @@ -87,11 +97,7 @@ Singleton { } function toggle(): void { - if (connected) { - disconnect(); - } else { - connect(); - } + connected ? disconnect() : connect(); } function checkStatus(): void { @@ -100,18 +106,223 @@ Singleton { } } - onConnectedChanged: { + function getStatusCommand(): var { + switch (providerName) { + case "tailscale": + return ["tailscale", "status", "--json"]; + case "netbird": + return ["netbird", "status", "--json"]; + case "warp": + return ["warp-cli", "status"]; + case "wireguard": + return ["ip", "link", "show"]; + default: + return ["ip", "link", "show"]; + } + } + + function parseTailscaleStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + + // Handle empty or whitespace-only output + if (!output || output.trim().length === 0) { + return status; + } + + // Check for common non-JSON states first + if (output.includes("Logged out") || output.includes("Stopped") || output.includes("not running") || output.includes("Tailscale is not running")) { + status.state = "disconnected"; + return status; + } + + // Try to parse as JSON + try { + const data = JSON.parse(output); + const backendState = data.BackendState || ""; + + if (backendState === "Running") { + status.connected = true; + status.state = "connected"; + } else if (backendState === "Starting") { + status.state = "connecting"; + } else if (backendState === "NeedsLogin" || backendState === "NeedsMachineAuth") { + status.state = "needs-auth"; + status.reason = backendState === "NeedsLogin" ? "Login required" : "Machine authorization required"; + status.authUrl = data.AuthURL || ""; + } + } catch (e) { + // JSON parsing failed - treat as disconnected unless it looks like an error + if (output.includes("error") || output.includes("Error") || output.includes("failed")) { + status.state = "disconnected"; + status.reason = "Tailscale may not be running"; + } else { + status.state = "disconnected"; + } + } + return status; + } + + function parseNetBirdStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + try { + const data = JSON.parse(output); + const mgmtConnected = data.management?.connected; + const signalConnected = data.signal?.connected; + + if (mgmtConnected && signalConnected) { + status.connected = true; + status.state = "connected"; + } else if (data.management?.error) { + const error = data.management.error; + if (error.includes("auth") || error.includes("login")) { + status.state = "needs-auth"; + status.reason = "Authentication required"; + } else { + status.reason = error; + } + } + } catch (e) { + status.state = "error"; + status.reason = "Failed to parse status"; + } + return status; + } + + function parseWarpStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + + if (output.includes("Connected")) { + status.connected = true; + status.state = "connected"; + } else if (output.includes("Connecting")) { + status.state = "connecting"; + } else if (output.includes("Unable") || output.includes("Registration Missing") || output.includes("registration") || output.includes("register")) { + status.state = "needs-auth"; + status.reason = "WARP registration required"; + } else if (!output.includes("Disconnected")) { + status.state = "error"; + status.reason = "Unknown WARP status"; + } + return status; + } + + function parseWireGuardStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + const iface = root.currentConfig?.interface || ""; + + if (iface && output.includes(iface + ":")) { + status.connected = true; + status.state = "connected"; + } + return status; + } + + function parseStatusOutput(output: string): var { + switch (providerName) { + case "tailscale": + return parseTailscaleStatus(output); + case "netbird": + return parseNetBirdStatus(output); + case "warp": + return parseWarpStatus(output); + case "wireguard": + default: + return parseWireGuardStatus(output); + } + } + + function extractAuthUrl(text: string): string { + const urlMatch = text.match(/(https?:\/\/[^\s]+)/); + return urlMatch ? urlMatch[1] : ""; + } + + function createAuthStatus(authUrl: string): var { + return { + connected: false, + state: "needs-auth", + reason: "Authentication required", + authUrl: authUrl + }; + } + + function updateStatus(newStatus: var): void { + const oldState = status.state; + if (newStatus.state === "needs-auth" && !newStatus.authUrl && status.authUrl) { + newStatus.authUrl = status.authUrl; + } + status = newStatus; + root.connected = newStatus.connected; + + if (oldState !== newStatus.state) { + emitStatusToast(newStatus); + } + } + + function emitStatusToast(statusObj: var): void { if (!Config.utilities.toasts.vpnChanged) return; const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; - if (connected) { + + switch (statusObj.state) { + case "connected": Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); - } else { - Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); + break; + case "disconnected": + if (status.connected) { + Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); + } + break; + case "needs-auth": + const authMsg = statusObj.reason || "Authentication required"; + Toaster.toast(qsTr("VPN authentication required"), qsTr("%1: %2").arg(displayName).arg(authMsg), "vpn_lock"); + break; + case "error": + if (status.state === "connected" || status.state === "connecting" || status.state === "needs-auth") { + const errMsg = statusObj.reason || "Unknown error"; + Toaster.toast(qsTr("VPN error"), qsTr("%1: %2").arg(displayName).arg(errMsg), "error"); + } + break; } } + onStatusChanged: { + if (providerName === "warp" && status.state === "needs-auth" && status.reason.includes("registration")) { + warpRegisterProc.exec(["warp-cli", "registration", "new"]); + } + } + + onProviderNameChanged: { + status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + root.connected = false; + statusCheckTimer.start(); + } + Component.onCompleted: root.enabled && statusCheckTimer.start() Process { @@ -127,7 +338,7 @@ Singleton { Process { id: statusProc - command: ["ip", "link", "show"] + command: root.getStatusCommand() // qmllint disable incompatible-type environment: ({ // qmllint enable incompatible-type @@ -136,8 +347,38 @@ Singleton { }) stdout: StdioCollector { onStreamFinished: { - const iface = root.currentConfig ? root.currentConfig.interface : ""; - root.connected = iface && text.includes(iface + ":"); + const newStatus = root.parseStatusOutput(text); + root.updateStatus(newStatus); + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim().length > 0) { + if (text.includes("doesn't appear to be running") || text.includes("failed to connect to local tailscaled") || text.includes("daemon is not running") || text.includes("not running") && (text.includes("netbird") || text.includes("warp"))) { + let cmd = "sudo systemctl start "; + switch (root.providerName) { + case "tailscale": + cmd += "tailscaled"; + break; + case "netbird": + cmd += "netbird"; + break; + case "warp": + cmd += "warp-svc"; + break; + default: + cmd += root.providerName + "d"; + break; + } + const errorStatus = { + connected: false, + state: "disconnected", + reason: `Service not running (run: ${cmd})`, + authUrl: "" + }; + root.updateStatus(errorStatus); + } + } } } } @@ -145,12 +386,59 @@ Singleton { Process { id: connectProc - onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters + onExited: exitCode => { // qmllint disable signal-handler-parameters + if (exitCode !== 0) { + return; + } + + if (root.providerName === "tailscale") { + Qt.callLater(() => { + if (root.status.state !== "needs-auth") { + statusCheckTimer.start(); + } + }); + } else if (root.status.state !== "needs-auth") { + statusCheckTimer.start(); + } + } + stdout: SplitParser { + onRead: data => { + const authUrl = root.extractAuthUrl(data); + if (authUrl) { + root.updateStatus(root.createAuthStatus(authUrl)); + } + } + } stderr: StdioCollector { onStreamFinished: { const error = text.trim(); - if (error && !error.includes("[#]") && !error.includes("already exists")) { - console.warn("VPN connection error:", error); + + if (error.includes("Access denied") || error.includes("checkprefs access denied")) { + const errorStatus = { + connected: false, + state: "disconnected", + reason: "Permission denied. Run in terminal: sudo tailscale set --operator=$USER", + authUrl: "" + }; + root.updateStatus(errorStatus); + return; + } + + if (error.includes("Unknown device type") || error.includes("Protocol not supported")) { + const errorStatus = { + connected: false, + state: "disconnected", + reason: "WireGuard module not loaded. Run: sudo modprobe wireguard", + authUrl: "" + }; + root.updateStatus(errorStatus); + return; + } + + const authUrl = root.extractAuthUrl(error); + + if (authUrl) { + root.updateStatus(root.createAuthStatus(authUrl)); } else if (error.includes("already exists")) { root.connected = true; } @@ -172,6 +460,16 @@ Singleton { } } + Process { + id: warpRegisterProc + + onExited: exitCode => { // qmllint disable signal-handler-parameters + if (exitCode === 0) { + statusCheckTimer.start(); + } + } + } + Timer { id: statusCheckTimer From 1f3656c2f6f715cbb2851732de4efad4f8f4c5d2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:41:26 +1100 Subject: [PATCH 200/409] fix: performance cpu/gpu name overlapping usage Fixes #1337 --- modules/dashboard/Performance.qml | 85 +++++++++++++++---------------- 1 file changed, 40 insertions(+), 45 deletions(-) diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 8d8d2c016..75198254f 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -358,67 +358,60 @@ Item { anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom - width: parent.width * heroCard.animatedUsage + implicitWidth: parent.width * heroCard.animatedUsage color: Qt.alpha(heroCard.accentColor, 0.15) } - ColumnLayout { - anchors.fill: parent + CardHeader { + anchors.left: parent.left + anchors.top: parent.top anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large - anchors.topMargin: Appearance.padding.normal - anchors.bottomMargin: Appearance.padding.normal - spacing: Appearance.spacing.small + anchors.topMargin: Math.round(Appearance.padding.large * 1.2) - CardHeader { - icon: heroCard.icon - title: heroCard.title - accentColor: heroCard.accentColor - } - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - spacing: Appearance.spacing.normal - - Column { - Layout.alignment: Qt.AlignBottom - Layout.fillWidth: true - spacing: Appearance.spacing.small + width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Appearance.spacing.normal + icon: heroCard.icon + title: heroCard.title + accentColor: heroCard.accentColor + } - Row { - spacing: Appearance.spacing.small + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Math.round(Appearance.padding.large * 1.2) + anchors.bottomMargin: Math.round(Appearance.padding.large * 1.3) - StyledText { - text: heroCard.secondaryValue - font.pointSize: Appearance.font.size.normal - font.weight: Font.Medium - } + spacing: Appearance.spacing.small - StyledText { - text: heroCard.secondaryLabel - font.pointSize: Appearance.font.size.small - color: Colours.palette.m3onSurfaceVariant - anchors.baseline: parent.children[0].baseline - } - } + Row { + spacing: Appearance.spacing.small - ProgressBar { - width: parent.width * 0.5 - height: 6 - value: heroCard.tempProgress - fgColor: heroCard.accentColor - bgColor: Qt.alpha(heroCard.accentColor, 0.2) - } + StyledText { + text: heroCard.secondaryValue + font.pointSize: Appearance.font.size.normal + font.weight: Font.Medium } - Item { - Layout.fillWidth: true + StyledText { + text: heroCard.secondaryLabel + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + anchors.baseline: parent.children[0].baseline } } + + ProgressBar { + implicitWidth: parent.width * 0.5 + implicitHeight: 6 + value: heroCard.tempProgress + fgColor: heroCard.accentColor + bgColor: Qt.alpha(heroCard.accentColor, 0.2) + } } Column { + id: usageColumn + anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter anchors.margins: Appearance.padding.large @@ -426,6 +419,8 @@ Item { spacing: 0 StyledText { + id: usageLabel + anchors.right: parent.right text: heroCard.mainLabel font.pointSize: Appearance.font.size.normal From 3bdffca061f0a2b55728f15f20334b5740858f08 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 30 Mar 2026 01:54:06 +1100 Subject: [PATCH 201/409] feat: c++ visualiser Closes #1126 --- modules/background/Visualiser.qml | 89 ++------ plugin/src/Caelestia/Internal/CMakeLists.txt | 1 + .../src/Caelestia/Internal/visualiserbars.cpp | 198 ++++++++++++++++++ .../src/Caelestia/Internal/visualiserbars.hpp | 72 +++++++ 4 files changed, 285 insertions(+), 75 deletions(-) create mode 100644 plugin/src/Caelestia/Internal/visualiserbars.cpp create mode 100644 plugin/src/Caelestia/Internal/visualiserbars.hpp diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 780abdf4b..2f59a3001 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import Quickshell -import Quickshell.Widgets +import Caelestia.Internal import Caelestia.Services import qs.components import qs.services @@ -55,25 +55,29 @@ Item { service: Audio.cava } - Item { - id: content + VisualiserBars { + id: bars anchors.fill: parent anchors.margins: Config.border.thickness anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing - Side { - content: content - } - Side { - content: content - isRight: true - } + values: Audio.cava.values + primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) + secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) + rounding: Appearance.rounding.small * Config.background.visualiser.rounding + spacing: Appearance.spacing.small * Config.background.visualiser.spacing + animationDuration: Appearance.anim.durations.normal Behavior on anchors.leftMargin { Anim {} } } + + FrameAnimation { + running: root.opacity > 0 && !bars.settled + onTriggered: bars.advance(frameTime) + } } } } @@ -85,69 +89,4 @@ Item { Behavior on opacity { Anim {} } - - component Side: Repeater { - id: side - - required property Item content - property bool isRight - - model: Config.services.visualiserBars - - ClippingRectangle { - id: bar - - required property int modelData - property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1])) - - clip: true - - x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0) - implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing - - y: side.content.height - height - implicitHeight: bar.value * side.content.height * 0.4 - - color: "transparent" - topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding - topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding - - Rectangle { - topLeftRadius: parent.topLeftRadius - topRightRadius: parent.topRightRadius - - gradient: Gradient { - orientation: Gradient.Vertical - - GradientStop { - position: 0 - color: Qt.alpha(Colours.palette.m3primary, 0.7) - - Behavior on color { - CAnim {} - } - } - GradientStop { - position: 1 - color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) - - Behavior on color { - CAnim {} - } - } - } - - anchors.left: parent.left - anchors.right: parent.right - y: parent.height - height - implicitHeight: side.content.height * 0.4 - } - - Behavior on value { - Anim { - duration: Appearance.anim.durations.small - } - } - } - } } diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index 85e85c856..bc4a6948d 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -9,6 +9,7 @@ qml_module(caelestia-internal hyprextras.hpp hyprextras.cpp logindmanager.hpp logindmanager.cpp sparklineitem.hpp sparklineitem.cpp + visualiserbars.hpp visualiserbars.cpp LIBRARIES Qt::Gui Qt::Quick diff --git a/plugin/src/Caelestia/Internal/visualiserbars.cpp b/plugin/src/Caelestia/Internal/visualiserbars.cpp new file mode 100644 index 000000000..926468b07 --- /dev/null +++ b/plugin/src/Caelestia/Internal/visualiserbars.cpp @@ -0,0 +1,198 @@ +#include "visualiserbars.hpp" + +#include +#include +#include +#include +#include +#include + +namespace caelestia::internal { + +VisualiserBars::VisualiserBars(QQuickItem* parent) + : QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void VisualiserBars::advance(qreal dt) { + if (m_displayValues.isEmpty() || m_settled) + return; + + // dt is in seconds (from FrameAnimation.frameTime), convert to ms + const qreal dtMs = dt * 1000.0; + const qreal tau = m_animationDuration / 3.0; + const qreal alpha = 1.0 - std::exp(-dtMs / tau); + + bool allSettled = true; + + for (qsizetype i = 0; i < m_displayValues.size(); ++i) { + const double diff = m_targetValues[i] - m_displayValues[i]; + + if (std::abs(diff) > 0.001) { + m_displayValues[i] += diff * alpha; + allSettled = false; + } else { + m_displayValues[i] = m_targetValues[i]; + } + } + + update(); + + if (allSettled && !m_settled) { + m_settled = true; + emit settledChanged(); + } +} + +void VisualiserBars::paint(QPainter* painter) { + if (m_displayValues.isEmpty()) + return; + + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(Qt::NoPen); + + const qreal h = height(); + const qreal maxBarHeight = h * 0.4; + + QLinearGradient gradient(0, h - maxBarHeight, 0, h); + gradient.setColorAt(0, m_primaryColor); + gradient.setColorAt(1, m_secondaryColor); + painter->setBrush(gradient); + + drawSide(painter, false); + drawSide(painter, true); +} + +void VisualiserBars::drawSide(QPainter* painter, bool rightSide) { + const qreal w = width(); + const qreal h = height(); + const auto count = m_displayValues.size(); + + if (count == 0) + return; + + const qreal sideWidth = w * 0.4; + const qreal slotWidth = sideWidth / static_cast(count); + const qreal barWidth = slotWidth - m_spacing; + + if (barWidth <= 0) + return; + + const qreal sideOffset = rightSide ? w * 0.6 : 0; + const qreal maxBarHeight = h * 0.4; + + for (qsizetype i = 0; i < count; ++i) { + const qsizetype valueIndex = rightSide ? i : (count - i - 1); + const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0); + const qreal barHeight = value * maxBarHeight; + + if (barHeight <= 0) + continue; + + const qreal x = static_cast(i) * slotWidth + sideOffset; + const qreal y = h - barHeight; + const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight }); + + QPainterPath path; + path.moveTo(x, h); + path.lineTo(x, y + r); + + if (r > 0) { + path.arcTo(x, y, r * 2, r * 2, 180, -90); + path.lineTo(x + barWidth - r, y); + path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90); + } else { + path.lineTo(x, y); + path.lineTo(x + barWidth, y); + } + + path.lineTo(x + barWidth, h); + path.closeSubpath(); + + painter->drawPath(path); + } +} + +QVector VisualiserBars::values() const { + return m_targetValues; +} + +void VisualiserBars::setValues(const QVector& values) { + m_targetValues = values; + + if (m_displayValues.size() != values.size()) { + m_displayValues.resize(values.size(), 0.0); + } + + if (m_settled) { + m_settled = false; + emit settledChanged(); + } + + emit valuesChanged(); +} + +bool VisualiserBars::settled() const { + return m_settled; +} + +QColor VisualiserBars::primaryColor() const { + return m_primaryColor; +} + +void VisualiserBars::setPrimaryColor(const QColor& color) { + if (m_primaryColor == color) + return; + m_primaryColor = color; + emit primaryColorChanged(); + update(); +} + +QColor VisualiserBars::secondaryColor() const { + return m_secondaryColor; +} + +void VisualiserBars::setSecondaryColor(const QColor& color) { + if (m_secondaryColor == color) + return; + m_secondaryColor = color; + emit secondaryColorChanged(); + update(); +} + +qreal VisualiserBars::rounding() const { + return m_rounding; +} + +void VisualiserBars::setRounding(qreal rounding) { + if (qFuzzyCompare(m_rounding, rounding)) + return; + m_rounding = rounding; + emit roundingChanged(); + update(); +} + +qreal VisualiserBars::spacing() const { + return m_spacing; +} + +void VisualiserBars::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing, spacing)) + return; + m_spacing = spacing; + emit spacingChanged(); + update(); +} + +int VisualiserBars::animationDuration() const { + return m_animationDuration; +} + +void VisualiserBars::setAnimationDuration(int duration) { + if (m_animationDuration == duration) + return; + m_animationDuration = duration; + emit animationDurationChanged(); +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/visualiserbars.hpp b/plugin/src/Caelestia/Internal/visualiserbars.hpp new file mode 100644 index 000000000..95c071243 --- /dev/null +++ b/plugin/src/Caelestia/Internal/visualiserbars.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::internal { + +class VisualiserBars : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QVector values READ values WRITE setValues NOTIFY valuesChanged) + Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged) + Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged) + Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged) + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged) + Q_PROPERTY(bool settled READ settled NOTIFY settledChanged) + +public: + explicit VisualiserBars(QQuickItem* parent = nullptr); + + void paint(QPainter* painter) override; + + Q_INVOKABLE void advance(qreal dt); + + [[nodiscard]] QVector values() const; + void setValues(const QVector& values); + + [[nodiscard]] QColor primaryColor() const; + void setPrimaryColor(const QColor& color); + + [[nodiscard]] QColor secondaryColor() const; + void setSecondaryColor(const QColor& color); + + [[nodiscard]] qreal rounding() const; + void setRounding(qreal rounding); + + [[nodiscard]] qreal spacing() const; + void setSpacing(qreal spacing); + + [[nodiscard]] int animationDuration() const; + void setAnimationDuration(int duration); + + [[nodiscard]] bool settled() const; + +signals: + void valuesChanged(); + void primaryColorChanged(); + void secondaryColorChanged(); + void roundingChanged(); + void spacingChanged(); + void animationDurationChanged(); + void settledChanged(); + +private: + void drawSide(QPainter* painter, bool rightSide); + + QVector m_targetValues; + QVector m_displayValues; + QColor m_primaryColor; + QColor m_secondaryColor; + qreal m_rounding = 0.0; + qreal m_spacing = 0.0; + int m_animationDuration = 200; + bool m_settled = true; +}; + +} // namespace caelestia::internal From 3fe54c5be808e702903925df501a62cabfe73a13 Mon Sep 17 00:00:00 2001 From: Chea Vuthearith <159639139+chea-vuthearith@users.noreply.github.com> Date: Mon, 30 Mar 2026 10:35:56 +0700 Subject: [PATCH 202/409] feat: add c-p and c-n to vim bindings (#1325) Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- modules/launcher/Content.qml | 4 ++-- modules/session/Content.qml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 88ff73e6d..8076eb544 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -110,10 +110,10 @@ Item { return; if (event.modifiers & Qt.ControlModifier) { - if (event.key === Qt.Key_J) { + if (event.key === Qt.Key_J || event.key === Qt.Key_N) { list.currentList?.incrementCurrentIndex(); event.accepted = true; - } else if (event.key === Qt.Key_K) { + } else if (event.key === Qt.Key_K || event.key === Qt.Key_P) { list.currentList?.decrementCurrentIndex(); event.accepted = true; } diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 405fce1ce..0f64ec4b4 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -96,10 +96,10 @@ Column { return; if (event.modifiers & Qt.ControlModifier) { - if (event.key === Qt.Key_J && KeyNavigation.down) { + if ((event.key === Qt.Key_J || event.key === Qt.Key_N) && KeyNavigation.down) { KeyNavigation.down.focus = true; event.accepted = true; - } else if (event.key === Qt.Key_K && KeyNavigation.up) { + } else if ((event.key === Qt.Key_K || event.key === Qt.Key_P) && KeyNavigation.up) { KeyNavigation.up.focus = true; event.accepted = true; } From 36aac876cca81c992b7d412d3639ac56fd25bd02 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AB=E5=A5=88=E8=A6=8B=20=E3=83=AC=E3=82=A4?= Date: Tue, 31 Mar 2026 10:59:16 +0530 Subject: [PATCH 203/409] feat: add option to change notif dock placeholder (#1190) * config: added option to change NotifDock Pictures * NotifDock: change config option `notifDockPic` to `noNotifsPic` and similar for lockscreen too * lockscreenNoNotifsPic -> lockNoNotifsPic * fix lint --------- Co-authored-by: Soramane <61896496+soramanew@users.noreply.github.com> --- README.md | 2 ++ config/Config.qml | 4 +++- config/UserPaths.qml | 2 ++ modules/controlcenter/notifications/NotificationsPane.qml | 6 +++--- modules/lock/NotifDock.qml | 3 ++- modules/sidebar/NotifDock.qml | 4 ++-- 6 files changed, 14 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 0c96b29fa..43d04fb13 100644 --- a/README.md +++ b/README.md @@ -600,6 +600,8 @@ default, you must create it manually. "paths": { "mediaGif": "root:/assets/bongocat.gif", "sessionGif": "root:/assets/kurukuru.gif", + "noNotifsPic": "root:/assets/dino.png", + "lockNoNotifsPic": "root:/assets/dino.png", "wallpaperDir": "~/Pictures/Wallpapers", "lyricsDir": "~/Music/lyrics" }, diff --git a/config/Config.qml b/config/Config.qml index df684b425..cdfd7123a 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -394,7 +394,9 @@ Singleton { wallpaperDir: paths.wallpaperDir, lyricsDir: paths.lyricsDir, sessionGif: paths.sessionGif, - mediaGif: paths.mediaGif + mediaGif: paths.mediaGif, + noNotifsPic: paths.noNotifsPic, + lockNoNotifsPic: paths.lockNoNotifsPic }; } diff --git a/config/UserPaths.qml b/config/UserPaths.qml index b7a2e34ba..3726017c5 100644 --- a/config/UserPaths.qml +++ b/config/UserPaths.qml @@ -6,4 +6,6 @@ JsonObject { property string lyricsDir: `${Paths.home}/Music/lyrics/` property string sessionGif: "root:/assets/kurukuru.gif" property string mediaGif: "root:/assets/bongocat.gif" + property string noNotifsPic: "root:/assets/dino.png" + property string lockNoNotifsPic: "root:/assets/dino.png" } diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml index 41920f69f..8d58e9e82 100644 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound +import ".." +import "../components" import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets -import ".." -import "../components" import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services import qs.config diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 62439d0c3..50fe21533 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -9,6 +9,7 @@ import qs.components.containers import qs.components.effects import qs.services import qs.config +import qs.utils ColumnLayout { id: root @@ -49,7 +50,7 @@ ColumnLayout { Image { asynchronous: true - source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) + source: Paths.absolutePath(Config.paths.lockNoNotifsPic) fillMode: Image.PreserveAspectFit sourceSize.width: clipRect.width * 0.8 diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index f041cc9be..9c677f7e7 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts -import Quickshell import Quickshell.Widgets import qs.components import qs.components.containers @@ -10,6 +9,7 @@ import qs.components.controls import qs.components.effects import qs.services import qs.config +import qs.utils Item { id: root @@ -96,7 +96,7 @@ Item { Image { asynchronous: true - source: Quickshell.shellPath("assets/dino.png") + source: Paths.absolutePath(Config.paths.noNotifsPic) fillMode: Image.PreserveAspectFit sourceSize.width: clipRect.width * 0.8 From a2a63977c1b165d828c2d6576db7f1a425b72bc0 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:03:52 +1100 Subject: [PATCH 204/409] fix: guard pw timer & stream for null --- plugin/src/Caelestia/Services/audiocollector.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp index fb051ccbd..72b9ce6b5 100644 --- a/plugin/src/Caelestia/Services/audiocollector.cpp +++ b/plugin/src/Caelestia/Services/audiocollector.cpp @@ -30,6 +30,12 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC }; m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this); + if (!m_timer) { + qWarning() << "PipeWireWorker::init: failed to create timer"; + pw_main_loop_destroy(m_loop); + pw_deinit(); + return; + } pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false); auto props = pw_properties_new( @@ -55,6 +61,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); pw_stream_events events{}; + events.version = PW_VERSION_STREAM_EVENTS; events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) { auto* self = static_cast(data); self->streamStateChanged(state); @@ -65,6 +72,12 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) }; m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this); + if (!m_stream) { + qWarning() << "PipeWireWorker::init: failed to create stream"; + pw_main_loop_destroy(m_loop); + pw_deinit(); + return; + } const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY, static_cast( From 4714fe87956c64c1c4981fed503f2d3a1097f995 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:17:22 +1100 Subject: [PATCH 205/409] feat: use logging categories --- modules/Shortcuts.qml | 10 +++++++- .../appearance/AppearancePane.qml | 9 +++++++- .../Internal/cachingimagemanager.cpp | 11 +++++---- plugin/src/Caelestia/Internal/hyprextras.cpp | 23 ++++++++++--------- .../src/Caelestia/Internal/logindmanager.cpp | 14 ++++++----- .../src/Caelestia/Services/audiocollector.cpp | 13 +++++++---- .../src/Caelestia/Services/audioprovider.cpp | 7 ++++-- .../src/Caelestia/Services/cavaprovider.cpp | 9 +++++--- plugin/src/Caelestia/Services/service.cpp | 1 - plugin/src/Caelestia/appdb.cpp | 7 ++++-- plugin/src/Caelestia/cutils.cpp | 21 +++++++++-------- plugin/src/Caelestia/imageanalyser.cpp | 5 +++- plugin/src/Caelestia/requests.cpp | 7 ++++-- plugin/src/Caelestia/toaster.cpp | 1 - services/Nmcli.qml | 11 +++++++-- services/VPN.qml | 9 +++++++- 16 files changed, 106 insertions(+), 52 deletions(-) diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index b4633e8f5..9ee796769 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -1,3 +1,4 @@ +import QtQuick import Quickshell import Quickshell.Io import Caelestia @@ -115,7 +116,7 @@ Scope { const visibilities = Visibilities.getForActive(); visibilities[drawer] = !visibilities[drawer]; } else { - console.warn(`[IPC] Drawer "${drawer}" does not exist`); + console.warn(lc, `Drawer "${drawer}" does not exist`); } } @@ -154,4 +155,11 @@ Scope { target: "toaster" } + + LoggingCategory { + id: lc + + name: "caelestia.qml.shortcuts" + defaultLogLevel: LoggingCategory.Info + } } diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index c42220762..c0ccfcd7a 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -136,7 +136,7 @@ Item { onStatusChanged: { if (status === Loader.Error) { - console.error("[AppearancePane] Wallpaper loader error!"); + console.error(lc, "Wallpaper loader error!"); } } @@ -259,4 +259,11 @@ Item { rightContent: appearanceRightContentComponent } + + LoggingCategory { + id: lc + + name: "caelestia.qml.controlcenter.appearance" + defaultLogLevel: LoggingCategory.Info + } } diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp index 9f63f99a2..46152d2a9 100644 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp @@ -6,9 +6,12 @@ #include #include #include +#include #include #include +Q_LOGGING_CATEGORY(lcCim, "caelestia.internal.cim", QtInfoMsg) + namespace caelestia::internal { qreal CachingImageManager::effectiveScale() const { @@ -132,7 +135,7 @@ void CachingImageManager::updateSource(const QString& path) { emit cachePathChanged(); if (!cache.isLocalFile()) { - qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; + qCWarning(lcCim) << "updateSource: cachePath" << cache << "is not a local file"; return; } @@ -161,7 +164,7 @@ void CachingImageManager::createCache( QImage image(path); if (image.isNull()) { - qWarning() << "CachingImageManager::createCache: failed to read" << path; + qCWarning(lcCim) << "createCache: failed to read" << path; return; } @@ -188,7 +191,7 @@ void CachingImageManager::createCache( const QString parent = QFileInfo(cache).absolutePath(); if (!QDir().mkpath(parent) || !image.save(cache)) { - qWarning() << "CachingImageManager::createCache: failed to save to" << cache; + qCWarning(lcCim) << "createCache: failed to save to" << cache; } }); } @@ -196,7 +199,7 @@ void CachingImageManager::createCache( QString CachingImageManager::sha256sum(const QString& path) { QFile file(path); if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "CachingImageManager::sha256sum: failed to open" << path; + qCWarning(lcCim) << "sha256sum: failed to open" << path; return ""; } diff --git a/plugin/src/Caelestia/Internal/hyprextras.cpp b/plugin/src/Caelestia/Internal/hyprextras.cpp index ab18d2f42..e95dfa574 100644 --- a/plugin/src/Caelestia/Internal/hyprextras.cpp +++ b/plugin/src/Caelestia/Internal/hyprextras.cpp @@ -3,8 +3,11 @@ #include #include #include +#include #include +Q_LOGGING_CATEGORY(lcHypr, "caelestia.internal.hypr", QtInfoMsg) + namespace caelestia::internal::hypr { HyprExtras::HyprExtras(QObject* parent) @@ -16,8 +19,7 @@ HyprExtras::HyprExtras(QObject* parent) , m_devices(new HyprDevices(this)) { const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); if (his.isEmpty()) { - qWarning() - << "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; + qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; return; } @@ -26,8 +28,7 @@ HyprExtras::HyprExtras(QObject* parent) hyprDir = "/tmp/hypr/" + his; if (!QDir(hyprDir).exists()) { - qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " - "Hyprland socket."; + qCWarning(lcHypr) << "Hyprland socket directory does not exist. Unable to connect to Hyprland socket."; return; } } @@ -62,7 +63,7 @@ void HyprExtras::message(const QString& message) { makeRequest(message, [](bool success, const QByteArray& res) { if (!success) { - qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); + qCWarning(lcHypr) << "message: request error:" << QString::fromUtf8(res); } }); } @@ -74,7 +75,7 @@ void HyprExtras::batchMessage(const QStringList& messages) { makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { if (!success) { - qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); + qCWarning(lcHypr) << "batchMessage: request error:" << QString::fromUtf8(res); } }); } @@ -95,7 +96,7 @@ void HyprExtras::applyOptions(const QVariantHash& options) { if (success) { refreshOptions(); } else { - qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); + qCWarning(lcHypr) << "applyOptions: request error" << QString::fromUtf8(res); } }); } @@ -145,15 +146,15 @@ void HyprExtras::refreshDevices() { void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { if (!m_socketValid) { - qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; + qCWarning(lcHypr) << "socketError: unable to connect to Hyprland event socket:" << error; } else { - qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; + qCWarning(lcHypr) << "socketError: Hyprland event socket error:" << error; } } void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::UnconnectedState && m_socketValid) { - qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; + qCWarning(lcHypr) << "socketStateChanged: Hyprland event socket disconnected."; } m_socketValid = state == QLocalSocket::ConnectedState; @@ -206,7 +207,7 @@ HyprExtras::SocketPtr HyprExtras::makeRequest( }); QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { - qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; + qCWarning(lcHypr) << "makeRequest: error making request:" << err << "| request:" << request; callback(false, {}); socket->close(); }); diff --git a/plugin/src/Caelestia/Internal/logindmanager.cpp b/plugin/src/Caelestia/Internal/logindmanager.cpp index 4194ee1e7..740005d9d 100644 --- a/plugin/src/Caelestia/Internal/logindmanager.cpp +++ b/plugin/src/Caelestia/Internal/logindmanager.cpp @@ -4,6 +4,9 @@ #include #include #include +#include + +Q_LOGGING_CATEGORY(lcLogindManager, "caelestia.internal.logindmanager", QtInfoMsg) namespace caelestia::internal { @@ -11,7 +14,7 @@ LogindManager::LogindManager(QObject* parent) : QObject(parent) { auto bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { - qWarning() << "LogindManager::LogindManager: failed to connect to system bus:" << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to system bus:" << bus.lastError().message(); return; } @@ -19,14 +22,13 @@ LogindManager::LogindManager(QObject* parent) "PrepareForSleep", this, SLOT(handlePrepareForSleep(bool))); if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to PrepareForSleep signal:" - << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to PrepareForSleep signal:" << bus.lastError().message(); } QDBusInterface login1("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", bus); const QDBusReply reply = login1.call("GetSession", "auto"); if (!reply.isValid()) { - qWarning() << "LogindManager::LogindManager: failed to get session path"; + qCWarning(lcLogindManager) << "Failed to get session path"; return; } const auto sessionPath = reply.value().path(); @@ -35,14 +37,14 @@ LogindManager::LogindManager(QObject* parent) SLOT(handleLockRequested())); if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to Lock signal:" << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to Lock signal:" << bus.lastError().message(); } ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Unlock", this, SLOT(handleUnlockRequested())); if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to Unlock signal:" << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to Unlock signal:" << bus.lastError().message(); } } diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp index 72b9ce6b5..69309f756 100644 --- a/plugin/src/Caelestia/Services/audiocollector.cpp +++ b/plugin/src/Caelestia/Services/audiocollector.cpp @@ -3,13 +3,16 @@ #include "service.hpp" #include #include -#include +#include #include #include #include #include #include +Q_LOGGING_CATEGORY(lcAc, "caelestia.services.ac", QtInfoMsg) +Q_LOGGING_CATEGORY(lcAcWorker, "caelestia.services.ac.worker", QtInfoMsg) + namespace caelestia::services { PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) @@ -23,7 +26,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) m_loop = pw_main_loop_new(nullptr); if (!m_loop) { - qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop"; + qCWarning(lcAcWorker) << "init: failed to create PipeWire main loop"; pw_deinit(); return; } @@ -31,7 +34,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC }; m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this); if (!m_timer) { - qWarning() << "PipeWireWorker::init: failed to create timer"; + qCWarning(lcAcWorker) << "init: failed to create timer"; pw_main_loop_destroy(m_loop); pw_deinit(); return; @@ -73,7 +76,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this); if (!m_stream) { - qWarning() << "PipeWireWorker::init: failed to create stream"; + qCWarning(lcAcWorker) << "init: failed to create stream"; pw_main_loop_destroy(m_loop); pw_deinit(); return; @@ -84,7 +87,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), params, 1); if (success < 0) { - qWarning() << "PipeWireWorker::init: failed to connect stream"; + qCWarning(lcAcWorker) << "init: failed to connect stream"; pw_stream_destroy(m_stream); pw_main_loop_destroy(m_loop); pw_deinit(); diff --git a/plugin/src/Caelestia/Services/audioprovider.cpp b/plugin/src/Caelestia/Services/audioprovider.cpp index 1fac9eea8..d2916f45e 100644 --- a/plugin/src/Caelestia/Services/audioprovider.cpp +++ b/plugin/src/Caelestia/Services/audioprovider.cpp @@ -2,9 +2,12 @@ #include "audiocollector.hpp" #include "service.hpp" -#include +#include #include +Q_LOGGING_CATEGORY(lcAp, "caelestia.services.ap", QtInfoMsg) +Q_LOGGING_CATEGORY(lcApProcessor, "caelestia.services.ap.processor", QtInfoMsg) + namespace caelestia::services { AudioProcessor::AudioProcessor(QObject* parent) @@ -48,7 +51,7 @@ AudioProvider::~AudioProvider() { void AudioProvider::init() { if (!m_processor) { - qWarning() << "AudioProvider::init: attempted to init with no processor set"; + qCWarning(lcAp) << "init: attempted to init with no processor set"; return; } diff --git a/plugin/src/Caelestia/Services/cavaprovider.cpp b/plugin/src/Caelestia/Services/cavaprovider.cpp index 7b6cc1f20..a57f4040a 100644 --- a/plugin/src/Caelestia/Services/cavaprovider.cpp +++ b/plugin/src/Caelestia/Services/cavaprovider.cpp @@ -4,7 +4,10 @@ #include "audioprovider.hpp" #include #include -#include +#include + +Q_LOGGING_CATEGORY(lcCava, "caelestia.services.cava", QtInfoMsg) +Q_LOGGING_CATEGORY(lcCavaProcessor, "caelestia.services.cava.processor", QtInfoMsg) namespace caelestia::services { @@ -57,7 +60,7 @@ void CavaProcessor::process() { void CavaProcessor::setBars(int bars) { if (bars < 0) { - qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0."; + qCWarning(lcCavaProcessor) << "setBars: bars must be greater than 0. Setting to 0."; bars = 0; } @@ -109,7 +112,7 @@ int CavaProvider::bars() const { void CavaProvider::setBars(int bars) { if (bars < 0) { - qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0."; + qCWarning(lcCava) << "setBars: bars must be greater than 0. Setting to 0."; bars = 0; } diff --git a/plugin/src/Caelestia/Services/service.cpp b/plugin/src/Caelestia/Services/service.cpp index bc2156717..4e1921e87 100644 --- a/plugin/src/Caelestia/Services/service.cpp +++ b/plugin/src/Caelestia/Services/service.cpp @@ -1,6 +1,5 @@ #include "service.hpp" -#include #include namespace caelestia::services { diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 1e33990da..8d80ced97 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -1,9 +1,12 @@ #include "appdb.hpp" +#include #include #include #include +Q_LOGGING_CATEGORY(lcAppDb, "caelestia.appdb", QtInfoMsg) + namespace caelestia { AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) @@ -180,7 +183,7 @@ void AppDb::setFavouriteApps(const QStringList& favApps) { if (re.isValid()) { m_favouriteAppsRegex << re; } else { - qWarning() << "AppDb::setFavouriteApps: Regular expression is not valid: " << re.pattern(); + qCWarning(lcAppDb) << "setFavouriteApps: regular expression is not valid:" << re.pattern(); } } @@ -218,7 +221,7 @@ void AppDb::incrementFrequency(const QString& id) { emit appsChanged(); } } else { - qWarning() << "AppDb::incrementFrequency: could not find app with id" << id; + qCWarning(lcAppDb) << "incrementFrequency: could not find app with id" << id; } } diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index b6ec33a88..28a0e1784 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -6,8 +6,11 @@ #include #include #include +#include #include +Q_LOGGING_CATEGORY(lcCUtils, "caelestia.cutils", QtInfoMsg) + namespace caelestia { void CUtils::saveItem(QQuickItem* target, const QUrl& path) { @@ -32,17 +35,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { if (!target) { - qWarning() << "CUtils::saveItem: a target is required"; + qCWarning(lcCUtils) << "saveItem: a target is required"; return; } if (!path.isLocalFile()) { - qWarning() << "CUtils::saveItem:" << path << "is not a local file"; + qCWarning(lcCUtils) << "saveItem:" << path << "is not a local file"; return; } if (!target->window()) { - qWarning() << "CUtils::saveItem: unable to save target" << target << "without a window"; + qCWarning(lcCUtils) << "saveItem: unable to save target" << target << "without a window"; return; } @@ -82,7 +85,7 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q onSaved.call(args); } } else { - qWarning() << "CUtils::saveItem: failed to save" << path; + qCWarning(lcCUtils) << "saveItem: failed to save" << path; if (onFailed.isCallable()) { if (engine) { onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); @@ -99,17 +102,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { if (!source.isLocalFile()) { - qWarning() << "CUtils::copyFile: source" << source << "is not a local file"; + qCWarning(lcCUtils) << "copyFile: source" << source << "is not a local file"; return false; } if (!target.isLocalFile()) { - qWarning() << "CUtils::copyFile: target" << target << "is not a local file"; + qCWarning(lcCUtils) << "copyFile: target" << target << "is not a local file"; return false; } if (overwrite && QFile::exists(target.toLocalFile())) { if (!QFile::remove(target.toLocalFile())) { - qWarning() << "CUtils::copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); + qCWarning(lcCUtils) << "copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); return false; } } @@ -119,7 +122,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co bool CUtils::deleteFile(const QUrl& path) const { if (!path.isLocalFile()) { - qWarning() << "CUtils::deleteFile: path" << path << "is not a local file"; + qCWarning(lcCUtils) << "deleteFile: path" << path << "is not a local file"; return false; } @@ -128,7 +131,7 @@ bool CUtils::deleteFile(const QUrl& path) const { QString CUtils::toLocalFile(const QUrl& url) const { if (!url.isLocalFile()) { - qWarning() << "CUtils::toLocalFile: given url is not a local file" << url; + qCWarning(lcCUtils) << "toLocalFile: given url is not a local file" << url; return QString(); } diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp index 0b623162b..7f3ba5261 100644 --- a/plugin/src/Caelestia/imageanalyser.cpp +++ b/plugin/src/Caelestia/imageanalyser.cpp @@ -4,8 +4,11 @@ #include #include #include +#include #include +Q_LOGGING_CATEGORY(lcImageAnalyser, "caelestia.imageanalyser", QtInfoMsg) + namespace caelestia { ImageAnalyser::ImageAnalyser(QObject* parent) @@ -152,7 +155,7 @@ void ImageAnalyser::update() { void ImageAnalyser::analyse(QPromise& promise, const QImage& image, int rescaleSize) { if (image.isNull()) { - qWarning() << "ImageAnalyser::analyse: image is null"; + qCWarning(lcImageAnalyser) << "analyse: image is null"; return; } diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp index dbc746ed1..862dea7ff 100644 --- a/plugin/src/Caelestia/requests.cpp +++ b/plugin/src/Caelestia/requests.cpp @@ -1,11 +1,14 @@ #include "requests.hpp" #include +#include #include #include #include #include +Q_LOGGING_CATEGORY(lcRequests, "caelestia.requests", QtInfoMsg) + namespace caelestia { Requests::Requests(QObject* parent) @@ -14,7 +17,7 @@ Requests::Requests(QObject* parent) void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { if (!onSuccess.isCallable()) { - qWarning() << "Requests::get: onSuccess is not callable"; + qCWarning(lcRequests) << "get: onSuccess is not callable"; return; } @@ -41,7 +44,7 @@ void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSVal } else if (onError.isCallable()) { onError.call({ reply->errorString() }); } else { - qWarning() << "Requests::get: request failed with error" << reply->errorString(); + qCWarning(lcRequests) << "get: request failed with error" << reply->errorString(); } reply->deleteLater(); diff --git a/plugin/src/Caelestia/toaster.cpp b/plugin/src/Caelestia/toaster.cpp index 978805de0..b51c77f8d 100644 --- a/plugin/src/Caelestia/toaster.cpp +++ b/plugin/src/Caelestia/toaster.cpp @@ -1,6 +1,5 @@ #include "toaster.hpp" -#include #include #include diff --git a/services/Nmcli.qml b/services/Nmcli.qml index dc783f8af..ea5d7eb31 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -391,7 +391,7 @@ Singleton { } if (!result.success && root.pendingConnection && retries < maxRetries) { - console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + console.warn(lc, "Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); Qt.callLater(() => { connectWireless(ssid, password, bssid, callback, retries + 1); }, 1000); @@ -417,7 +417,7 @@ Singleton { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); + console.warn(lc, "Connection profile creation failed, trying fallback..."); let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; executeCommand(fallbackCmd, fallbackResult => { if (callback) @@ -1277,6 +1277,13 @@ Singleton { } } + LoggingCategory { + id: lc + + name: "caelestia.qml.services.nmcli" + defaultLogLevel: LoggingCategory.Info + } + component CommandProcess: Process { id: proc diff --git a/services/VPN.qml b/services/VPN.qml index bfeacea46..31b683194 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -454,7 +454,7 @@ Singleton { onStreamFinished: { const error = text.trim(); if (error && !error.includes("[#]")) { - console.warn("VPN disconnection error:", error); + console.warn(lc, "Disconnection error:", error); } } } @@ -476,4 +476,11 @@ Singleton { interval: 500 onTriggered: root.checkStatus() } + + LoggingCategory { + id: lc + + name: "caelestia.qml.services.vpn" + defaultLogLevel: LoggingCategory.Info + } } From c2221a6d8e100a8e9b827b6d0f196fe202d87163 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 2 Apr 2026 15:57:24 +1100 Subject: [PATCH 206/409] fix: use height scaled region for top panel detection Same as bottom panel, doesn't take into account the 5px offset this way --- modules/drawers/Interactions.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 464d7777d..aeabf4018 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -42,7 +42,8 @@ CustomMouseArea { } function inTopPanel(panel: Item, x: real, y: real): bool { - return y < Math.max(Config.border.minThickness, Config.border.thickness + panel.height) + panel.y && withinPanelWidth(panel, x, y); + const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property + return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y); } function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { From ffe9bb851af2be20f925a94aecc2a4a9052f42f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas?= <67807483+Mestane@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:37:45 +0300 Subject: [PATCH 207/409] fix: notif image and vpn serialisation (#1370) * fix: serialize VPN interface name when configured via UI * fix: override last icon notification history * screw qmlls --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- config/Config.qml | 2 +- services/NotifData.qml | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index cdfd7123a..d0b24935f 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -326,7 +326,7 @@ Singleton { const provider = { displayName: p.displayName, enabled: p.enabled, - iface: p.iface, + interface: p.iface, name: p.name }; if (p.connectCmd && p.connectCmd.length > 0) { diff --git a/services/NotifData.qml b/services/NotifData.qml index 3fde0145a..d84183e50 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -77,7 +77,8 @@ QtObject { h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - const cache = Paths.notifimagecache + "/${hash}.png"; + Paths; // Screw you qmlls + const cache = `${Paths.notifimagecache}/${hash}.png`; CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { notif.image = cache; notif.dummyImageLoader.active = false; From 9fa889536c73b65725e73eacf6d334141c0718de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C4=B0lyas?= <67807483+Mestane@users.noreply.github.com> Date: Thu, 2 Apr 2026 13:49:22 +0300 Subject: [PATCH 208/409] fix: reset lyrics when player is killed (#1359) Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- services/LyricsService.qml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/services/LyricsService.qml b/services/LyricsService.qml index d41b93b4b..a6b94f275 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -78,8 +78,11 @@ Singleton { function _doLoadLyrics() { const meta = getMetadata(); - if (!meta) + if (!meta) { + lyricsModel.clear(); + root.currentIndex = -1; return; + } loading = true; lyricsModel.clear(); From 736eda671589c418e3b6169d92fdbc4a2e3ff37a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:13:47 +1100 Subject: [PATCH 209/409] feat: c++ resizable list view component Partially fixes notif dock lag --- modules/sidebar/NotifDock.qml | 14 +- modules/sidebar/NotifDockList.qml | 224 ++--- plugin/src/Caelestia/CMakeLists.txt | 1 + .../src/Caelestia/Components/CMakeLists.txt | 7 + .../src/Caelestia/Components/lazylistview.cpp | 936 ++++++++++++++++++ .../src/Caelestia/Components/lazylistview.hpp | 244 +++++ 6 files changed, 1285 insertions(+), 141 deletions(-) create mode 100644 plugin/src/Caelestia/Components/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Components/lazylistview.cpp create mode 100644 plugin/src/Caelestia/Components/lazylistview.hpp diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 9c677f7e7..c00f2bd43 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -1,5 +1,3 @@ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Layouts import Quickshell.Widgets @@ -153,14 +151,10 @@ Item { repeat: true interval: 50 onTriggered: { - let next = null; - for (let i = 0; i < notifList.repeater.count; i++) { - next = notifList.repeater.itemAt(i); - if (!next?.closed) // qmllint disable missing-property - break; - } - if (next) { - next.closeAll(); // qmllint disable missing-property + const first = Notifs.notClosed[0]; + if (first) { + for (const n of Notifs.notClosed.filter(n => n.appName === first.appName)) + n.close(); } else { stop(); } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 7ffb64182..c7d1ebaf5 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -1,168 +1,130 @@ -pragma ComponentBehavior: Bound - import QtQuick import Quickshell +import Caelestia.Components import qs.components import qs.services import qs.config -Item { +LazyListView { id: root required property Props props required property Flickable container required property DrawerVisibilities visibilities - readonly property alias repeater: repeater - readonly property int spacing: Appearance.spacing.small - property bool flag - - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: { - const item = repeater.itemAt(repeater.count - 1); - return item ? item.y + item.implicitHeight : 0; - } - - Repeater { - id: repeater - - model: ScriptModel { - values: { - const map = new Map(); - for (const n of Notifs.notClosed) - map.set(n.appName, null); - for (const n of Notifs.list) - map.set(n.appName, null); - return [...map.keys()]; - } - onValuesChanged: root.flagChanged() + anchors.left: parent?.left + anchors.right: parent?.right + implicitHeight: contentHeight + + spacing: Appearance.spacing.small + cacheBuffer: 200 + + useCustomViewport: true + viewport: Qt.rect(0, container.contentY, width, container.height) + + addDuration: Appearance.anim.durations.expressiveDefaultSpatial + addCurve.type: Easing.BezierSpline + addCurve.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + addFromOpacity: 0 + addFromScale: 0 + + removeDuration: Appearance.anim.durations.normal + removeCurve.type: Easing.BezierSpline + removeCurve.bezierCurve: Appearance.anim.curves.standard + removeToOpacity: 0 + removeToScale: 0.6 + + moveDuration: Appearance.anim.durations.expressiveDefaultSpatial + moveCurve.type: Easing.BezierSpline + moveCurve.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + + model: ScriptModel { + values: { + const map = new Map(); + for (const n of Notifs.notClosed) + map.set(n.appName, null); + for (const n of Notifs.list) + map.set(n.appName, null); + return [...map.keys()]; } - - delegate: NotifGroupDelegate {} } - component NotifGroupDelegate: MouseArea { - id: notif + delegate: Component { + MouseArea { + id: notif - required property int index - required property string modelData + required property int index + required property string modelData - readonly property bool closed: notifInner.notifCount === 0 - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - property int startY + readonly property bool closed: notifInner.notifCount === 0 + property int startY - function closeAll(): void { - for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) - n.close(); - } - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as NotifGroupDelegate; - if (item && !item.closed) - y += item.nonAnimHeight + root.spacing; + function closeAll(): void { + for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) + n.close(); } - return y; - } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); + containmentMask: QtObject { + function contains(p: point): bool { + if (!root.container.contains(notif.mapToItem(root.container, p))) + return false; + return notifInner.contains(p); + } } - } - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + implicitHeight: closed ? 0 : notifInner.implicitHeight - hoverEnabled: true - cursorShape: pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: true - enabled: !closed + hoverEnabled: true + cursorShape: pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: true + enabled: !closed - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - notifInner.toggleExpand(!notifInner.expanded); - else if (event.button === Qt.MiddleButton) - closeAll(); - } - onPositionChanged: event => { - if (pressed) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - notifInner.toggleExpand(diffY > 0); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + notifInner.toggleExpand(!notifInner.expanded); + else if (event.button === Qt.MiddleButton) + closeAll(); } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - closeAll(); - } - - ParallelAnimation { - running: true - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 + onPositionChanged: event => { + if (pressed) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + notifInner.toggleExpand(diffY > 0); + } } - Anim { - target: notif - property: "scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + closeAll(); } - } - ParallelAnimation { - running: notif.closed + NotifGroup { + id: notifInner - Anim { - target: notif - property: "opacity" - to: 0 - } - Anim { - target: notif - property: "scale" - to: 0.6 + modelData: notif.modelData + props: root.props + container: root.container + visibilities: root.visibilities } - } - - NotifGroup { - id: notifInner - modelData: notif.modelData - props: root.props - container: root.container - visibilities: root.visibilities - } - - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on implicitHeight { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 1b7d0e493..426b3acdf 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -57,6 +57,7 @@ qml_module(caelestia PkgConfig::Qalculate ) +add_subdirectory(Components) add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) diff --git a/plugin/src/Caelestia/Components/CMakeLists.txt b/plugin/src/Caelestia/Components/CMakeLists.txt new file mode 100644 index 000000000..f880d3183 --- /dev/null +++ b/plugin/src/Caelestia/Components/CMakeLists.txt @@ -0,0 +1,7 @@ +qml_module(caelestia-components + URI Caelestia.Components + SOURCES + lazylistview.hpp lazylistview.cpp + LIBRARIES + Qt::Quick +) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp new file mode 100644 index 000000000..126c12025 --- /dev/null +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -0,0 +1,936 @@ +#include "lazylistview.hpp" + +#include +#include + +namespace caelestia::components { + +LazyListView::LazyListView(QQuickItem* parent) + : QQuickItem(parent) { + setFlag(ItemHasContents, false); + setClip(true); +} + +LazyListView::~LazyListView() { + for (auto& entry : m_delegates) + destroyDelegate(entry); + for (auto& entry : m_dyingDelegates) + destroyDelegate(entry); +} + +// --- Model & Delegate --- + +QAbstractItemModel* LazyListView::model() const { + return m_model; +} + +void LazyListView::setModel(QAbstractItemModel* model) { + if (m_model == model) + return; + + if (m_model) + disconnectModel(); + + m_model = model; + + if (m_model) + connectModel(); + + resetContent(); + emit modelChanged(); +} + +QQmlComponent* LazyListView::delegate() const { + return m_delegate; +} + +void LazyListView::setDelegate(QQmlComponent* delegate) { + if (m_delegate == delegate) + return; + + m_delegate = delegate; + resetContent(); + emit delegateChanged(); +} + +// --- Layout --- + +qreal LazyListView::spacing() const { + return m_spacing; +} + +void LazyListView::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing, spacing)) + return; + m_spacing = spacing; + emit spacingChanged(); + polish(); +} + +qreal LazyListView::contentHeight() const { + return m_contentHeight; +} + +qreal LazyListView::contentY() const { + return m_contentY; +} + +void LazyListView::setContentY(qreal contentY) { + if (qFuzzyCompare(m_contentY, contentY)) + return; + m_contentY = contentY; + emit contentYChanged(); + polish(); +} + +// --- Viewport --- + +QRectF LazyListView::viewport() const { + return m_viewport; +} + +void LazyListView::setViewport(const QRectF& viewport) { + if (m_viewport == viewport) + return; + m_viewport = viewport; + emit viewportChanged(); + if (m_useCustomViewport) + polish(); +} + +bool LazyListView::useCustomViewport() const { + return m_useCustomViewport; +} + +void LazyListView::setUseCustomViewport(bool use) { + if (m_useCustomViewport == use) + return; + m_useCustomViewport = use; + emit useCustomViewportChanged(); + polish(); +} + +qreal LazyListView::cacheBuffer() const { + return m_cacheBuffer; +} + +void LazyListView::setCacheBuffer(qreal buffer) { + if (qFuzzyCompare(m_cacheBuffer, buffer)) + return; + m_cacheBuffer = buffer; + emit cacheBufferChanged(); + polish(); +} + +// --- Sizing --- + +qreal LazyListView::estimatedHeight() const { + return m_estimatedHeight; +} + +void LazyListView::setEstimatedHeight(qreal height) { + if (qFuzzyCompare(m_estimatedHeight, height)) + return; + m_estimatedHeight = height; + emit estimatedHeightChanged(); + polish(); +} + +qreal LazyListView::effectiveEstimatedHeight() const { + if (m_estimatedHeight >= 0) + return m_estimatedHeight; + if (m_knownHeightCount > 0) + return m_knownHeightSum / m_knownHeightCount; + return 40; +} + +void LazyListView::trackHeight(qreal height) { + m_knownHeightSum += height; + ++m_knownHeightCount; +} + +void LazyListView::untrackHeight(qreal height) { + m_knownHeightSum -= height; + --m_knownHeightCount; +} + +// --- Add Animation --- + +int LazyListView::addDuration() const { + return m_addDuration; +} + +void LazyListView::setAddDuration(int duration) { + if (m_addDuration == duration) + return; + m_addDuration = duration; + emit addDurationChanged(); +} + +QEasingCurve LazyListView::addCurve() const { + return m_addCurve; +} + +void LazyListView::setAddCurve(const QEasingCurve& curve) { + if (m_addCurve == curve) + return; + m_addCurve = curve; + emit addCurveChanged(); +} + +qreal LazyListView::addFromOpacity() const { + return m_addFromOpacity; +} + +void LazyListView::setAddFromOpacity(qreal opacity) { + if (qFuzzyCompare(m_addFromOpacity, opacity)) + return; + m_addFromOpacity = opacity; + emit addFromOpacityChanged(); +} + +qreal LazyListView::addFromScale() const { + return m_addFromScale; +} + +void LazyListView::setAddFromScale(qreal scale) { + if (qFuzzyCompare(m_addFromScale, scale)) + return; + m_addFromScale = scale; + emit addFromScaleChanged(); +} + +// --- Remove Animation --- + +int LazyListView::removeDuration() const { + return m_removeDuration; +} + +void LazyListView::setRemoveDuration(int duration) { + if (m_removeDuration == duration) + return; + m_removeDuration = duration; + emit removeDurationChanged(); +} + +QEasingCurve LazyListView::removeCurve() const { + return m_removeCurve; +} + +void LazyListView::setRemoveCurve(const QEasingCurve& curve) { + if (m_removeCurve == curve) + return; + m_removeCurve = curve; + emit removeCurveChanged(); +} + +qreal LazyListView::removeToOpacity() const { + return m_removeToOpacity; +} + +void LazyListView::setRemoveToOpacity(qreal opacity) { + if (qFuzzyCompare(m_removeToOpacity, opacity)) + return; + m_removeToOpacity = opacity; + emit removeToOpacityChanged(); +} + +qreal LazyListView::removeToScale() const { + return m_removeToScale; +} + +void LazyListView::setRemoveToScale(qreal scale) { + if (qFuzzyCompare(m_removeToScale, scale)) + return; + m_removeToScale = scale; + emit removeToScaleChanged(); +} + +// --- Move Animation --- + +int LazyListView::moveDuration() const { + return m_moveDuration; +} + +void LazyListView::setMoveDuration(int duration) { + if (m_moveDuration == duration) + return; + m_moveDuration = duration; + emit moveDurationChanged(); +} + +QEasingCurve LazyListView::moveCurve() const { + return m_moveCurve; +} + +void LazyListView::setMoveCurve(const QEasingCurve& curve) { + if (m_moveCurve == curve) + return; + m_moveCurve = curve; + emit moveCurveChanged(); +} + +// --- State --- + +int LazyListView::count() const { + return m_model ? m_model->rowCount() : 0; +} + +bool LazyListView::settled() const { + return m_activeAnimations == 0; +} + +// --- QQuickItem Overrides --- + +void LazyListView::componentComplete() { + QQuickItem::componentComplete(); + m_componentComplete = true; + resetContent(); +} + +void LazyListView::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + + if (!m_componentComplete) + return; + + if (!qFuzzyCompare(newGeometry.width(), oldGeometry.width())) { + for (auto& entry : m_delegates) { + if (entry.item) + entry.item->setWidth(newGeometry.width()); + } + } + + polish(); +} + +void LazyListView::updatePolish() { + if (!m_componentComplete || !m_model || !m_delegate) + return; + + relayout(); + syncDelegates(); + positionDelegates(); +} + +// --- Layout Engine --- + +void LazyListView::relayout() { + qreal y = 0; + for (auto& record : m_layout) { + record.targetY = y; + y += (record.heightKnown ? record.height : effectiveEstimatedHeight()) + m_spacing; + } + + const qreal newHeight = m_layout.isEmpty() ? 0 : y - m_spacing; + if (!qFuzzyCompare(m_contentHeight, newHeight)) { + m_contentHeight = newHeight; + emit contentHeightChanged(); + } +} + +QRectF LazyListView::effectiveViewport() const { + if (m_useCustomViewport) + return m_viewport.adjusted(0, -m_cacheBuffer, 0, m_cacheBuffer); + + return QRectF(0, m_contentY - m_cacheBuffer, width(), height() + 2 * m_cacheBuffer); +} + +std::pair LazyListView::computeVisibleRange() const { + if (m_layout.isEmpty()) + return { -1, -1 }; + + const auto vp = effectiveViewport(); + const qreal vpTop = vp.y(); + const qreal vpBottom = vp.y() + vp.height(); + + // Binary search for first visible item + int lo = 0; + int hi = static_cast(m_layout.size()) - 1; + int first = static_cast(m_layout.size()); + + while (lo <= hi) { + const int mid = lo + (hi - lo) / 2; + const auto& record = m_layout[mid]; + const qreal itemBottom = record.targetY + (record.heightKnown ? record.height : effectiveEstimatedHeight()); + + if (itemBottom >= vpTop) { + first = mid; + hi = mid - 1; + } else { + lo = mid + 1; + } + } + + if (first >= static_cast(m_layout.size())) + return { -1, -1 }; + + // Linear scan for last visible item + int last = first; + for (int i = first; i < static_cast(m_layout.size()); ++i) { + if (m_layout[i].targetY > vpBottom) + break; + last = i; + } + + return { first, last }; +} + +// --- Delegate Lifecycle --- + +void LazyListView::syncDelegates() { + const auto [first, last] = computeVisibleRange(); + + // Collect indices that should be alive + QSet visibleIndices; + if (first >= 0) { + for (int i = first; i <= last; ++i) + visibleIndices.insert(i); + } + + // Destroy delegates outside visible range (if not animating) + QList toRemove; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + if (!visibleIndices.contains(it.key()) && !it->animation) { + toRemove.append(it.key()); + } + } + for (int idx : toRemove) { + auto entry = m_delegates.take(idx); + destroyDelegate(entry); + } + + // Create delegates for newly visible indices + if (first >= 0) { + for (int i = first; i <= last; ++i) { + if (m_delegates.contains(i)) + continue; + + auto entry = createDelegate(i); + if (entry.item) { + // Measure height + const qreal h = entry.item->implicitHeight(); + if (h > 0 && !m_layout[i].heightKnown) { + m_layout[i].height = h; + m_layout[i].heightKnown = true; + trackHeight(h); + } + m_delegates.insert(i, std::move(entry)); + } + } + } +} + +LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { + DelegateEntry entry; + entry.modelIndex = modelIndex; + + if (!m_delegate || !m_model) + return entry; + + // Use the delegate component's creation context so the delegate + // can access ids and properties from the scope where it was defined. + auto* compContext = m_delegate->creationContext(); + auto* parentContext = compContext ? compContext : qmlContext(this); + if (!parentContext) + return entry; + + entry.context = new QQmlContext(parentContext, this); + + // Build property map for both context properties and initial properties + const auto roleNames = m_model->roleNames(); + const auto index = m_model->index(modelIndex, 0); + QVariantMap initialProps; + + bool hasModelData = false; + for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { + const auto name = QString::fromUtf8(it.value()); + const auto value = m_model->data(index, it.key()); + entry.context->setContextProperty(name, value); + initialProps.insert(name, value); + if (name == QStringLiteral("modelData")) + hasModelData = true; + } + entry.context->setContextProperty(QStringLiteral("index"), modelIndex); + initialProps.insert(QStringLiteral("index"), modelIndex); + + // Provide modelData for single-role models or if not already provided by role names + if (!hasModelData) { + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + const auto value = m_model->data(index, role); + entry.context->setContextProperty(QStringLiteral("modelData"), value); + initialProps.insert(QStringLiteral("modelData"), value); + } + + auto* obj = m_delegate->beginCreate(entry.context); + entry.item = qobject_cast(obj); + + if (!entry.item) { + delete obj; + delete entry.context; + entry.context = nullptr; + return entry; + } + + // Set initial properties to satisfy required property declarations + m_delegate->setInitialProperties(entry.item, initialProps); + + entry.item->setParentItem(this); + entry.item->setWidth(width()); + m_delegate->completeCreate(); + + // Watch for height changes + connect(entry.item, &QQuickItem::implicitHeightChanged, this, [this, modelIndex] { + if (!m_delegates.contains(modelIndex)) + return; + auto& e = m_delegates[modelIndex]; + if (!e.item) + return; + const qreal h = e.item->implicitHeight(); + if (modelIndex < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[modelIndex].height, h)) { + const qreal oldH = m_layout[modelIndex].height; + const bool wasKnown = m_layout[modelIndex].heightKnown; + m_layout[modelIndex].height = h; + m_layout[modelIndex].heightKnown = true; + if (wasKnown) + untrackHeight(oldH); + trackHeight(h); + polish(); + } + }); + + return entry; +} + +void LazyListView::destroyDelegate(DelegateEntry& entry) { + if (entry.animation) { + entry.animation->stop(); + entry.animation = nullptr; + } + delete entry.item; + entry.item = nullptr; + delete entry.context; + entry.context = nullptr; +} + +void LazyListView::updateDelegateData(DelegateEntry& entry) { + if (!m_model) + return; + + const auto roleNames = m_model->roleNames(); + const auto index = m_model->index(entry.modelIndex, 0); + bool hasModelData = false; + + for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { + const auto name = QString::fromUtf8(it.value()); + const auto value = m_model->data(index, it.key()); + if (entry.context) + entry.context->setContextProperty(name, value); + if (entry.item) + entry.item->setProperty(name.toUtf8().constData(), value); + if (name == QStringLiteral("modelData")) + hasModelData = true; + } + + if (entry.context) + entry.context->setContextProperty(QStringLiteral("index"), entry.modelIndex); + if (entry.item) + entry.item->setProperty("index", entry.modelIndex); + + if (!hasModelData) { + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + const auto value = m_model->data(index, role); + if (entry.context) + entry.context->setContextProperty(QStringLiteral("modelData"), value); + if (entry.item) + entry.item->setProperty("modelData", value); + } +} + +void LazyListView::positionDelegates() { + for (auto& entry : m_delegates) { + if (!entry.item || entry.pendingRemoval) + continue; + + // Don't reposition if a move animation is running on this delegate + if (entry.animation) + continue; + + const int idx = entry.modelIndex; + if (idx < 0 || idx >= static_cast(m_layout.size())) + continue; + + entry.item->setY(m_layout[idx].targetY - m_contentY); + } +} + +// --- Model Connection --- + +void LazyListView::connectModel() { + if (!m_model) + return; + + m_modelConnections = { + connect(m_model, &QAbstractItemModel::rowsInserted, this, &LazyListView::onRowsInserted), + connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &LazyListView::onRowsAboutToBeRemoved), + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LazyListView::onRowsRemoved), + connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), + connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), + connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), + connect(m_model, &QAbstractItemModel::layoutChanged, this, &LazyListView::onModelReset), + connect(m_model, &QObject::destroyed, this, + [this] { + m_model = nullptr; + resetContent(); + emit modelChanged(); + }), + }; +} + +void LazyListView::disconnectModel() { + for (auto& conn : m_modelConnections) + disconnect(conn); + m_modelConnections.clear(); +} + +void LazyListView::resetContent() { + // Stop all animations and destroy all delegates + for (auto& entry : m_delegates) + destroyDelegate(entry); + m_delegates.clear(); + + for (auto& entry : m_dyingDelegates) + destroyDelegate(entry); + m_dyingDelegates.clear(); + + if (m_activeAnimations != 0) { + m_activeAnimations = 0; + emit settledChanged(); + } + + // Reset height tracking + m_knownHeightSum = 0; + m_knownHeightCount = 0; + + // Rebuild layout from model + m_layout.clear(); + if (m_model && m_componentComplete) { + const int rows = m_model->rowCount(); + m_layout.resize(rows); + for (int i = 0; i < rows; ++i) { + m_layout[i].height = 0; + m_layout[i].heightKnown = false; + } + emit countChanged(); + } + + polish(); +} + +void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last) { + if (parent.isValid()) + return; + + const int insertCount = last - first + 1; + + // Capture old positions of existing delegates for move animation + QHash oldPositions; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + if (it.key() >= first) + oldPositions.insert(it.key(), m_layout[it.key()].targetY); + } + + // Insert new layout records + m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false }); + + // Shift existing delegate indices + QHash shifted; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + int newIdx = it.key() >= first ? it.key() + insertCount : it.key(); + auto entry = std::move(it.value()); + entry.modelIndex = newIdx; + if (entry.context) + entry.context->setContextProperty(QStringLiteral("index"), newIdx); + shifted.insert(newIdx, std::move(entry)); + } + m_delegates = std::move(shifted); + + relayout(); + syncDelegates(); + positionDelegates(); + + // Animate new items + for (int i = first; i <= last; ++i) { + if (m_delegates.contains(i) && m_addDuration > 0) + startAddAnimation(m_delegates[i]); + } + + // Animate displaced items + for (auto it = oldPositions.begin(); it != oldPositions.end(); ++it) { + const int newIdx = it.key() + insertCount; + if (m_delegates.contains(newIdx) && m_moveDuration > 0) { + const qreal oldY = it.value() - m_contentY; + startMoveAnimation(m_delegates[newIdx], oldY); + } + } + + emit countChanged(); +} + +void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { + if (parent.isValid()) + return; + + // Start remove animations for visible delegates being removed + for (int i = first; i <= last; ++i) { + if (!m_delegates.contains(i)) + continue; + + auto entry = m_delegates.take(i); + entry.pendingRemoval = true; + + if (m_removeDuration > 0 && entry.item) { + startRemoveAnimation(entry); + m_dyingDelegates.append(std::move(entry)); + } else { + destroyDelegate(entry); + } + } +} + +void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) { + if (parent.isValid()) + return; + + const int removeCount = last - first + 1; + + // Capture old positions for displaced animation + QHash oldPositions; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + if (it.key() > last) + oldPositions.insert(it.key(), m_layout[it.key()].targetY); + } + + // Untrack known heights being removed + for (int i = first; i <= last; ++i) { + if (m_layout[i].heightKnown) + untrackHeight(m_layout[i].height); + } + + // Remove layout records + m_layout.remove(first, removeCount); + + // Shift remaining delegate indices down + QHash shifted; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + int newIdx = it.key() > last ? it.key() - removeCount : it.key(); + auto entry = std::move(it.value()); + entry.modelIndex = newIdx; + if (entry.context) + entry.context->setContextProperty(QStringLiteral("index"), newIdx); + shifted.insert(newIdx, std::move(entry)); + } + m_delegates = std::move(shifted); + + relayout(); + syncDelegates(); + positionDelegates(); + + // Animate displaced items + for (auto it = oldPositions.begin(); it != oldPositions.end(); ++it) { + const int newIdx = it.key() - removeCount; + if (m_delegates.contains(newIdx) && m_moveDuration > 0) { + const qreal oldY = it.value() - m_contentY; + startMoveAnimation(m_delegates[newIdx], oldY); + } + } + + emit countChanged(); +} + +void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { + Q_UNUSED(parent) + Q_UNUSED(start) + Q_UNUSED(end) + Q_UNUSED(destination) + Q_UNUSED(row) + + // Full reset for moves — complex index remapping + onModelReset(); +} + +void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) { + Q_UNUSED(roles) + + if (topLeft.parent().isValid()) + return; + + for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { + if (m_delegates.contains(i)) + updateDelegateData(m_delegates[i]); + } +} + +void LazyListView::onModelReset() { + resetContent(); +} + +// --- Animation --- + +void LazyListView::startAddAnimation(DelegateEntry& entry) { + if (!entry.item || m_addDuration <= 0) + return; + + stopAnimation(entry); + + auto* group = new QParallelAnimationGroup(this); + + if (!qFuzzyCompare(m_addFromOpacity, 1.0)) { + auto* opacityAnim = new QPropertyAnimation(entry.item, "opacity"); + opacityAnim->setDuration(m_addDuration); + opacityAnim->setEasingCurve(m_addCurve); + opacityAnim->setStartValue(m_addFromOpacity); + opacityAnim->setEndValue(1.0); + group->addAnimation(opacityAnim); + entry.item->setOpacity(m_addFromOpacity); + } + + if (!qFuzzyCompare(m_addFromScale, 1.0)) { + auto* scaleAnim = new QPropertyAnimation(entry.item, "scale"); + scaleAnim->setDuration(m_addDuration); + scaleAnim->setEasingCurve(m_addCurve); + scaleAnim->setStartValue(m_addFromScale); + scaleAnim->setEndValue(1.0); + group->addAnimation(scaleAnim); + entry.item->setScale(m_addFromScale); + } + + if (group->animationCount() == 0) { + delete group; + return; + } + + entry.animation = group; + ++m_activeAnimations; + if (m_activeAnimations == 1) + emit settledChanged(); + + connect(group, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); + group->start(QAbstractAnimation::DeleteWhenStopped); +} + +void LazyListView::startRemoveAnimation(DelegateEntry& entry) { + if (!entry.item || m_removeDuration <= 0) + return; + + stopAnimation(entry); + + auto* group = new QParallelAnimationGroup(this); + + if (!qFuzzyCompare(m_removeToOpacity, 1.0)) { + auto* opacityAnim = new QPropertyAnimation(entry.item, "opacity"); + opacityAnim->setDuration(m_removeDuration); + opacityAnim->setEasingCurve(m_removeCurve); + opacityAnim->setStartValue(entry.item->opacity()); + opacityAnim->setEndValue(m_removeToOpacity); + group->addAnimation(opacityAnim); + } + + if (!qFuzzyCompare(m_removeToScale, 1.0)) { + auto* scaleAnim = new QPropertyAnimation(entry.item, "scale"); + scaleAnim->setDuration(m_removeDuration); + scaleAnim->setEasingCurve(m_removeCurve); + scaleAnim->setStartValue(entry.item->scale()); + scaleAnim->setEndValue(m_removeToScale); + group->addAnimation(scaleAnim); + } + + if (group->animationCount() == 0) { + delete group; + return; + } + + entry.animation = group; + ++m_activeAnimations; + if (m_activeAnimations == 1) + emit settledChanged(); + + connect(group, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); + group->start(QAbstractAnimation::DeleteWhenStopped); +} + +void LazyListView::startMoveAnimation(DelegateEntry& entry, qreal fromY) { + if (!entry.item || m_moveDuration <= 0) + return; + + const int idx = entry.modelIndex; + if (idx < 0 || idx >= static_cast(m_layout.size())) + return; + + const qreal toY = m_layout[idx].targetY - m_contentY; + if (qFuzzyCompare(fromY, toY)) + return; + + stopAnimation(entry); + + auto* group = new QParallelAnimationGroup(this); + + auto* yAnim = new QPropertyAnimation(entry.item, "y"); + yAnim->setDuration(m_moveDuration); + yAnim->setEasingCurve(m_moveCurve); + yAnim->setStartValue(fromY); + yAnim->setEndValue(toY); + group->addAnimation(yAnim); + + entry.item->setY(fromY); + entry.animation = group; + ++m_activeAnimations; + if (m_activeAnimations == 1) + emit settledChanged(); + + connect(group, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); + group->start(QAbstractAnimation::DeleteWhenStopped); +} + +void LazyListView::stopAnimation(DelegateEntry& entry) { + if (!entry.animation) + return; + + entry.animation->stop(); + entry.animation = nullptr; + + --m_activeAnimations; + if (m_activeAnimations == 0) + emit settledChanged(); +} + +void LazyListView::onAnimationFinished() { + auto* group = qobject_cast(sender()); + + // Clear animation pointer from live delegates + for (auto& entry : m_delegates) { + if (entry.animation == group) + entry.animation = nullptr; + } + + // Clean up dying delegates whose animation finished + m_dyingDelegates.erase(std::remove_if(m_dyingDelegates.begin(), m_dyingDelegates.end(), + [this, group](DelegateEntry& entry) { + if (entry.animation == group) { + entry.animation = nullptr; + destroyDelegate(entry); + return true; + } + return false; + }), + m_dyingDelegates.end()); + + --m_activeAnimations; + if (m_activeAnimations == 0) + emit settledChanged(); + + // Re-sync in case viewport changed during animation + polish(); +} + +} // namespace caelestia::components diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp new file mode 100644 index 000000000..a310f96f1 --- /dev/null +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -0,0 +1,244 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace caelestia::components { + +class LazyListView : public QQuickItem { + Q_OBJECT + QML_ELEMENT + + // Model & Delegate + Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + // Layout + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged) + Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) + + // Viewport & Lazy Loading + Q_PROPERTY(QRectF viewport READ viewport WRITE setViewport NOTIFY viewportChanged) + Q_PROPERTY(bool useCustomViewport READ useCustomViewport WRITE setUseCustomViewport NOTIFY useCustomViewportChanged) + Q_PROPERTY(qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged) + + // Sizing + Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged) + + // Add Animation + Q_PROPERTY(int addDuration READ addDuration WRITE setAddDuration NOTIFY addDurationChanged) + Q_PROPERTY(QEasingCurve addCurve READ addCurve WRITE setAddCurve NOTIFY addCurveChanged) + Q_PROPERTY(qreal addFromOpacity READ addFromOpacity WRITE setAddFromOpacity NOTIFY addFromOpacityChanged) + Q_PROPERTY(qreal addFromScale READ addFromScale WRITE setAddFromScale NOTIFY addFromScaleChanged) + + // Remove Animation + Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged) + Q_PROPERTY(QEasingCurve removeCurve READ removeCurve WRITE setRemoveCurve NOTIFY removeCurveChanged) + Q_PROPERTY(qreal removeToOpacity READ removeToOpacity WRITE setRemoveToOpacity NOTIFY removeToOpacityChanged) + Q_PROPERTY(qreal removeToScale READ removeToScale WRITE setRemoveToScale NOTIFY removeToScaleChanged) + + // Move/Displaced Animation + Q_PROPERTY(int moveDuration READ moveDuration WRITE setMoveDuration NOTIFY moveDurationChanged) + Q_PROPERTY(QEasingCurve moveCurve READ moveCurve WRITE setMoveCurve NOTIFY moveCurveChanged) + + // State + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(bool settled READ settled NOTIFY settledChanged) + +public: + explicit LazyListView(QQuickItem* parent = nullptr); + ~LazyListView() override; + + // Model & Delegate + [[nodiscard]] QAbstractItemModel* model() const; + void setModel(QAbstractItemModel* model); + + [[nodiscard]] QQmlComponent* delegate() const; + void setDelegate(QQmlComponent* delegate); + + // Layout + [[nodiscard]] qreal spacing() const; + void setSpacing(qreal spacing); + + [[nodiscard]] qreal contentHeight() const; + + [[nodiscard]] qreal contentY() const; + void setContentY(qreal contentY); + + // Viewport + [[nodiscard]] QRectF viewport() const; + void setViewport(const QRectF& viewport); + + [[nodiscard]] bool useCustomViewport() const; + void setUseCustomViewport(bool use); + + [[nodiscard]] qreal cacheBuffer() const; + void setCacheBuffer(qreal buffer); + + // Sizing + [[nodiscard]] qreal estimatedHeight() const; + void setEstimatedHeight(qreal height); + + // Add Animation + [[nodiscard]] int addDuration() const; + void setAddDuration(int duration); + + [[nodiscard]] QEasingCurve addCurve() const; + void setAddCurve(const QEasingCurve& curve); + + [[nodiscard]] qreal addFromOpacity() const; + void setAddFromOpacity(qreal opacity); + + [[nodiscard]] qreal addFromScale() const; + void setAddFromScale(qreal scale); + + // Remove Animation + [[nodiscard]] int removeDuration() const; + void setRemoveDuration(int duration); + + [[nodiscard]] QEasingCurve removeCurve() const; + void setRemoveCurve(const QEasingCurve& curve); + + [[nodiscard]] qreal removeToOpacity() const; + void setRemoveToOpacity(qreal opacity); + + [[nodiscard]] qreal removeToScale() const; + void setRemoveToScale(qreal scale); + + // Move Animation + [[nodiscard]] int moveDuration() const; + void setMoveDuration(int duration); + + [[nodiscard]] QEasingCurve moveCurve() const; + void setMoveCurve(const QEasingCurve& curve); + + // State + [[nodiscard]] int count() const; + [[nodiscard]] bool settled() const; + +signals: + void modelChanged(); + void delegateChanged(); + void spacingChanged(); + void contentHeightChanged(); + void contentYChanged(); + void viewportChanged(); + void useCustomViewportChanged(); + void cacheBufferChanged(); + void estimatedHeightChanged(); + void addDurationChanged(); + void addCurveChanged(); + void addFromOpacityChanged(); + void addFromScaleChanged(); + void removeDurationChanged(); + void removeCurveChanged(); + void removeToOpacityChanged(); + void removeToScaleChanged(); + void moveDurationChanged(); + void moveCurveChanged(); + void countChanged(); + void settledChanged(); + +protected: + void componentComplete() override; + void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; + void updatePolish() override; + +private: + struct ItemRecord { + qreal targetY = 0; + qreal height = 0; + bool heightKnown = false; + }; + + struct DelegateEntry { + int modelIndex = -1; + QQuickItem* item = nullptr; + QQmlContext* context = nullptr; + bool pendingRemoval = false; + QParallelAnimationGroup* animation = nullptr; + }; + + // Layout + void relayout(); + [[nodiscard]] std::pair computeVisibleRange() const; + [[nodiscard]] QRectF effectiveViewport() const; + [[nodiscard]] qreal effectiveEstimatedHeight() const; + void trackHeight(qreal height); + void untrackHeight(qreal height); + + // Delegate lifecycle + void syncDelegates(); + DelegateEntry createDelegate(int modelIndex); + void destroyDelegate(DelegateEntry& entry); + void updateDelegateData(DelegateEntry& entry); + void positionDelegates(); + + // Model connection + void connectModel(); + void disconnectModel(); + void resetContent(); + void onRowsInserted(const QModelIndex& parent, int first, int last); + void onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); + void onRowsRemoved(const QModelIndex& parent, int first, int last); + void onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row); + void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles); + void onModelReset(); + + // Animation + void startAddAnimation(DelegateEntry& entry); + void startRemoveAnimation(DelegateEntry& entry); + void startMoveAnimation(DelegateEntry& entry, qreal fromY); + void stopAnimation(DelegateEntry& entry); + void onAnimationFinished(); + + // Members + QAbstractItemModel* m_model = nullptr; + QQmlComponent* m_delegate = nullptr; + + qreal m_spacing = 0; + qreal m_contentHeight = 0; + qreal m_contentY = 0; + + QRectF m_viewport; + bool m_useCustomViewport = false; + qreal m_cacheBuffer = 0; + + qreal m_estimatedHeight = -1; + qreal m_knownHeightSum = 0; + int m_knownHeightCount = 0; + + int m_addDuration = 300; + QEasingCurve m_addCurve; + qreal m_addFromOpacity = 0; + qreal m_addFromScale = 1; + + int m_removeDuration = 300; + QEasingCurve m_removeCurve; + qreal m_removeToOpacity = 0; + qreal m_removeToScale = 1; + + int m_moveDuration = 300; + QEasingCurve m_moveCurve; + + QVector m_layout; + QHash m_delegates; + QVector m_dyingDelegates; + + int m_activeAnimations = 0; + bool m_componentComplete = false; + + QList m_modelConnections; +}; + +} // namespace caelestia::components From 9e35e93f76ab0510c5f136b06521c150d672e674 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:27:53 +1100 Subject: [PATCH 210/409] fix: model change sigabrt crash + resize anim --- modules/sidebar/NotifDockList.qml | 10 +- .../src/Caelestia/Components/lazylistview.cpp | 183 +++++++++++------- .../src/Caelestia/Components/lazylistview.hpp | 26 ++- 3 files changed, 137 insertions(+), 82 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index c7d1ebaf5..f2dea1c2f 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -72,7 +72,8 @@ LazyListView { } } - implicitHeight: closed ? 0 : notifInner.implicitHeight + LazyListView.preferredHeight: closed ? 0 : notifInner.implicitHeight + implicitHeight: notifInner.implicitHeight hoverEnabled: true cursorShape: pressed ? Qt.ClosedHandCursor : undefined @@ -119,13 +120,6 @@ LazyListView { easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } - - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } } } } diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 126c12025..ea059a101 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -5,12 +5,34 @@ namespace caelestia::components { +// --- LazyListViewAttached --- + +LazyListViewAttached::LazyListViewAttached(QObject* parent) + : QObject(parent) {} + +qreal LazyListViewAttached::preferredHeight() const { + return m_preferredHeight; +} + +void LazyListViewAttached::setPreferredHeight(qreal height) { + if (qFuzzyCompare(m_preferredHeight, height)) + return; + m_preferredHeight = height; + emit preferredHeightChanged(); +} + +// --- LazyListView --- + LazyListView::LazyListView(QQuickItem* parent) : QQuickItem(parent) { setFlag(ItemHasContents, false); setClip(true); } +LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) { + return new LazyListViewAttached(object); +} + LazyListView::~LazyListView() { for (auto& entry : m_delegates) destroyDelegate(entry); @@ -154,6 +176,18 @@ void LazyListView::untrackHeight(qreal height) { --m_knownHeightCount; } +qreal LazyListView::delegateHeight(QQuickItem* item) { + if (!item) + return 0; + + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(item, false)); + if (attached && attached->preferredHeight() >= 0) + return attached->preferredHeight(); + + return item->implicitHeight(); +} + // --- Add Animation --- int LazyListView::addDuration() const { @@ -310,7 +344,37 @@ void LazyListView::updatePolish() { relayout(); syncDelegates(); - positionDelegates(); + + // Animate newly created delegates that were pending add animation + QSet pendingAdds; + m_pendingAddAnimations.swap(pendingAdds); + for (int idx : std::as_const(pendingAdds)) { + if (m_delegates.contains(idx) && m_addDuration > 0) + startAddAnimation(m_delegates[idx]); + } + + // Position delegates, animating displacement if a model change occurred + const bool animate = m_animateDisplacement; + m_animateDisplacement = false; + + for (auto& entry : m_delegates) { + if (!entry.item || entry.pendingRemoval || entry.animation) + continue; + + const int idx = entry.modelIndex; + if (idx < 0 || idx >= static_cast(m_layout.size())) + continue; + + const qreal targetY = m_layout[idx].targetY - m_contentY; + const qreal currentY = entry.item->y(); + + if (animate && !qFuzzyCompare(currentY, targetY) && m_moveDuration > 0 + && !pendingAdds.contains(idx)) { + startMoveAnimation(entry, currentY); + } else if (!entry.animation) { + entry.item->setY(targetY); + } + } } // --- Layout Engine --- @@ -408,8 +472,8 @@ void LazyListView::syncDelegates() { auto entry = createDelegate(i); if (entry.item) { - // Measure height - const qreal h = entry.item->implicitHeight(); + // Measure height (prefer attached preferredHeight, fall back to implicitHeight) + const qreal h = delegateHeight(entry.item); if (h > 0 && !m_layout[i].heightKnown) { m_layout[i].height = h; m_layout[i].heightKnown = true; @@ -479,14 +543,14 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.item->setWidth(width()); m_delegate->completeCreate(); - // Watch for height changes - connect(entry.item, &QQuickItem::implicitHeightChanged, this, [this, modelIndex] { + // Shared height-change handler + auto onHeightChanged = [this, modelIndex] { if (!m_delegates.contains(modelIndex)) return; auto& e = m_delegates[modelIndex]; if (!e.item) return; - const qreal h = e.item->implicitHeight(); + const qreal h = delegateHeight(e.item); if (modelIndex < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[modelIndex].height, h)) { const qreal oldH = m_layout[modelIndex].height; const bool wasKnown = m_layout[modelIndex].heightKnown; @@ -497,20 +561,45 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { trackHeight(h); polish(); } - }); + }; + + // Watch implicitHeight as fallback + connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); + + // Watch attached preferredHeight if the delegate uses it + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(entry.item, false)); + if (attached) { + entry.attachedConnection = connect(attached, &LazyListViewAttached::preferredHeightChanged, + this, onHeightChanged); + } return entry; } void LazyListView::destroyDelegate(DelegateEntry& entry) { if (entry.animation) { + // Disconnect before stopping to prevent re-entrant onAnimationFinished + disconnect(entry.animation, &QAbstractAnimation::finished, + this, &LazyListView::onAnimationFinished); entry.animation->stop(); entry.animation = nullptr; + --m_activeAnimations; + if (m_activeAnimations == 0) + emit settledChanged(); + } + if (entry.attachedConnection) + disconnect(entry.attachedConnection); + if (entry.item) { + entry.item->setParentItem(nullptr); + entry.item->setVisible(false); + entry.item->deleteLater(); + entry.item = nullptr; + } + if (entry.context) { + entry.context->deleteLater(); + entry.context = nullptr; } - delete entry.item; - entry.item = nullptr; - delete entry.context; - entry.context = nullptr; } void LazyListView::updateDelegateData(DelegateEntry& entry) { @@ -547,23 +636,6 @@ void LazyListView::updateDelegateData(DelegateEntry& entry) { } } -void LazyListView::positionDelegates() { - for (auto& entry : m_delegates) { - if (!entry.item || entry.pendingRemoval) - continue; - - // Don't reposition if a move animation is running on this delegate - if (entry.animation) - continue; - - const int idx = entry.modelIndex; - if (idx < 0 || idx >= static_cast(m_layout.size())) - continue; - - entry.item->setY(m_layout[idx].targetY - m_contentY); - } -} - // --- Model Connection --- void LazyListView::connectModel() { @@ -608,9 +680,11 @@ void LazyListView::resetContent() { emit settledChanged(); } - // Reset height tracking + // Reset pending state m_knownHeightSum = 0; m_knownHeightCount = 0; + m_pendingAddAnimations.clear(); + m_animateDisplacement = false; // Rebuild layout from model m_layout.clear(); @@ -633,13 +707,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last const int insertCount = last - first + 1; - // Capture old positions of existing delegates for move animation - QHash oldPositions; - for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (it.key() >= first) - oldPositions.insert(it.key(), m_layout[it.key()].targetY); - } - // Insert new layout records m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false }); @@ -655,26 +722,13 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last } m_delegates = std::move(shifted); - relayout(); - syncDelegates(); - positionDelegates(); - - // Animate new items - for (int i = first; i <= last; ++i) { - if (m_delegates.contains(i) && m_addDuration > 0) - startAddAnimation(m_delegates[i]); - } - - // Animate displaced items - for (auto it = oldPositions.begin(); it != oldPositions.end(); ++it) { - const int newIdx = it.key() + insertCount; - if (m_delegates.contains(newIdx) && m_moveDuration > 0) { - const qreal oldY = it.value() - m_contentY; - startMoveAnimation(m_delegates[newIdx], oldY); - } - } + // Queue add animations and mark displacement + for (int i = first; i <= last; ++i) + m_pendingAddAnimations.insert(i); + m_animateDisplacement = true; emit countChanged(); + polish(); } void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { @@ -704,13 +758,6 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) const int removeCount = last - first + 1; - // Capture old positions for displaced animation - QHash oldPositions; - for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (it.key() > last) - oldPositions.insert(it.key(), m_layout[it.key()].targetY); - } - // Untrack known heights being removed for (int i = first; i <= last; ++i) { if (m_layout[i].heightKnown) @@ -732,20 +779,10 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) } m_delegates = std::move(shifted); - relayout(); - syncDelegates(); - positionDelegates(); - - // Animate displaced items - for (auto it = oldPositions.begin(); it != oldPositions.end(); ++it) { - const int newIdx = it.key() - removeCount; - if (m_delegates.contains(newIdx) && m_moveDuration > 0) { - const qreal oldY = it.value() - m_contentY; - startMoveAnimation(m_delegates[newIdx], oldY); - } - } + m_animateDisplacement = true; emit countChanged(); + polish(); } void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index a310f96f1..fdc0907f8 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -14,9 +14,28 @@ namespace caelestia::components { +class LazyListViewAttached : public QObject { + Q_OBJECT + + Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) + +public: + explicit LazyListViewAttached(QObject* parent = nullptr); + + [[nodiscard]] qreal preferredHeight() const; + void setPreferredHeight(qreal height); + +signals: + void preferredHeightChanged(); + +private: + qreal m_preferredHeight = -1; +}; + class LazyListView : public QQuickItem { Q_OBJECT QML_ELEMENT + QML_ATTACHED(LazyListViewAttached) // Model & Delegate Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged) @@ -59,6 +78,8 @@ class LazyListView : public QQuickItem { explicit LazyListView(QQuickItem* parent = nullptr); ~LazyListView() override; + static LazyListViewAttached* qmlAttachedProperties(QObject* object); + // Model & Delegate [[nodiscard]] QAbstractItemModel* model() const; void setModel(QAbstractItemModel* model); @@ -167,6 +188,7 @@ class LazyListView : public QQuickItem { QQmlContext* context = nullptr; bool pendingRemoval = false; QParallelAnimationGroup* animation = nullptr; + QMetaObject::Connection attachedConnection; }; // Layout @@ -174,6 +196,7 @@ class LazyListView : public QQuickItem { [[nodiscard]] std::pair computeVisibleRange() const; [[nodiscard]] QRectF effectiveViewport() const; [[nodiscard]] qreal effectiveEstimatedHeight() const; + [[nodiscard]] static qreal delegateHeight(QQuickItem* item); void trackHeight(qreal height); void untrackHeight(qreal height); @@ -182,7 +205,6 @@ class LazyListView : public QQuickItem { DelegateEntry createDelegate(int modelIndex); void destroyDelegate(DelegateEntry& entry); void updateDelegateData(DelegateEntry& entry); - void positionDelegates(); // Model connection void connectModel(); @@ -237,6 +259,8 @@ class LazyListView : public QQuickItem { int m_activeAnimations = 0; bool m_componentComplete = false; + bool m_animateDisplacement = false; + QSet m_pendingAddAnimations; QList m_modelConnections; }; From dfc5bcf8b9511fde1fee7de15cdfa79c5f5aeeb6 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Thu, 2 Apr 2026 17:43:22 +0200 Subject: [PATCH 211/409] fix: preserve VPN interface and command fields (#1373) --- config/Config.qml | 2 +- modules/controlcenter/network/VpnDetails.qml | 35 +++++++++---- modules/controlcenter/network/VpnList.qml | 27 +++++++++- modules/controlcenter/network/VpnSettings.qml | 50 +++++++++++++++---- 4 files changed, 94 insertions(+), 20 deletions(-) diff --git a/config/Config.qml b/config/Config.qml index d0b24935f..997eaf1ee 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -326,7 +326,7 @@ Singleton { const provider = { displayName: p.displayName, enabled: p.enabled, - interface: p.iface, + interface: p.interface, name: p.name }; if (p.connectCmd && p.connectCmd.length > 0) { diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 4d749d8b9..b02f4c497 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -72,6 +72,13 @@ DeviceDetails { newProvider.enabled = (i === index) ? false : (p.enabled !== false); } + if (p.connectCmd && p.connectCmd.length > 0) { + newProvider.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + newProvider.disconnectCmd = p.disconnectCmd; + } + providers.push(newProvider); } else { providers.push(p); @@ -504,21 +511,31 @@ DeviceDetails { const newProvider = { displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, enabled: wasEnabled, - iface: editVpnDialog.interfaceName, - name: editVpnDialog.providerName, - connectCmd: hasCommands ? editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0) : undefined, - disconnectCmd: hasCommands ? editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0) : undefined + interface: editVpnDialog.interfaceName, + name: editVpnDialog.providerName }; - // Remove undefined properties - if (!hasCommands) { - delete newProvider.connectCmd; - delete newProvider.disconnectCmd; + if (hasCommands) { + newProvider.connectCmd = editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0); + newProvider.disconnectCmd = editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); } providers.push(newProvider); } else { - providers.push(Config.utilities.vpn.provider[i]); + const p = Config.utilities.vpn.provider[i]; + const reconstructed = { + displayName: p.displayName, + enabled: p.enabled, + interface: p.interface, + name: p.name + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); } } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 6dabc2cfc..8522f9729 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -36,6 +36,12 @@ ColumnLayout { interface: p.interface, enabled: (i === targetIndex) }; + if (p.connectCmd && p.connectCmd.length > 0) { + newProvider.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + newProvider.disconnectCmd = p.disconnectCmd; + } providers.push(newProvider); } else { providers.push(p); @@ -226,6 +232,12 @@ ColumnLayout { interface: p.interface, enabled: (i === clickedIndex) }; + if (p.connectCmd && p.connectCmd.length > 0) { + newProvider.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + newProvider.disconnectCmd = p.disconnectCmd; + } providers.push(newProvider); } else { providers.push(p); @@ -265,7 +277,20 @@ ColumnLayout { const providers = []; for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { if (i !== modelData.index) { - providers.push(Config.utilities.vpn.provider[i]); + const p = Config.utilities.vpn.provider[i]; + const reconstructed = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: p.enabled + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); } } Config.utilities.vpn.provider = providers; diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index ae689050e..80732fdeb 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -119,30 +119,62 @@ ColumnLayout { icon: modelData.isActive ? "arrow_downward" : "arrow_upward" visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1 onClicked: { - if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) { + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + const p = Config.utilities.vpn.provider[i]; + const reconstructed = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: p.enabled + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); + } + + if (modelData.isActive && index < providers.length - 1) { // Move down - const providers = [...Config.utilities.vpn.provider]; const temp = providers[index]; providers[index] = providers[index + 1]; providers[index + 1] = temp; - Config.utilities.vpn.provider = providers; - Config.save(); } else if (!modelData.isActive) { // Make active (move to top) - const providers = [...Config.utilities.vpn.provider]; const provider = providers.splice(index, 1)[0]; providers.unshift(provider); - Config.utilities.vpn.provider = providers; - Config.save(); } + + Config.utilities.vpn.provider = providers; + Config.save(); } } IconButton { icon: "delete" onClicked: { - const providers = [...Config.utilities.vpn.provider]; - providers.splice(index, 1); + const providers = []; + for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + if (i !== index) { + const p = Config.utilities.vpn.provider[i]; + const reconstructed = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: p.enabled + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); + } + } Config.utilities.vpn.provider = providers; Config.save(); } From 2acd8f13f8d8be759e7eb559e91cdf04f28971c2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:55:04 +1100 Subject: [PATCH 212/409] fix: don't recreate all delegates on model change --- .../src/Caelestia/Components/lazylistview.cpp | 142 +++++++++++++++--- .../src/Caelestia/Components/lazylistview.hpp | 1 + 2 files changed, 122 insertions(+), 21 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index ea059a101..d1e2570ce 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -375,6 +375,18 @@ void LazyListView::updatePolish() { entry.item->setY(targetY); } } + + // Flush delegate pool — any delegates not reclaimed are truly removed + for (auto& pooled : m_delegatePool) { + pooled.pendingRemoval = true; + if (m_removeDuration > 0 && pooled.item) { + startRemoveAnimation(pooled); + m_dyingDelegates.append(std::move(pooled)); + } else { + destroyDelegate(pooled); + } + } + m_delegatePool.clear(); } // --- Layout Engine --- @@ -492,6 +504,27 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (!m_delegate || !m_model) return entry; + // Try to reclaim a delegate from the pool (reuse after remove+insert cycle) + const auto roleNames = m_model->roleNames(); + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + const auto targetData = m_model->data(m_model->index(modelIndex, 0), role); + + for (auto it = m_delegatePool.begin(); it != m_delegatePool.end(); ++it) { + if (!it->item) + continue; + const auto poolData = it->item->property("modelData"); + if (poolData == targetData) { + entry = std::move(*it); + m_delegatePool.erase(it); + entry.modelIndex = modelIndex; + entry.pendingRemoval = false; + updateDelegateData(entry); + entry.item->setParentItem(this); + entry.item->setWidth(width()); + return entry; + } + } + // Use the delegate component's creation context so the delegate // can access ids and properties from the scope where it was defined. auto* compContext = m_delegate->creationContext(); @@ -502,7 +535,6 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.context = new QQmlContext(parentContext, this); // Build property map for both context properties and initial properties - const auto roleNames = m_model->roleNames(); const auto index = m_model->index(modelIndex, 0); QVariantMap initialProps; @@ -520,7 +552,6 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { // Provide modelData for single-role models or if not already provided by role names if (!hasModelData) { - const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); const auto value = m_model->data(index, role); entry.context->setContextProperty(QStringLiteral("modelData"), value); initialProps.insert(QStringLiteral("modelData"), value); @@ -649,7 +680,11 @@ void LazyListView::connectModel() { connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), - connect(m_model, &QAbstractItemModel::layoutChanged, this, &LazyListView::onModelReset), + connect(m_model, &QAbstractItemModel::layoutChanged, this, [this] { + for (auto& entry : m_delegates) + updateDelegateData(entry); + polish(); + }), connect(m_model, &QObject::destroyed, this, [this] { m_model = nullptr; @@ -675,6 +710,10 @@ void LazyListView::resetContent() { destroyDelegate(entry); m_dyingDelegates.clear(); + for (auto& entry : m_delegatePool) + destroyDelegate(entry); + m_delegatePool.clear(); + if (m_activeAnimations != 0) { m_activeAnimations = 0; emit settledChanged(); @@ -706,7 +745,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last return; const int insertCount = last - first + 1; - // Insert new layout records m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false }); @@ -735,20 +773,14 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, if (parent.isValid()) return; - // Start remove animations for visible delegates being removed + // Pool removed delegates — they may be reused if the model re-inserts the same data for (int i = first; i <= last; ++i) { if (!m_delegates.contains(i)) continue; auto entry = m_delegates.take(i); - entry.pendingRemoval = true; - - if (m_removeDuration > 0 && entry.item) { - startRemoveAnimation(entry); - m_dyingDelegates.append(std::move(entry)); - } else { - destroyDelegate(entry); - } + stopAnimation(entry); + m_delegatePool.append(std::move(entry)); } } @@ -785,15 +817,48 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) polish(); } -void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { - Q_UNUSED(parent) - Q_UNUSED(start) - Q_UNUSED(end) - Q_UNUSED(destination) - Q_UNUSED(row) +void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, + const QModelIndex& destination, int row) { + if (parent.isValid() || destination.isValid()) + return; + + const int count = end - start + 1; + const int dest = row > start ? row - count : row; + + // Reorder layout records + QVector moved; + moved.reserve(count); + for (int i = start; i <= end; ++i) + moved.append(m_layout[i]); + m_layout.remove(start, count); + for (int i = 0; i < count; ++i) + m_layout.insert(dest + i, moved[i]); - // Full reset for moves — complex index remapping - onModelReset(); + // Remap delegate indices to match new model order + QHash remapped; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + int oldIdx = it.key(); + int newIdx = oldIdx; + + if (oldIdx >= start && oldIdx <= end) { + newIdx = dest + (oldIdx - start); + } else { + if (oldIdx > end) + newIdx -= count; + if (newIdx >= dest) + newIdx += count; + } + + auto entry = std::move(it.value()); + entry.modelIndex = newIdx; + if (entry.context) + entry.context->setContextProperty(QStringLiteral("index"), newIdx); + remapped.insert(newIdx, std::move(entry)); + } + m_delegates = std::move(remapped); + + m_animateDisplacement = true; + polish(); } void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) { @@ -809,6 +874,41 @@ void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& } void LazyListView::onModelReset() { + if (!m_model) { + resetContent(); + return; + } + + const int newRows = m_model->rowCount(); + const int oldRows = static_cast(m_layout.size()); + + // Check if the model data actually changed + if (newRows == oldRows) { + const auto roleNames = m_model->roleNames(); + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + bool changed = false; + + for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) { + if (!it->item || it.key() >= newRows) { + changed = true; + break; + } + const auto newData = m_model->data(m_model->index(it.key(), 0), role); + const auto oldData = it->item->property("modelData"); + if (newData != oldData) { + changed = true; + break; + } + } + + if (!changed) { + // Model content unchanged, just refresh delegate data + for (auto& entry : m_delegates) + updateDelegateData(entry); + return; + } + } + resetContent(); } diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index fdc0907f8..c47705529 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -256,6 +256,7 @@ class LazyListView : public QQuickItem { QVector m_layout; QHash m_delegates; QVector m_dyingDelegates; + QVector m_delegatePool; int m_activeAnimations = 0; bool m_componentComplete = false; From 0252be370ea7d2c6437995af36521bc5c32f73d9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 02:57:15 +1100 Subject: [PATCH 213/409] fix: delegate size not being tracked after rearrange --- .../src/Caelestia/Components/lazylistview.cpp | 35 ++++++++++--------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index d1e2570ce..d4baab49f 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -574,23 +574,26 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.item->setWidth(width()); m_delegate->completeCreate(); - // Shared height-change handler - auto onHeightChanged = [this, modelIndex] { - if (!m_delegates.contains(modelIndex)) - return; - auto& e = m_delegates[modelIndex]; - if (!e.item) + // Shared height-change handler — captures item pointer instead of index + // so it remains valid after model inserts/removes/moves shift indices. + auto onHeightChanged = [this, item = entry.item] { + // Find the delegate entry by item pointer + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + if (it->item != item) + continue; + const int idx = it.key(); + const qreal h = delegateHeight(item); + if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height, h)) { + const qreal oldH = m_layout[idx].height; + const bool wasKnown = m_layout[idx].heightKnown; + m_layout[idx].height = h; + m_layout[idx].heightKnown = true; + if (wasKnown) + untrackHeight(oldH); + trackHeight(h); + polish(); + } return; - const qreal h = delegateHeight(e.item); - if (modelIndex < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[modelIndex].height, h)) { - const qreal oldH = m_layout[modelIndex].height; - const bool wasKnown = m_layout[modelIndex].heightKnown; - m_layout[modelIndex].height = h; - m_layout[modelIndex].heightKnown = true; - if (wasKnown) - untrackHeight(oldH); - trackHeight(h); - polish(); } }; From 051b8caee6d742cd5602120a9e6b3c13da9b5fd4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:51:33 +1100 Subject: [PATCH 214/409] fix: move anim logic to qml Fixes closing in quick succession not updating movement correctly --- modules/sidebar/NotifDockList.qml | 23 ++- .../src/Caelestia/Components/lazylistview.cpp | 162 ++++++++---------- .../src/Caelestia/Components/lazylistview.hpp | 15 +- 3 files changed, 101 insertions(+), 99 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index f2dea1c2f..8517870ba 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -72,9 +72,12 @@ LazyListView { } } - LazyListView.preferredHeight: closed ? 0 : notifInner.implicitHeight + LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight implicitHeight: notifInner.implicitHeight + opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1 + scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1 + hoverEnabled: true cursorShape: pressed ? Qt.ClosedHandCursor : undefined acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton @@ -114,6 +117,24 @@ LazyListView { visibilities: root.visibilities } + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + Behavior on x { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index d4baab49f..c92f6963f 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -2,6 +2,7 @@ #include #include +#include namespace caelestia::components { @@ -21,12 +22,33 @@ void LazyListViewAttached::setPreferredHeight(qreal height) { emit preferredHeightChanged(); } +bool LazyListViewAttached::adding() const { + return m_adding; +} + +void LazyListViewAttached::setAdding(bool adding) { + if (m_adding == adding) + return; + m_adding = adding; + emit addingChanged(); +} + +bool LazyListViewAttached::removing() const { + return m_removing; +} + +void LazyListViewAttached::setRemoving(bool removing) { + if (m_removing == removing) + return; + m_removing = removing; + emit removingChanged(); +} + // --- LazyListView --- LazyListView::LazyListView(QQuickItem* parent) : QQuickItem(parent) { setFlag(ItemHasContents, false); - setClip(true); } LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) { @@ -345,48 +367,22 @@ void LazyListView::updatePolish() { relayout(); syncDelegates(); - // Animate newly created delegates that were pending add animation - QSet pendingAdds; - m_pendingAddAnimations.swap(pendingAdds); - for (int idx : std::as_const(pendingAdds)) { - if (m_delegates.contains(idx) && m_addDuration > 0) - startAddAnimation(m_delegates[idx]); - } - - // Position delegates, animating displacement if a model change occurred - const bool animate = m_animateDisplacement; - m_animateDisplacement = false; - + // Position delegates — QML Behavior on y handles the animation for (auto& entry : m_delegates) { - if (!entry.item || entry.pendingRemoval || entry.animation) + if (!entry.item || entry.pendingRemoval) continue; const int idx = entry.modelIndex; if (idx < 0 || idx >= static_cast(m_layout.size())) continue; - const qreal targetY = m_layout[idx].targetY - m_contentY; - const qreal currentY = entry.item->y(); + if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height)) + continue; - if (animate && !qFuzzyCompare(currentY, targetY) && m_moveDuration > 0 - && !pendingAdds.contains(idx)) { - startMoveAnimation(entry, currentY); - } else if (!entry.animation) { - entry.item->setY(targetY); - } + // Use setProperty to go through the QML property system, + // which triggers Behaviors (setY bypasses them). + entry.item->setProperty("y", m_layout[idx].targetY - m_contentY); } - - // Flush delegate pool — any delegates not reclaimed are truly removed - for (auto& pooled : m_delegatePool) { - pooled.pendingRemoval = true; - if (m_removeDuration > 0 && pooled.item) { - startRemoveAnimation(pooled); - m_dyingDelegates.append(std::move(pooled)); - } else { - destroyDelegate(pooled); - } - } - m_delegatePool.clear(); } // --- Layout Engine --- @@ -491,6 +487,8 @@ void LazyListView::syncDelegates() { m_layout[i].heightKnown = true; trackHeight(h); } + // Position immediately so it doesn't flash at y=0 + entry.item->setY(m_layout[i].targetY - m_contentY); m_delegates.insert(i, std::move(entry)); } } @@ -504,26 +502,8 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (!m_delegate || !m_model) return entry; - // Try to reclaim a delegate from the pool (reuse after remove+insert cycle) const auto roleNames = m_model->roleNames(); const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); - const auto targetData = m_model->data(m_model->index(modelIndex, 0), role); - - for (auto it = m_delegatePool.begin(); it != m_delegatePool.end(); ++it) { - if (!it->item) - continue; - const auto poolData = it->item->property("modelData"); - if (poolData == targetData) { - entry = std::move(*it); - m_delegatePool.erase(it); - entry.modelIndex = modelIndex; - entry.pendingRemoval = false; - updateDelegateData(entry); - entry.item->setParentItem(this); - entry.item->setWidth(width()); - return entry; - } - } // Use the delegate component's creation context so the delegate // can access ids and properties from the scope where it was defined. @@ -572,8 +552,19 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.item->setParentItem(this); entry.item->setWidth(width()); + + // Set adding = true before completeCreate so bindings see it during initial evaluation. + // Cleared after creation so the transition from true→false triggers QML Behaviors. + auto* addingAttached = qobject_cast( + qmlAttachedPropertiesObject(entry.item, true)); + if (addingAttached) + addingAttached->setAdding(true); + m_delegate->completeCreate(); + if (addingAttached) + addingAttached->setAdding(false); + // Shared height-change handler — captures item pointer instead of index // so it remains valid after model inserts/removes/moves shift indices. auto onHeightChanged = [this, item = entry.item] { @@ -713,10 +704,6 @@ void LazyListView::resetContent() { destroyDelegate(entry); m_dyingDelegates.clear(); - for (auto& entry : m_delegatePool) - destroyDelegate(entry); - m_delegatePool.clear(); - if (m_activeAnimations != 0) { m_activeAnimations = 0; emit settledChanged(); @@ -726,7 +713,6 @@ void LazyListView::resetContent() { m_knownHeightSum = 0; m_knownHeightCount = 0; m_pendingAddAnimations.clear(); - m_animateDisplacement = false; // Rebuild layout from model m_layout.clear(); @@ -766,7 +752,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last // Queue add animations and mark displacement for (int i = first; i <= last; ++i) m_pendingAddAnimations.insert(i); - m_animateDisplacement = true; emit countChanged(); polish(); @@ -776,14 +761,36 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, if (parent.isValid()) return; - // Pool removed delegates — they may be reused if the model re-inserts the same data for (int i = first; i <= last; ++i) { if (!m_delegates.contains(i)) continue; auto entry = m_delegates.take(i); + entry.pendingRemoval = true; stopAnimation(entry); - m_delegatePool.append(std::move(entry)); + + if (m_removeDuration > 0 && entry.item) { + // Signal the delegate via attached property — QML handles the visual transition + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(entry.item, false)); + if (attached) + attached->setRemoving(true); + + // Schedule destruction after the remove animation duration + auto* item = entry.item; + QTimer::singleShot(m_removeDuration, this, [this, item] { + for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) { + if (it->item == item) { + destroyDelegate(*it); + m_dyingDelegates.erase(it); + return; + } + } + }); + m_dyingDelegates.append(std::move(entry)); + } else { + destroyDelegate(entry); + } } } @@ -814,7 +821,6 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) } m_delegates = std::move(shifted); - m_animateDisplacement = true; emit countChanged(); polish(); @@ -860,7 +866,6 @@ void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, } m_delegates = std::move(remapped); - m_animateDisplacement = true; polish(); } @@ -999,39 +1004,6 @@ void LazyListView::startRemoveAnimation(DelegateEntry& entry) { group->start(QAbstractAnimation::DeleteWhenStopped); } -void LazyListView::startMoveAnimation(DelegateEntry& entry, qreal fromY) { - if (!entry.item || m_moveDuration <= 0) - return; - - const int idx = entry.modelIndex; - if (idx < 0 || idx >= static_cast(m_layout.size())) - return; - - const qreal toY = m_layout[idx].targetY - m_contentY; - if (qFuzzyCompare(fromY, toY)) - return; - - stopAnimation(entry); - - auto* group = new QParallelAnimationGroup(this); - - auto* yAnim = new QPropertyAnimation(entry.item, "y"); - yAnim->setDuration(m_moveDuration); - yAnim->setEasingCurve(m_moveCurve); - yAnim->setStartValue(fromY); - yAnim->setEndValue(toY); - group->addAnimation(yAnim); - - entry.item->setY(fromY); - entry.animation = group; - ++m_activeAnimations; - if (m_activeAnimations == 1) - emit settledChanged(); - - connect(group, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); - group->start(QAbstractAnimation::DeleteWhenStopped); -} - void LazyListView::stopAnimation(DelegateEntry& entry) { if (!entry.animation) return; diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index c47705529..b934a2f40 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -18,6 +18,8 @@ class LazyListViewAttached : public QObject { Q_OBJECT Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) + Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) + Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) public: explicit LazyListViewAttached(QObject* parent = nullptr); @@ -25,11 +27,21 @@ class LazyListViewAttached : public QObject { [[nodiscard]] qreal preferredHeight() const; void setPreferredHeight(qreal height); + [[nodiscard]] bool adding() const; + void setAdding(bool adding); + + [[nodiscard]] bool removing() const; + void setRemoving(bool removing); + signals: void preferredHeightChanged(); + void addingChanged(); + void removingChanged(); private: qreal m_preferredHeight = -1; + bool m_adding = false; + bool m_removing = false; }; class LazyListView : public QQuickItem { @@ -220,7 +232,6 @@ class LazyListView : public QQuickItem { // Animation void startAddAnimation(DelegateEntry& entry); void startRemoveAnimation(DelegateEntry& entry); - void startMoveAnimation(DelegateEntry& entry, qreal fromY); void stopAnimation(DelegateEntry& entry); void onAnimationFinished(); @@ -256,11 +267,9 @@ class LazyListView : public QQuickItem { QVector m_layout; QHash m_delegates; QVector m_dyingDelegates; - QVector m_delegatePool; int m_activeAnimations = 0; bool m_componentComplete = false; - bool m_animateDisplacement = false; QSet m_pendingAddAnimations; QList m_modelConnections; From f924dbfbbb8f1a0fe8c36ccca254063f1bbe0e1b Mon Sep 17 00:00:00 2001 From: Evertiro Date: Thu, 2 Apr 2026 23:32:00 -0500 Subject: [PATCH 215/409] feat: show battery status in Bluetooth popout (#1130) * Display bluetooth battery in popout when possible Signed-off-by: Dan Griffiths * Actually return the alert icon if battery status can't be determined Signed-off-by: Dan Griffiths * Fix lint error Signed-off-by: Dan Griffiths * move icon to right + static popout width --------- Signed-off-by: Dan Griffiths Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- modules/bar/popouts/Bluetooth.qml | 9 +++++++++ utils/Icons.qml | 20 ++++++++++++++++++++ 2 files changed, 29 insertions(+) diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 2e87270a1..027b3d41a 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -15,6 +15,7 @@ ColumnLayout { required property PopoutState popouts + width: 300 spacing: Appearance.spacing.small StyledText { @@ -99,6 +100,13 @@ ColumnLayout { Layout.rightMargin: Appearance.spacing.small / 2 Layout.fillWidth: true text: device.modelData.name + elide: Text.ElideRight + } + + MaterialIcon { + visible: device.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type + text: Icons.getBatteryIcon(device.modelData.batteryAvailable ? device.modelData.battery * 100 : -1) + color: device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant } StyledRect { @@ -141,6 +149,7 @@ ColumnLayout { } Loader { + visible: status === Loader.Ready asynchronous: true active: device.modelData.bonded sourceComponent: Item { diff --git a/utils/Icons.qml b/utils/Icons.qml index 8a62f62d7..aded0f4bc 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -240,4 +240,24 @@ Singleton { } return icon; } + + function getBatteryIcon(charge: int): string { + if (charge > 0 && charge < 5) + return "battery_0_bar"; + if (charge >= 5 && charge < 20) + return "battery_1_bar"; + if (charge >= 20 && charge < 35) + return "battery_2_bar"; + if (charge >= 35 && charge < 50) + return "battery_3_bar"; + if (charge >= 50 && charge < 65) + return "battery_4_bar"; + if (charge >= 65 && charge < 80) + return "battery_5_bar"; + if (charge >= 80 && charge < 95) + return "battery_6_bar"; + if (charge >= 95) + return "battery_full"; + return "battery_alert"; + } } From 916b70256293500e7f9c761b28860a8df6172946 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:04:51 +1100 Subject: [PATCH 216/409] fix: use visible height to calc contentHeight --- modules/sidebar/NotifDockList.qml | 1 + .../src/Caelestia/Components/lazylistview.cpp | 55 +++++++++++++++++-- .../src/Caelestia/Components/lazylistview.hpp | 7 +++ 3 files changed, 59 insertions(+), 4 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 8517870ba..98ad0d4dc 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -73,6 +73,7 @@ LazyListView { } LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight + LazyListView.visibleHeight: notifInner.implicitHeight implicitHeight: notifInner.implicitHeight opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1 diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index c92f6963f..84073315a 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -22,6 +22,17 @@ void LazyListViewAttached::setPreferredHeight(qreal height) { emit preferredHeightChanged(); } +qreal LazyListViewAttached::visibleHeight() const { + return m_visibleHeight; +} + +void LazyListViewAttached::setVisibleHeight(qreal height) { + if (qFuzzyCompare(m_visibleHeight, height)) + return; + m_visibleHeight = height; + emit visibleHeightChanged(); +} + bool LazyListViewAttached::adding() const { return m_adding; } @@ -210,6 +221,22 @@ qreal LazyListView::delegateHeight(QQuickItem* item) { return item->implicitHeight(); } +qreal LazyListView::delegateVisibleHeight(QQuickItem* item) { + if (!item) + return 0; + + auto* attached = qobject_cast( + qmlAttachedPropertiesObject(item, false)); + if (attached) { + if (attached->visibleHeight() >= 0) + return attached->visibleHeight(); + if (attached->preferredHeight() >= 0) + return attached->preferredHeight(); + } + + return item->implicitHeight(); +} + // --- Add Animation --- int LazyListView::addDuration() const { @@ -388,15 +415,33 @@ void LazyListView::updatePolish() { // --- Layout Engine --- void LazyListView::relayout() { + // Layout positioning uses preferredHeight (final/non-animated) qreal y = 0; for (auto& record : m_layout) { record.targetY = y; y += (record.heightKnown ? record.height : effectiveEstimatedHeight()) + m_spacing; } - const qreal newHeight = m_layout.isEmpty() ? 0 : y - m_spacing; - if (!qFuzzyCompare(m_contentHeight, newHeight)) { - m_contentHeight = newHeight; + // Content height tracks actual visible heights so scrolling follows animations + qreal visY = 0; + for (int i = 0; i < static_cast(m_layout.size()); ++i) { + qreal h; + if (m_delegates.contains(i) && m_delegates[i].item) + h = delegateVisibleHeight(m_delegates[i].item); + else + h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); + visY += h + m_spacing; + } + qreal maxBottom = m_layout.isEmpty() ? 0 : visY - m_spacing; + + // Account for dying delegates still visually present + for (const auto& dying : m_dyingDelegates) { + if (dying.item) + maxBottom = std::max(maxBottom, dying.item->y() + delegateVisibleHeight(dying.item)); + } + + if (!qFuzzyCompare(m_contentHeight, maxBottom)) { + m_contentHeight = maxBottom; emit contentHeightChanged(); } } @@ -591,12 +636,14 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { // Watch implicitHeight as fallback connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); - // Watch attached preferredHeight if the delegate uses it + // Watch attached properties if the delegate uses them auto* attached = qobject_cast( qmlAttachedPropertiesObject(entry.item, false)); if (attached) { entry.attachedConnection = connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); + connect(attached, &LazyListViewAttached::visibleHeightChanged, + this, [this] { polish(); }); } return entry; diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index b934a2f40..d2cc4fe67 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -18,6 +18,7 @@ class LazyListViewAttached : public QObject { Q_OBJECT Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) + Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged) Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) @@ -27,6 +28,9 @@ class LazyListViewAttached : public QObject { [[nodiscard]] qreal preferredHeight() const; void setPreferredHeight(qreal height); + [[nodiscard]] qreal visibleHeight() const; + void setVisibleHeight(qreal height); + [[nodiscard]] bool adding() const; void setAdding(bool adding); @@ -35,11 +39,13 @@ class LazyListViewAttached : public QObject { signals: void preferredHeightChanged(); + void visibleHeightChanged(); void addingChanged(); void removingChanged(); private: qreal m_preferredHeight = -1; + qreal m_visibleHeight = -1; bool m_adding = false; bool m_removing = false; }; @@ -209,6 +215,7 @@ class LazyListView : public QQuickItem { [[nodiscard]] QRectF effectiveViewport() const; [[nodiscard]] qreal effectiveEstimatedHeight() const; [[nodiscard]] static qreal delegateHeight(QQuickItem* item); + [[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item); void trackHeight(qreal height); void untrackHeight(qreal height); From faaae7be44b6cc348397ea3acba695d264b51ba4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 16:09:00 +1100 Subject: [PATCH 217/409] chore: format --- .../src/Caelestia/Components/lazylistview.cpp | 46 +++++++++---------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 84073315a..47db7556b 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -213,8 +213,7 @@ qreal LazyListView::delegateHeight(QQuickItem* item) { if (!item) return 0; - auto* attached = qobject_cast( - qmlAttachedPropertiesObject(item, false)); + auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); if (attached && attached->preferredHeight() >= 0) return attached->preferredHeight(); @@ -225,8 +224,7 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) { if (!item) return 0; - auto* attached = qobject_cast( - qmlAttachedPropertiesObject(item, false)); + auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); if (attached) { if (attached->visibleHeight() >= 0) return attached->visibleHeight(); @@ -435,7 +433,7 @@ void LazyListView::relayout() { qreal maxBottom = m_layout.isEmpty() ? 0 : visY - m_spacing; // Account for dying delegates still visually present - for (const auto& dying : m_dyingDelegates) { + for (const auto& dying : std::as_const(m_dyingDelegates)) { if (dying.item) maxBottom = std::max(maxBottom, dying.item->y() + delegateVisibleHeight(dying.item)); } @@ -600,8 +598,8 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { // Set adding = true before completeCreate so bindings see it during initial evaluation. // Cleared after creation so the transition from true→false triggers QML Behaviors. - auto* addingAttached = qobject_cast( - qmlAttachedPropertiesObject(entry.item, true)); + auto* addingAttached = + qobject_cast(qmlAttachedPropertiesObject(entry.item, true)); if (addingAttached) addingAttached->setAdding(true); @@ -637,13 +635,13 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); // Watch attached properties if the delegate uses them - auto* attached = qobject_cast( - qmlAttachedPropertiesObject(entry.item, false)); + auto* attached = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); if (attached) { - entry.attachedConnection = connect(attached, &LazyListViewAttached::preferredHeightChanged, - this, onHeightChanged); - connect(attached, &LazyListViewAttached::visibleHeightChanged, - this, [this] { polish(); }); + entry.attachedConnection = + connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); + connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] { + polish(); + }); } return entry; @@ -652,8 +650,7 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { void LazyListView::destroyDelegate(DelegateEntry& entry) { if (entry.animation) { // Disconnect before stopping to prevent re-entrant onAnimationFinished - disconnect(entry.animation, &QAbstractAnimation::finished, - this, &LazyListView::onAnimationFinished); + disconnect(entry.animation, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); entry.animation->stop(); entry.animation = nullptr; --m_activeAnimations; @@ -721,11 +718,12 @@ void LazyListView::connectModel() { connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), - connect(m_model, &QAbstractItemModel::layoutChanged, this, [this] { - for (auto& entry : m_delegates) - updateDelegateData(entry); - polish(); - }), + connect(m_model, &QAbstractItemModel::layoutChanged, this, + [this] { + for (auto& entry : m_delegates) + updateDelegateData(entry); + polish(); + }), connect(m_model, &QObject::destroyed, this, [this] { m_model = nullptr; @@ -818,8 +816,8 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, if (m_removeDuration > 0 && entry.item) { // Signal the delegate via attached property — QML handles the visual transition - auto* attached = qobject_cast( - qmlAttachedPropertiesObject(entry.item, false)); + auto* attached = + qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); if (attached) attached->setRemoving(true); @@ -868,13 +866,11 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) } m_delegates = std::move(shifted); - emit countChanged(); polish(); } -void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, - const QModelIndex& destination, int row) { +void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { if (parent.isValid() || destination.isValid()) return; From cf8b68be3b308a44bea3374c45cce046489ada0d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:03:18 +1100 Subject: [PATCH 218/409] fix: add null guards to notifs --- modules/sidebar/Notif.qml | 18 +++++++++--------- modules/sidebar/NotifActionList.qml | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index fe56798f7..88bd03052 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -22,7 +22,7 @@ StyledRect { radius: Appearance.rounding.small color: { - const c = root.modelData.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); + const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); return expanded ? c : Qt.alpha(c, 0); } @@ -61,8 +61,8 @@ StyledRect { anchors.left: parent.left width: parent.width - text: root.modelData.summary - color: root.modelData.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + text: root.modelData?.summary ?? "" + color: root.modelData?.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 1 @@ -75,7 +75,7 @@ StyledRect { anchors.left: parent.left visible: false - text: root.modelData.summary + text: root.modelData?.summary ?? "" } WrappedLoader { @@ -88,8 +88,8 @@ StyledRect { anchors.leftMargin: Appearance.spacing.small sourceComponent: StyledText { - text: root.modelData.body.replace(/\n/g, " ") - color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + text: (root.modelData?.body ?? "").replace(/\n/g, " ") + color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline elide: Text.ElideRight } } @@ -103,7 +103,7 @@ StyledRect { sourceComponent: StyledText { animate: true - text: root.modelData.timeStr + text: root.modelData?.timeStr ?? "" color: Colours.palette.m3outline font.pointSize: Appearance.font.size.small } @@ -138,8 +138,8 @@ StyledRect { Layout.fillWidth: true textFormat: Text.MarkdownText - text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") - color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + text: (root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") + color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline wrapMode: Text.WordWrap onLinkActivated: link => { diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 370a79cbc..dfc83bd2f 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -101,7 +101,7 @@ Item { { isClose: true }, - ...root.notif.actions, + ...(root.notif?.actions ?? []), { isCopy: true } From 20983ffa6e128fedabbce104630172f2af12ff46 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:59:27 +1100 Subject: [PATCH 219/409] feat: replace notif group list with lazy list view --- modules/sidebar/NotifGroupList.qml | 246 +++++++----------- .../src/Caelestia/Components/lazylistview.cpp | 51 +++- .../src/Caelestia/Components/lazylistview.hpp | 4 + 3 files changed, 142 insertions(+), 159 deletions(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index fe7510b55..e33b3eeae 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,13 +1,12 @@ -pragma ComponentBehavior: Bound - import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Components import qs.components import qs.services import qs.config -Item { +LazyListView { id: root required property Props props @@ -16,19 +15,8 @@ Item { required property Flickable container required property DrawerVisibilities visibilities - readonly property real nonAnimHeight: { - let h = -root.spacing; - for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) - h += item.nonAnimHeight + root.spacing; - } - return h; - } - - readonly property int spacing: Math.round(Appearance.spacing.small / 2) + readonly property real nonAnimHeight: layoutHeight property bool showAllNotifs - property bool flag signal requestToggleExpand(expand: bool) @@ -42,7 +30,15 @@ Item { } Layout.fillWidth: true - implicitHeight: nonAnimHeight + implicitHeight: contentHeight + + spacing: Math.round(Appearance.spacing.small / 2) + + removeDuration: Appearance.anim.durations.normal + + useCustomViewport: true + viewport: Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, + width, container.height) Timer { id: clearTimer @@ -51,164 +47,116 @@ Item { onTriggered: root.showAllNotifs = false } - Repeater { - id: repeater - - model: ScriptModel { - values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) - onValuesChanged: root.flagChanged() - } - - delegate: NotifDelegate {} - } - - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + model: ScriptModel { + values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) } - component NotifDelegate: MouseArea { - id: notif + delegate: Component { + MouseArea { + id: notif - required property int index - required property NotifData modelData + required property int index + required property NotifData modelData - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - readonly property bool previewHidden: { - if (root.expanded) - return false; + readonly property bool previewHidden: { + if (root.expanded) + return false; - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i].closed) - extraHidden++; + let extraHidden = 0; + for (let i = 0; i < index; i++) + if (root.notifs[i]?.closed) + extraHidden++; - return index >= Config.notifs.groupPreviewNum + extraHidden; - } - property int startY - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i) as NotifDelegate; - if (item && !item.modelData.closed && !item.previewHidden) - y += item.nonAnimHeight + root.spacing; + return index >= Config.notifs.groupPreviewNum + extraHidden; } - return y; - } + property int startY - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } - } + Component.onCompleted: modelData?.lock(this) + Component.onDestruction: modelData?.unlock(this) - opacity: previewHidden ? 0 : 1 - scale: previewHidden ? 0.7 : 1 + LazyListView.preferredHeight: modelData?.closed || previewHidden ? 0 : notifInner.nonAnimHeight + LazyListView.visibleHeight: modelData?.closed || previewHidden ? 0 : notifInner.implicitHeight + implicitHeight: notifInner.implicitHeight - implicitWidth: root.width - implicitHeight: notifInner.implicitHeight + opacity: LazyListView.removing || modelData?.closed || previewHidden || LazyListView.adding ? 0 : 1 + scale: LazyListView.removing || previewHidden ? 0.7 : LazyListView.adding ? 0.7 : 1 - hoverEnabled: true - cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: !root.expanded - enabled: !modelData.closed + hoverEnabled: true + cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: !root.expanded + enabled: !(modelData?.closed ?? true) - drag.target: this - drag.axis: Drag.XAxis + drag.target: this + drag.axis: Drag.XAxis - onPressed: event => { - startY = event.y; - if (event.button === Qt.RightButton) - root.requestToggleExpand(!root.expanded); - else if (event.button === Qt.MiddleButton) - modelData.close(); - } - onPositionChanged: event => { - if (pressed && !root.expanded) { - const diffY = event.y - startY; - if (Math.abs(diffY) > Config.notifs.expandThreshold) - root.requestToggleExpand(diffY > 0); + onPressed: event => { + startY = event.y; + if (event.button === Qt.RightButton) + root.requestToggleExpand(!root.expanded); + else if (event.button === Qt.MiddleButton) + modelData?.close(); } - } - onReleased: event => { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - ParallelAnimation { - Component.onCompleted: running = !notif.previewHidden - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 + onPositionChanged: event => { + if (pressed && !root.expanded) { + const diffY = event.y - startY; + if (Math.abs(diffY) > Config.notifs.expandThreshold) + root.requestToggleExpand(diffY > 0); + } } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 + onReleased: event => { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else + modelData?.close(); } - } - - ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) - Anim { - target: notif - property: "opacity" - to: 0 + ParallelAnimation { + running: notif.modelData?.closed ?? false + onFinished: notif.modelData?.unlock(notif) + + Anim { + target: notif + property: "opacity" + to: 0 + } + Anim { + target: notif + property: "x" + to: notif.x >= 0 ? notif.width : -notif.width + } } - Anim { - target: notif - property: "x" - to: notif.x >= 0 ? notif.width : -notif.width - } - } - Notif { - id: notifInner + Notif { + id: notifInner - anchors.fill: parent - modelData: notif.modelData - props: root.props - expanded: root.expanded - visibilities: root.visibilities - } + anchors.fill: parent + modelData: notif.modelData + props: root.props + expanded: root.expanded + visibilities: root.visibilities + } - Behavior on opacity { - Anim {} - } + Behavior on y { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } - Behavior on scale { - Anim {} - } + Behavior on opacity { + Anim {} + } - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on scale { + Anim {} } - } - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } } } diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 47db7556b..88703a7f6 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -126,6 +126,10 @@ qreal LazyListView::contentHeight() const { return m_contentHeight; } +qreal LazyListView::layoutHeight() const { + return m_layoutHeight; +} + qreal LazyListView::contentY() const { return m_contentY; } @@ -413,33 +417,55 @@ void LazyListView::updatePolish() { // --- Layout Engine --- void LazyListView::relayout() { - // Layout positioning uses preferredHeight (final/non-animated) + // Layout positioning uses preferredHeight (final/non-animated). + // Only add spacing between items with non-zero height. qreal y = 0; + bool hasLayoutItem = false; for (auto& record : m_layout) { record.targetY = y; - y += (record.heightKnown ? record.height : effectiveEstimatedHeight()) + m_spacing; + const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); + if (layoutH > 0) { + if (hasLayoutItem) + y += m_spacing; + hasLayoutItem = true; + y += layoutH; + } } - // Content height tracks actual visible heights so scrolling follows animations + if (!qFuzzyCompare(m_layoutHeight, y)) { + m_layoutHeight = y; + emit layoutHeightChanged(); + } + + // Content height tracks actual visible heights so scrolling follows animations. + // Only add spacing between items with non-zero visible height. qreal visY = 0; + bool hasVisItem = false; for (int i = 0; i < static_cast(m_layout.size()); ++i) { qreal h; if (m_delegates.contains(i) && m_delegates[i].item) h = delegateVisibleHeight(m_delegates[i].item); else h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); - visY += h + m_spacing; + if (h > 0) { + if (hasVisItem) + visY += m_spacing; + hasVisItem = true; + visY += h; + } } - qreal maxBottom = m_layout.isEmpty() ? 0 : visY - m_spacing; // Account for dying delegates still visually present for (const auto& dying : std::as_const(m_dyingDelegates)) { - if (dying.item) - maxBottom = std::max(maxBottom, dying.item->y() + delegateVisibleHeight(dying.item)); + if (!dying.item) + continue; + const qreal dyingH = delegateVisibleHeight(dying.item); + if (dyingH > 0) + visY = std::max(visY, dying.item->y() + dyingH); } - if (!qFuzzyCompare(m_contentHeight, maxBottom)) { - m_contentHeight = maxBottom; + if (!qFuzzyCompare(m_contentHeight, visY)) { + m_contentHeight = visY; emit contentHeightChanged(); } } @@ -525,7 +551,9 @@ void LazyListView::syncDelegates() { if (entry.item) { // Measure height (prefer attached preferredHeight, fall back to implicitHeight) const qreal h = delegateHeight(entry.item); - if (h > 0 && !m_layout[i].heightKnown) { + if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) { + if (m_layout[i].heightKnown) + untrackHeight(m_layout[i].height); m_layout[i].height = h; m_layout[i].heightKnown = true; trackHeight(h); @@ -625,6 +653,9 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (wasKnown) untrackHeight(oldH); trackHeight(h); + // Relayout immediately so layoutHeight/contentHeight update + // synchronously for parent bindings, then polish for delegate sync. + relayout(); polish(); } return; diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index d2cc4fe67..f3f922084 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -62,6 +62,7 @@ class LazyListView : public QQuickItem { // Layout Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged) + Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged) Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) // Viewport & Lazy Loading @@ -110,6 +111,7 @@ class LazyListView : public QQuickItem { void setSpacing(qreal spacing); [[nodiscard]] qreal contentHeight() const; + [[nodiscard]] qreal layoutHeight() const; [[nodiscard]] qreal contentY() const; void setContentY(qreal contentY); @@ -170,6 +172,7 @@ class LazyListView : public QQuickItem { void delegateChanged(); void spacingChanged(); void contentHeightChanged(); + void layoutHeightChanged(); void contentYChanged(); void viewportChanged(); void useCustomViewportChanged(); @@ -248,6 +251,7 @@ class LazyListView : public QQuickItem { qreal m_spacing = 0; qreal m_contentHeight = 0; + qreal m_layoutHeight = 0; qreal m_contentY = 0; QRectF m_viewport; From ea776c20e09b9189eefc2b816be9e50cc69c3aa0 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:14:49 +1100 Subject: [PATCH 220/409] fix: lazy list view spacing + debounce relayout --- .../src/Caelestia/Components/lazylistview.cpp | 18 +++++++++++++----- .../src/Caelestia/Components/lazylistview.hpp | 1 + 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 88703a7f6..b8c379763 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -422,13 +422,15 @@ void LazyListView::relayout() { qreal y = 0; bool hasLayoutItem = false; for (auto& record : m_layout) { - record.targetY = y; const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); if (layoutH > 0) { if (hasLayoutItem) y += m_spacing; hasLayoutItem = true; + record.targetY = y; y += layoutH; + } else { + record.targetY = y; } } @@ -653,10 +655,16 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (wasKnown) untrackHeight(oldH); trackHeight(h); - // Relayout immediately so layoutHeight/contentHeight update - // synchronously for parent bindings, then polish for delegate sync. - relayout(); - polish(); + // Batch relayout: multiple height changes in the same event loop + // iteration are coalesced into a single relayout + polish. + if (!m_relayoutPending) { + m_relayoutPending = true; + QTimer::singleShot(0, this, [this] { + m_relayoutPending = false; + relayout(); + polish(); + }); + } } return; } diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index f3f922084..7147169cb 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -281,6 +281,7 @@ class LazyListView : public QQuickItem { int m_activeAnimations = 0; bool m_componentComplete = false; + bool m_relayoutPending = false; QSet m_pendingAddAnimations; QList m_modelConnections; From 700ad61969ac9ae9339b4335f0b119ad552bb103 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:33:30 +1100 Subject: [PATCH 221/409] feat: listview async delegate creation and destruction --- modules/sidebar/NotifGroupList.qml | 1 + .../src/Caelestia/Components/lazylistview.cpp | 89 ++++++++++++++----- .../src/Caelestia/Components/lazylistview.hpp | 9 ++ 3 files changed, 77 insertions(+), 22 deletions(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index e33b3eeae..fab8c9b08 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -33,6 +33,7 @@ LazyListView { implicitHeight: contentHeight spacing: Math.round(Appearance.spacing.small / 2) + asynchronous: true removeDuration: Appearance.anim.durations.normal diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index b8c379763..07c4d31e1 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -4,6 +4,13 @@ #include #include +namespace { + +constexpr int ASYNC_BATCH_CREATE = 2; +constexpr int ASYNC_BATCH_DESTROY = 4; + +} // namespace + namespace caelestia::components { // --- LazyListViewAttached --- @@ -195,6 +202,17 @@ void LazyListView::setEstimatedHeight(qreal height) { polish(); } +bool LazyListView::asynchronous() const { + return m_asynchronous; +} + +void LazyListView::setAsynchronous(bool async) { + if (m_asynchronous == async) + return; + m_asynchronous = async; + emit asynchronousChanged(); +} + qreal LazyListView::effectiveEstimatedHeight() const { if (m_estimatedHeight >= 0) return m_estimatedHeight; @@ -531,41 +549,68 @@ void LazyListView::syncDelegates() { visibleIndices.insert(i); } - // Destroy delegates outside visible range (if not animating) + // Collect delegates to destroy (outside visible range and not animating) QList toRemove; for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (!visibleIndices.contains(it.key()) && !it->animation) { + if (!visibleIndices.contains(it.key()) && !it->animation) toRemove.append(it.key()); - } } + + // Batch destroy + const int destroyBudget = m_asynchronous ? ASYNC_BATCH_DESTROY : static_cast(toRemove.size()); + QVector removedEntries; + removedEntries.reserve(std::min(destroyBudget, static_cast(toRemove.size()))); + int destroyed = 0; for (int idx : toRemove) { - auto entry = m_delegates.take(idx); - destroyDelegate(entry); + if (destroyed >= destroyBudget) + break; + removedEntries.append(m_delegates.take(idx)); + ++destroyed; } + for (auto& entry : removedEntries) + destroyDelegate(entry); - // Create delegates for newly visible indices + // Collect indices to create + QList toCreate; if (first >= 0) { for (int i = first; i <= last; ++i) { - if (m_delegates.contains(i)) - continue; + if (!m_delegates.contains(i)) + toCreate.append(i); + } + } - auto entry = createDelegate(i); - if (entry.item) { - // Measure height (prefer attached preferredHeight, fall back to implicitHeight) - const qreal h = delegateHeight(entry.item); - if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) { - if (m_layout[i].heightKnown) - untrackHeight(m_layout[i].height); - m_layout[i].height = h; - m_layout[i].heightKnown = true; - trackHeight(h); - } - // Position immediately so it doesn't flash at y=0 - entry.item->setY(m_layout[i].targetY - m_contentY); - m_delegates.insert(i, std::move(entry)); + // Batch create + const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast(toCreate.size()); + int created = 0; + bool layoutChanged = false; + for (int i : toCreate) { + if (created >= createBudget) + break; + + auto entry = createDelegate(i); + if (entry.item) { + const qreal h = delegateHeight(entry.item); + if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) { + if (m_layout[i].heightKnown) + untrackHeight(m_layout[i].height); + m_layout[i].height = h; + m_layout[i].heightKnown = true; + trackHeight(h); + layoutChanged = true; } + entry.item->setY(m_layout[i].targetY - m_contentY); + m_delegates.insert(i, std::move(entry)); + ++created; } } + + if (layoutChanged) + relayout(); + + // If async and there's remaining work, schedule another pass + if (m_asynchronous && + (destroyed < static_cast(toRemove.size()) || created < static_cast(toCreate.size()))) + polish(); } LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index 7147169cb..e24be426a 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -73,6 +73,9 @@ class LazyListView : public QQuickItem { // Sizing Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged) + // Async + Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged) + // Add Animation Q_PROPERTY(int addDuration READ addDuration WRITE setAddDuration NOTIFY addDurationChanged) Q_PROPERTY(QEasingCurve addCurve READ addCurve WRITE setAddCurve NOTIFY addCurveChanged) @@ -130,6 +133,10 @@ class LazyListView : public QQuickItem { [[nodiscard]] qreal estimatedHeight() const; void setEstimatedHeight(qreal height); + // Async + [[nodiscard]] bool asynchronous() const; + void setAsynchronous(bool async); + // Add Animation [[nodiscard]] int addDuration() const; void setAddDuration(int duration); @@ -178,6 +185,7 @@ class LazyListView : public QQuickItem { void useCustomViewportChanged(); void cacheBufferChanged(); void estimatedHeightChanged(); + void asynchronousChanged(); void addDurationChanged(); void addCurveChanged(); void addFromOpacityChanged(); @@ -261,6 +269,7 @@ class LazyListView : public QQuickItem { qreal m_estimatedHeight = -1; qreal m_knownHeightSum = 0; int m_knownHeightCount = 0; + bool m_asynchronous = false; int m_addDuration = 300; QEasingCurve m_addCurve; From 1c09222dd7475b8e39cb7722f6f2393b0954b59b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:37:14 +1100 Subject: [PATCH 222/409] fix: only destroy delegates when visually out of viewport --- plugin/src/Caelestia/Components/lazylistview.cpp | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 07c4d31e1..50340ca79 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -549,10 +549,19 @@ void LazyListView::syncDelegates() { visibleIndices.insert(i); } - // Collect delegates to destroy (outside visible range and not animating) + // Collect delegates to destroy — only if visually outside the viewport + const auto vp = effectiveViewport(); QList toRemove; for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (!visibleIndices.contains(it.key()) && !it->animation) + if (visibleIndices.contains(it.key()) || it->animation) + continue; + if (!it->item) { + toRemove.append(it.key()); + continue; + } + const qreal itemTop = it->item->y(); + const qreal itemBottom = itemTop + delegateVisibleHeight(it->item); + if (itemBottom < vp.top() || itemTop > vp.bottom()) toRemove.append(it.key()); } From 8f3f1b905657dddb8b05706155a2f6f9536a7031 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:49:07 +1100 Subject: [PATCH 223/409] feat: map listview delegates to indexes --- .../src/Caelestia/Components/lazylistview.cpp | 72 +++++++++++-------- .../src/Caelestia/Components/lazylistview.hpp | 1 + 2 files changed, 42 insertions(+), 31 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 50340ca79..d3c85c280 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -463,8 +463,9 @@ void LazyListView::relayout() { bool hasVisItem = false; for (int i = 0; i < static_cast(m_layout.size()); ++i) { qreal h; - if (m_delegates.contains(i) && m_delegates[i].item) - h = delegateVisibleHeight(m_delegates[i].item); + auto dit = m_delegates.find(i); + if (dit != m_delegates.end() && dit->item) + h = delegateVisibleHeight(dit->item); else h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); if (h > 0) { @@ -573,7 +574,10 @@ void LazyListView::syncDelegates() { for (int idx : toRemove) { if (destroyed >= destroyBudget) break; - removedEntries.append(m_delegates.take(idx)); + auto entry = m_delegates.take(idx); + if (entry.item) + m_itemToIndex.remove(entry.item); + removedEntries.append(std::move(entry)); ++destroyed; } for (auto& entry : removedEntries) @@ -608,6 +612,7 @@ void LazyListView::syncDelegates() { layoutChanged = true; } entry.item->setY(m_layout[i].targetY - m_contentY); + m_itemToIndex.insert(entry.item, i); m_delegates.insert(i, std::move(entry)); ++created; } @@ -692,35 +697,32 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (addingAttached) addingAttached->setAdding(false); - // Shared height-change handler — captures item pointer instead of index - // so it remains valid after model inserts/removes/moves shift indices. + // Height-change handler — uses m_itemToIndex for O(1) lookup auto onHeightChanged = [this, item = entry.item] { - // Find the delegate entry by item pointer - for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (it->item != item) - continue; - const int idx = it.key(); - const qreal h = delegateHeight(item); - if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height, h)) { - const qreal oldH = m_layout[idx].height; - const bool wasKnown = m_layout[idx].heightKnown; - m_layout[idx].height = h; - m_layout[idx].heightKnown = true; - if (wasKnown) - untrackHeight(oldH); - trackHeight(h); - // Batch relayout: multiple height changes in the same event loop - // iteration are coalesced into a single relayout + polish. - if (!m_relayoutPending) { - m_relayoutPending = true; - QTimer::singleShot(0, this, [this] { - m_relayoutPending = false; - relayout(); - polish(); - }); - } - } + auto indexIt = m_itemToIndex.find(item); + if (indexIt == m_itemToIndex.end()) + return; + const int idx = indexIt.value(); + auto delegateIt = m_delegates.find(idx); + if (delegateIt == m_delegates.end() || delegateIt->item != item) return; + const qreal h = delegateHeight(item); + if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height, h)) { + const qreal oldH = m_layout[idx].height; + const bool wasKnown = m_layout[idx].heightKnown; + m_layout[idx].height = h; + m_layout[idx].heightKnown = true; + if (wasKnown) + untrackHeight(oldH); + trackHeight(h); + if (!m_relayoutPending) { + m_relayoutPending = true; + QTimer::singleShot(0, this, [this] { + m_relayoutPending = false; + relayout(); + polish(); + }); + } } }; @@ -837,6 +839,7 @@ void LazyListView::resetContent() { for (auto& entry : m_delegates) destroyDelegate(entry); m_delegates.clear(); + m_itemToIndex.clear(); for (auto& entry : m_dyingDelegates) destroyDelegate(entry); @@ -883,6 +886,8 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last entry.modelIndex = newIdx; if (entry.context) entry.context->setContextProperty(QStringLiteral("index"), newIdx); + if (entry.item) + m_itemToIndex[entry.item] = newIdx; shifted.insert(newIdx, std::move(entry)); } m_delegates = std::move(shifted); @@ -904,11 +909,12 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, continue; auto entry = m_delegates.take(i); + if (entry.item) + m_itemToIndex.remove(entry.item); entry.pendingRemoval = true; stopAnimation(entry); if (m_removeDuration > 0 && entry.item) { - // Signal the delegate via attached property — QML handles the visual transition auto* attached = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); if (attached) @@ -955,6 +961,8 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) entry.modelIndex = newIdx; if (entry.context) entry.context->setContextProperty(QStringLiteral("index"), newIdx); + if (entry.item) + m_itemToIndex[entry.item] = newIdx; shifted.insert(newIdx, std::move(entry)); } m_delegates = std::move(shifted); @@ -998,6 +1006,8 @@ void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, co entry.modelIndex = newIdx; if (entry.context) entry.context->setContextProperty(QStringLiteral("index"), newIdx); + if (entry.item) + m_itemToIndex[entry.item] = newIdx; remapped.insert(newIdx, std::move(entry)); } m_delegates = std::move(remapped); diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index e24be426a..f4f28c631 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -286,6 +286,7 @@ class LazyListView : public QQuickItem { QVector m_layout; QHash m_delegates; + QHash m_itemToIndex; QVector m_dyingDelegates; int m_activeAnimations = 0; From 08809320dee040adf8ae45806762bad22a9554dc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:30:42 +1100 Subject: [PATCH 224/409] fix: qFuzzyCompare + 1.0 --- plugin/src/Caelestia/Components/lazylistview.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index d3c85c280..d76ae3a7e 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -23,7 +23,7 @@ qreal LazyListViewAttached::preferredHeight() const { } void LazyListViewAttached::setPreferredHeight(qreal height) { - if (qFuzzyCompare(m_preferredHeight, height)) + if (qFuzzyCompare(m_preferredHeight + 1.0, height + 1.0)) return; m_preferredHeight = height; emit preferredHeightChanged(); @@ -34,7 +34,7 @@ qreal LazyListViewAttached::visibleHeight() const { } void LazyListViewAttached::setVisibleHeight(qreal height) { - if (qFuzzyCompare(m_visibleHeight, height)) + if (qFuzzyCompare(m_visibleHeight + 1.0, height + 1.0)) return; m_visibleHeight = height; emit visibleHeightChanged(); @@ -452,7 +452,7 @@ void LazyListView::relayout() { } } - if (!qFuzzyCompare(m_layoutHeight, y)) { + if (!qFuzzyCompare(m_layoutHeight + 1.0, y + 1.0)) { m_layoutHeight = y; emit layoutHeightChanged(); } @@ -485,7 +485,7 @@ void LazyListView::relayout() { visY = std::max(visY, dying.item->y() + dyingH); } - if (!qFuzzyCompare(m_contentHeight, visY)) { + if (!qFuzzyCompare(m_contentHeight + 1.0, visY + 1.0)) { m_contentHeight = visY; emit contentHeightChanged(); } @@ -603,7 +603,7 @@ void LazyListView::syncDelegates() { auto entry = createDelegate(i); if (entry.item) { const qreal h = delegateHeight(entry.item); - if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height, h)) { + if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height + 1.0, h + 1.0)) { if (m_layout[i].heightKnown) untrackHeight(m_layout[i].height); m_layout[i].height = h; @@ -707,7 +707,7 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (delegateIt == m_delegates.end() || delegateIt->item != item) return; const qreal h = delegateHeight(item); - if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height, h)) { + if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) { const qreal oldH = m_layout[idx].height; const bool wasKnown = m_layout[idx].heightKnown; m_layout[idx].height = h; From 2fbdfad472b5e13bb13f423625d08e060315db59 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 19:59:14 +1100 Subject: [PATCH 225/409] fix: notif group collapse lag It sort of breaks the anim but it's a worthwhile tradeoff Without this it is just wayyy too laggy with large amounts of notifs --- modules/sidebar/NotifGroupList.qml | 22 ++-------------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index fab8c9b08..fce03a504 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -16,19 +16,9 @@ LazyListView { required property DrawerVisibilities visibilities readonly property real nonAnimHeight: layoutHeight - property bool showAllNotifs signal requestToggleExpand(expand: bool) - onExpandedChanged: { - if (expanded) { - clearTimer.stop(); - showAllNotifs = true; - } else { - clearTimer.start(); - } - } - Layout.fillWidth: true implicitHeight: contentHeight @@ -38,18 +28,10 @@ LazyListView { removeDuration: Appearance.anim.durations.normal useCustomViewport: true - viewport: Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, - width, container.height) - - Timer { - id: clearTimer - - interval: Appearance.anim.durations.normal - onTriggered: root.showAllNotifs = false - } + viewport: Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height) model: ScriptModel { - values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) + values: root.expanded ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) } delegate: Component { From b83ac6e50a92fea7e85bf8ddab26da36b1cb01b6 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:25:04 +1100 Subject: [PATCH 226/409] feat: use curve for notif clear all anim Speeds up clearing large numbers of notifications --- modules/sidebar/NotifDock.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index c00f2bd43..177b6147b 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -149,7 +149,7 @@ Item { id: clearTimer repeat: true - interval: 50 + interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(Notifs.notClosed.length))) onTriggered: { const first = Notifs.notClosed[0]; if (first) { From 5b153fde8f31061099ed5844f809068f2c6c4513 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 20:47:13 +1100 Subject: [PATCH 227/409] fix: batch clear all notifs to prevent blocking --- modules/sidebar/NotifDock.qml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 177b6147b..dd88ec3d1 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -149,14 +149,24 @@ Item { id: clearTimer repeat: true + triggeredOnStart: true interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(Notifs.notClosed.length))) onTriggered: { const first = Notifs.notClosed[0]; - if (first) { - for (const n of Notifs.notClosed.filter(n => n.appName === first.appName)) - n.close(); - } else { + if (!first) { stop(); + return; + } + + const appName = first.appName; + let cleared = 0; + for (const n of Notifs.notClosed.filter(n => n.appName === appName)) { + n.close(); + cleared++; + if (cleared > 30) { + interval = 5; + return; + } } } } From a40b1c2c77d615efda7089e843e675e219ebc9ec Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 3 Apr 2026 22:28:10 +1100 Subject: [PATCH 228/409] fix: use bound component context --- modules/sidebar/Notif.qml | 4 ++-- modules/sidebar/NotifDock.qml | 2 ++ modules/sidebar/NotifDockList.qml | 2 ++ modules/sidebar/NotifGroupList.qml | 2 ++ plugin/src/Caelestia/Components/lazylistview.cpp | 12 +++++++++--- 5 files changed, 17 insertions(+), 5 deletions(-) diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 88bd03052..d646c4f01 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -88,7 +88,7 @@ StyledRect { anchors.leftMargin: Appearance.spacing.small sourceComponent: StyledText { - text: (root.modelData?.body ?? "").replace(/\n/g, " ") + text: String(root.modelData?.body ?? "").replace(/\n/g, " ") color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline elide: Text.ElideRight } @@ -138,7 +138,7 @@ StyledRect { Layout.fillWidth: true textFormat: Text.MarkdownText - text: (root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") + text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline wrapMode: Text.WordWrap diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index dd88ec3d1..11ce4dac2 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts import Quickshell.Widgets diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 98ad0d4dc..a799cf1ae 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import Quickshell import Caelestia.Components diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index fce03a504..a54a13b63 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts import Quickshell diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index d76ae3a7e..d05ca095e 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -637,8 +637,9 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { const auto roleNames = m_model->roleNames(); const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); - // Use the delegate component's creation context so the delegate - // can access ids and properties from the scope where it was defined. + // Use the delegate component's creation context directly for beginCreate + // so bound components (pragma ComponentBehavior: Bound) are accepted. + // A per-delegate child context is kept for data updates. auto* compContext = m_delegate->creationContext(); auto* parentContext = compContext ? compContext : qmlContext(this); if (!parentContext) @@ -669,10 +670,15 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { initialProps.insert(QStringLiteral("modelData"), value); } - auto* obj = m_delegate->beginCreate(entry.context); + // Use the creation context for beginCreate to satisfy bound component checks + // (pragma ComponentBehavior: Bound). Data is passed via setInitialProperties. + auto* creationCtx = compContext ? compContext : parentContext; + auto* obj = m_delegate->beginCreate(creationCtx); entry.item = qobject_cast(obj); if (!entry.item) { + if (obj) + m_delegate->completeCreate(); delete obj; delete entry.context; entry.context = nullptr; From 673b09626167612dd3afa1d1568640a9a72f25f5 Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Mon, 6 Apr 2026 11:18:30 +0200 Subject: [PATCH 229/409] fix: iface -> interface in VPN service (#1376) --- services/VPN.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/VPN.qml b/services/VPN.qml index 31b683194..bc0addce4 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -25,7 +25,7 @@ Singleton { } readonly property bool isCustomProvider: typeof providerInput === "object" readonly property string providerName: isCustomProvider ? (providerInput.name || "custom") : String(providerInput) - readonly property string interfaceName: isCustomProvider ? (providerInput.iface || "") : "" + readonly property string interfaceName: isCustomProvider ? (providerInput.interface || "") : "" readonly property var currentConfig: { const name = providerName; const iface = interfaceName; @@ -36,7 +36,7 @@ Singleton { return { connectCmd: custom.connectCmd || defaults.connectCmd, disconnectCmd: custom.disconnectCmd || defaults.disconnectCmd, - interface: custom.iface || defaults.interface, + interface: custom.interface || defaults.interface, displayName: custom.displayName || defaults.displayName }; } From ffa7d88caee13abea21d9243bc06588d4b2d693c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 6 Apr 2026 21:12:14 +1000 Subject: [PATCH 230/409] ci: use clang format dry-run -Werror --- .github/workflows/check-format.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml index b26cb9498..ea5fe9ef0 100644 --- a/.github/workflows/check-format.yml +++ b/.github/workflows/check-format.yml @@ -34,6 +34,5 @@ jobs: - name: Check C++ format shell: fish {0} run: | - for file in (string match -v 'build/*' **.cpp **.hpp) - clang-format $file | diff -u $file - || exit 1 - end + find plugin extras -name '*.cpp' -o -name '*.hpp' \ + | xargs clang-format --dry-run --Werror From 612f828b9f84ca0dda76ce009c3ff64f75c5733e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:04:48 +1000 Subject: [PATCH 231/409] fix: don't loader variants Change model instead This may or may not help with the background not loading --- modules/background/Background.qml | 261 +++++++++++++++--------------- 1 file changed, 128 insertions(+), 133 deletions(-) diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 95109e121..0f44b1a00 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -7,160 +7,155 @@ import qs.components.containers import qs.services import qs.config -Loader { - asynchronous: true - active: Config.background.enabled +Variants { + model: Config.background.enabled ? Screens.screens : [] - sourceComponent: Variants { - model: Screens.screens + StyledWindow { + id: win - StyledWindow { - id: win + required property ShellScreen modelData - required property ShellScreen modelData + screen: modelData + name: "background" + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom + color: Config.background.wallpaperEnabled ? "black" : "transparent" + surfaceFormat.opaque: false - screen: modelData - name: "background" - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom - color: Config.background.wallpaperEnabled ? "black" : "transparent" - surfaceFormat.opaque: false + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true - anchors.top: true - anchors.bottom: true - anchors.left: true - anchors.right: true + Item { + id: behindClock - Item { - id: behindClock + anchors.fill: parent + + Loader { + id: wallpaper + + asynchronous: true anchors.fill: parent + active: Config.background.wallpaperEnabled - Loader { - id: wallpaper + sourceComponent: Wallpaper {} + } - asynchronous: true + Visualiser { + anchors.fill: parent + screen: win.modelData + wallpaper: wallpaper + } + } - anchors.fill: parent - active: Config.background.wallpaperEnabled + Loader { + id: clockLoader - sourceComponent: Wallpaper {} - } + asynchronous: true + active: Config.background.desktopClock.enabled - Visualiser { - anchors.fill: parent - screen: win.modelData - wallpaper: wallpaper - } - } + anchors.margins: Appearance.padding.large * 2 + anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) - Loader { - id: clockLoader + state: Config.background.desktopClock.position + states: [ + State { + name: "top-left" - asynchronous: true - active: Config.background.desktopClock.enabled - - anchors.margins: Appearance.padding.large * 2 - anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) - - state: Config.background.desktopClock.position - states: [ - State { - name: "top-left" - - AnchorChanges { - target: clockLoader - anchors.top: parent.top - anchors.left: parent.left - } - }, - State { - name: "top-center" - - AnchorChanges { - target: clockLoader - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - } - }, - State { - name: "top-right" - - AnchorChanges { - target: clockLoader - anchors.top: parent.top - anchors.right: parent.right - } - }, - State { - name: "middle-left" - - AnchorChanges { - target: clockLoader - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - } - }, - State { - name: "middle-center" - - AnchorChanges { - target: clockLoader - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - } - }, - State { - name: "middle-right" - - AnchorChanges { - target: clockLoader - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - } - }, - State { - name: "bottom-left" - - AnchorChanges { - target: clockLoader - anchors.bottom: parent.bottom - anchors.left: parent.left - } - }, - State { - name: "bottom-center" - - AnchorChanges { - target: clockLoader - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - } - }, - State { - name: "bottom-right" - - AnchorChanges { - target: clockLoader - anchors.bottom: parent.bottom - anchors.right: parent.right - } + AnchorChanges { + target: clockLoader + anchors.top: parent.top + anchors.left: parent.left } - ] - - transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + }, + State { + name: "top-center" + + AnchorChanges { + target: clockLoader + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + } + }, + State { + name: "top-right" + + AnchorChanges { + target: clockLoader + anchors.top: parent.top + anchors.right: parent.right + } + }, + State { + name: "middle-left" + + AnchorChanges { + target: clockLoader + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + } + }, + State { + name: "middle-center" + + AnchorChanges { + target: clockLoader + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + } + }, + State { + name: "middle-right" + + AnchorChanges { + target: clockLoader + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } + }, + State { + name: "bottom-left" + + AnchorChanges { + target: clockLoader + anchors.bottom: parent.bottom + anchors.left: parent.left + } + }, + State { + name: "bottom-center" + + AnchorChanges { + target: clockLoader + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + } + }, + State { + name: "bottom-right" + + AnchorChanges { + target: clockLoader + anchors.bottom: parent.bottom + anchors.right: parent.right } } + ] - sourceComponent: DesktopClock { - wallpaper: behindClock - absX: clockLoader.x - absY: clockLoader.y + transitions: Transition { + AnchorAnimation { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial } } + + sourceComponent: DesktopClock { + wallpaper: behindClock + absX: clockLoader.x + absY: clockLoader.y + } } } } From 0e50b624138c437e578fa51914f70ce9e8a52929 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:32:17 +1000 Subject: [PATCH 232/409] refactor: move drawers window -> ContentWindow.qml --- modules/bar/BarWrapper.qml | 4 +- modules/drawers/ContentWindow.qml | 322 +++++++++++++++++++++++++++ modules/drawers/Drawers.qml | 346 +----------------------------- 3 files changed, 328 insertions(+), 344 deletions(-) create mode 100644 modules/drawers/ContentWindow.qml diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index f29e23f93..ec9df2d18 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import qs.components import qs.config +import qs.utils import qs.modules.bar.popouts as BarPopouts Item { @@ -12,9 +13,10 @@ Item { required property ShellScreen screen required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts - required property bool disabled required property bool fullscreen + readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) + readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml new file mode 100644 index 000000000..7171c8c57 --- /dev/null +++ b/modules/drawers/ContentWindow.qml @@ -0,0 +1,322 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Caelestia.Blobs +import qs.components +import qs.components.containers +import qs.services +import qs.config +import qs.modules.bar + +StyledWindow { + id: root + + readonly property alias bar: bar + + readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) + readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0 + readonly property bool hasFullscreen: { + if (hasSpecialWorkspace) { + const specialName = monitor?.lastIpcObject.specialWorkspace?.name; + if (!specialName) + return false; + const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); + return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + property real borderThickness: hasFullscreen ? 0 : Config.border.thickness + readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness + property real borderRounding: hasFullscreen ? 0 : Config.border.rounding + property real shadowOpacity: hasFullscreen ? 0 : 0.7 + + readonly property int dragMaskPadding: { + if (focusGrab.active || panels.popouts.isDetached) + return 0; + + if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0) + return 0; + + const thresholds = []; + for (const panel of ["dashboard", "launcher", "session", "sidebar"]) + if (Config[panel].enabled) + thresholds.push(Config[panel].dragThreshold); + return Math.max(...thresholds); + } + + onHasFullscreenChanged: { + visibilities.launcher = false; + visibilities.session = false; + visibilities.dashboard = false; + } + + name: "drawers" + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + mask: Regions { + bar: bar + panels: panels + win: root + } + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + Behavior on borderThickness { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on borderRounding { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on shadowOpacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + HyprlandFocusGrab { + id: focusGrab + + active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) + windows: [root] + onCleared: { + visibilities.launcher = false; + visibilities.session = false; + visibilities.sidebar = false; + visibilities.dashboard = false; + panels.popouts.hasCurrent = false; + bar.closeTray(); + } + } + + StyledRect { + anchors.fill: parent + opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 + color: Colours.palette.m3scrim + + Behavior on opacity { + Anim {} + } + } + + Item { + anchors.fill: parent + opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + blurMax: 15 + shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity)) + } + + // Border { + // bar: bar + // } + + // Backgrounds { + // panels: panels + // bar: bar + // } + + BlobGroup { + id: blobGroup + + color: Colours.palette.m3surface + + Behavior on color { + CAnim {} + } + } + + BlobInvertedRect { + anchors.fill: parent + anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers + group: blobGroup + radius: root.borderRounding + borderLeft: bar.implicitWidth - anchors.margins + borderRight: root.borderThickness - anchors.margins + borderTop: root.borderThickness - anchors.margins + borderBottom: root.borderThickness - anchors.margins + } + + PanelBg { + id: dashBg + + panel: panels.dashboard + deformAmount: 0.1 + } + + PanelBg { + id: launcherBg + + panel: panels.launcher + deformAmount: 0.1 + } + + PanelBg { + id: sessionBg + + panel: panels.sessionWrapper + deformAmount: 0.25 + x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth + implicitWidth: panels.session.width + } + + PanelBg { + id: sidebarBg + + panel: panels.sidebar + deformAmount: 0.05 + implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] + bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + } + + PanelBg { + id: osdBg + + panel: panels.osdWrapper + deformAmount: 0.3 + x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth + implicitWidth: panels.osd.width + } + + PanelBg { + id: notifsBg + + panel: panels.notifications + } + + PanelBg { + id: utilsBg + + panel: panels.utilities + deformAmount: panels.sidebar.visible ? 0.1 : 0.15 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] + topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + } + + PanelBg { + id: popoutBg + + panel: panels.popouts + x: bar.implicitWidth - (panels.popouts.isDetached ? -(root.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) + implicitWidth: panels.popouts.shownWidth + + Behavior on x { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on implicitWidth { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + } + } + + DrawerVisibilities { + id: visibilities + + Component.onCompleted: Visibilities.load(root.screen, this) + } + + Interactions { + screen: root.screen + popouts: panels.popouts + visibilities: visibilities + panels: panels + bar: bar + borderThickness: root.borderLayoutThickness + fullscreen: root.hasFullscreen + + Panels { + id: panels + + screen: root.screen + visibilities: visibilities + bar: bar + borderThickness: root.borderThickness + + utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 + + dashboard.transform: Matrix4x4 { + matrix: dashBg.deformMatrix + } + launcher.transform: Matrix4x4 { + matrix: launcherBg.deformMatrix + } + session.transform: Matrix4x4 { + matrix: sessionBg.deformMatrix + } + sidebar.transform: Matrix4x4 { + matrix: sidebarBg.deformMatrix + } + osd.transform: Matrix4x4 { + matrix: osdBg.deformMatrix + } + notifications.transform: Matrix4x4 { + matrix: notifsBg.deformMatrix + } + utilities.transform: Matrix4x4 { + matrix: utilsBg.deformMatrix + } + popouts.transform: Matrix4x4 { + matrix: popoutBg.deformMatrix + } + } + + BarWrapper { + id: bar + + anchors.top: parent.top + anchors.bottom: parent.bottom + + screen: root.screen + visibilities: visibilities + popouts: panels.popouts + + fullscreen: root.hasFullscreen + + Component.onCompleted: Visibilities.bars.set(root.screen, this) + } + } + + component PanelBg: BlobRect { + required property Item panel + property real deformAmount: 0.15 + + group: panel.width > 0 && panel.height > 0 ? blobGroup : null + x: panel.x + bar.implicitWidth + y: panel.y + root.borderThickness + implicitWidth: panel.width + implicitHeight: panel.height + radius: Config.border.rounding + deformScale: deformAmount / 10000 + } +} diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 0c801193a..ed886b31a 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -1,18 +1,9 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Controls -import QtQuick.Effects import Quickshell -import Quickshell.Hyprland -import Quickshell.Wayland -import Caelestia.Blobs -import qs.components -import qs.components.containers import qs.services import qs.config -import qs.utils -import qs.modules.bar Variants { model: Screens.screens @@ -21,348 +12,17 @@ Variants { id: scope required property ShellScreen modelData - readonly property bool barDisabled: Strings.testRegexList(Config.bar.excludedScreens, modelData.name) Exclusions { screen: scope.modelData - bar: bar + bar: content.bar borderThickness: Config.border.thickness } - StyledWindow { - id: win - - readonly property var monitor: Hypr.monitorFor(screen) - readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject?.specialWorkspace?.name.length ?? 0) > 0 - readonly property bool hasFullscreen: { - if (hasSpecialWorkspace) { - const specialName = monitor?.lastIpcObject?.specialWorkspace?.name; - if (!specialName) - return false; - const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); - return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; - } - return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; - } - property real borderThickness: hasFullscreen ? 0 : Config.border.thickness - readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness - property real borderRounding: hasFullscreen ? 0 : Config.border.rounding - property real shadowOpacity: hasFullscreen ? 0 : 0.7 - readonly property int dragMaskPadding: { - if (focusGrab.active || panels.popouts.isDetached) - return 0; - - const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace.lastIpcObject.windows > 0) - return 0; - - const thresholds = []; - for (const panel of ["dashboard", "launcher", "session", "sidebar"]) - if (Config[panel].enabled) - thresholds.push(Config[panel].dragThreshold); - return Math.max(...thresholds); - } - - onHasFullscreenChanged: { - visibilities.launcher = false; - visibilities.session = false; - visibilities.dashboard = false; - } + ContentWindow { + id: content screen: scope.modelData - name: "drawers" - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Overlay - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - mask: Regions { - bar: bar - panels: panels - win: win - } - - anchors.top: true - anchors.bottom: true - anchors.left: true - anchors.right: true - - Behavior on borderThickness { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on borderRounding { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on shadowOpacity { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - HyprlandFocusGrab { - id: focusGrab - - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) - windows: [win] - onCleared: { - visibilities.launcher = false; - visibilities.session = false; - visibilities.sidebar = false; - visibilities.dashboard = false; - panels.popouts.hasCurrent = false; - bar.closeTray(); - } - } - - StyledRect { - anchors.fill: parent - opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 - color: Colours.palette.m3scrim - - Behavior on opacity { - Anim {} - } - } - - Item { - anchors.fill: parent - opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity)) - } - - // Border { - // bar: bar - // } - - // Backgrounds { - // panels: panels - // bar: bar - // } - - BlobGroup { - id: blobGroup - - color: Colours.palette.m3surface - - Behavior on color { - CAnim {} - } - } - - BlobInvertedRect { - anchors.fill: parent - anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers - group: blobGroup - radius: win.borderRounding - borderLeft: bar.implicitWidth - anchors.margins - borderRight: win.borderThickness - anchors.margins - borderTop: win.borderThickness - anchors.margins - borderBottom: win.borderThickness - anchors.margins - } - - PanelBg { - id: dashBg - - blobGroup: blobGroup - panel: panels.dashboard - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.1 - } - - PanelBg { - id: launcherBg - - blobGroup: blobGroup - panel: panels.launcher - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.1 - } - - PanelBg { - id: sessionBg - - blobGroup: blobGroup - panel: panels.sessionWrapper - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.25 - x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth - implicitWidth: panels.session.width - } - - PanelBg { - id: sidebarBg - - blobGroup: blobGroup - panel: panels.sidebar - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.05 - implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 - exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] - bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius - } - - PanelBg { - id: osdBg - - blobGroup: blobGroup - panel: panels.osdWrapper - bar: bar - borderThickness: win.borderThickness - deformAmount: 0.3 - x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth - implicitWidth: panels.osd.width - } - - PanelBg { - id: notifsBg - - blobGroup: blobGroup - panel: panels.notifications - bar: bar - borderThickness: win.borderThickness - } - - PanelBg { - id: utilsBg - - blobGroup: blobGroup - panel: panels.utilities - bar: bar - borderThickness: win.borderThickness - deformAmount: panels.sidebar.visible ? 0.1 : 0.15 - exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] - topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius - } - - PanelBg { - id: popoutBg - - blobGroup: blobGroup - panel: panels.popouts - bar: bar - borderThickness: win.borderThickness - - x: bar.implicitWidth - (panels.popouts.isDetached ? -(win.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) - implicitWidth: panels.popouts.shownWidth - - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on implicitWidth { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - } - } - - DrawerVisibilities { - id: visibilities - - Component.onCompleted: Visibilities.load(scope.modelData, this) - } - - Interactions { - screen: scope.modelData - popouts: panels.popouts - visibilities: visibilities - panels: panels - bar: bar - borderThickness: win.borderLayoutThickness - fullscreen: win.hasFullscreen - - Panels { - id: panels - - screen: scope.modelData - visibilities: visibilities - bar: bar - borderThickness: win.borderThickness - - utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 - - dashboard.transform: Matrix4x4 { - matrix: dashBg.deformMatrix - } - launcher.transform: Matrix4x4 { - matrix: launcherBg.deformMatrix - } - session.transform: Matrix4x4 { - matrix: sessionBg.deformMatrix - } - sidebar.transform: Matrix4x4 { - matrix: sidebarBg.deformMatrix - } - osd.transform: Matrix4x4 { - matrix: osdBg.deformMatrix - } - notifications.transform: Matrix4x4 { - matrix: notifsBg.deformMatrix - } - utilities.transform: Matrix4x4 { - matrix: utilsBg.deformMatrix - } - popouts.transform: Matrix4x4 { - matrix: popoutBg.deformMatrix - } - } - - BarWrapper { - id: bar - - anchors.top: parent.top - anchors.bottom: parent.bottom - - screen: scope.modelData - visibilities: visibilities - popouts: panels.popouts - - disabled: scope.barDisabled - fullscreen: win.hasFullscreen - - Component.onCompleted: Visibilities.bars.set(scope.modelData, this) - } - } } } - - component PanelBg: BlobRect { - required property BlobGroup blobGroup - required property Item panel - required property Item bar - required property real borderThickness - property real deformAmount: 0.15 - - group: panel.width > 0 && panel.height > 0 ? blobGroup : null - x: panel.x + bar.implicitWidth - y: panel.y + borderThickness - implicitWidth: panel.width - implicitHeight: panel.height - radius: Config.border.rounding - deformScale: deformAmount / 10000 - } } From c126075d40d670c38ddb00898c70134e6e23c5bc Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 6 Apr 2026 23:41:23 +1000 Subject: [PATCH 233/409] chore: clean out legacy background code --- modules/bar/popouts/Background.qml | 73 ---------------------- modules/dashboard/Background.qml | 66 -------------------- modules/drawers/Backgrounds.qml | 91 ---------------------------- modules/drawers/Border.qml | 45 -------------- modules/drawers/ContentWindow.qml | 9 --- modules/launcher/Background.qml | 60 ------------------ modules/notifications/Background.qml | 53 ---------------- modules/osd/Background.qml | 59 ------------------ modules/session/Background.qml | 60 ------------------ modules/sidebar/Background.qml | 50 --------------- 10 files changed, 566 deletions(-) delete mode 100644 modules/bar/popouts/Background.qml delete mode 100644 modules/dashboard/Background.qml delete mode 100644 modules/drawers/Backgrounds.qml delete mode 100644 modules/drawers/Border.qml delete mode 100644 modules/launcher/Background.qml delete mode 100644 modules/notifications/Background.qml delete mode 100644 modules/osd/Background.qml delete mode 100644 modules/session/Background.qml delete mode 100644 modules/sidebar/Background.qml diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml deleted file mode 100644 index cfba86d3a..000000000 --- a/modules/bar/popouts/Background.qml +++ /dev/null @@ -1,73 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - required property bool invertBottomRounding - readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding - readonly property bool flatten: wrapper.width < rounding * 2 - readonly property real roundingX: flatten ? wrapper.width / 2 : rounding - property real ibr: invertBottomRounding ? -1 : 1 - - property real sideRounding: startX > 0 ? -1 : 1 - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: root.roundingX - relativeY: root.rounding * root.sideRounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.width - root.roundingX * 2 - relativeY: 0 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: -root.roundingX * root.ibr - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise - } - PathLine { - relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr) - relativeY: 0 - } - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding * root.sideRounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise - } - - Behavior on fillColor { - CAnim {} - } - - Behavior on ibr { - Anim {} - } - - Behavior on sideRounding { - Anim {} - } -} diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml deleted file mode 100644 index c6223eb6c..000000000 --- a/modules/dashboard/Background.qml +++ /dev/null @@ -1,66 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - readonly property real rounding: Config.border.rounding - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - - PathLine { - relativeX: root.wrapper.width - root.rounding * 2 - relativeY: 0 - } - - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - - PathLine { - relativeX: 0 - relativeY: -(root.wrapper.height - root.roundingY * 2) - } - - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml deleted file mode 100644 index 7592411c3..000000000 --- a/modules/drawers/Backgrounds.qml +++ /dev/null @@ -1,91 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.config -import qs.modules.dashboard as Dashboard -import qs.modules.launcher as Launcher -import qs.modules.notifications as Notifications -import qs.modules.osd as Osd -import qs.modules.session as Session -import qs.modules.sidebar as Sidebar -import qs.modules.utilities as Utilities -import qs.modules.bar.popouts as BarPopouts - -Shape { - id: root - - required property Panels panels - required property Item bar - required property real borderThickness - required property real borderRounding - - anchors.fill: parent - anchors.margins: root.borderThickness - anchors.leftMargin: bar.implicitWidth - preferredRendererType: Shape.CurveRenderer - - Osd.Background { - wrapper: root.panels.osd // qmllint disable incompatible-type - rounding: Config.border.rounding - - startX: root.width - root.panels.session.width - root.panels.sidebar.width - startY: (root.height - wrapper.height) / 2 - rounding - } - - Notifications.Background { - wrapper: root.panels.notifications // qmllint disable incompatible-type - sidebar: sidebar - rounding: Config.border.rounding - - startX: root.width - startY: 0 - } - - Session.Background { - wrapper: root.panels.session // qmllint disable incompatible-type - - startX: root.width - root.panels.sidebar.width - startY: (root.height - wrapper.height) / 2 - rounding - } - - Launcher.Background { - wrapper: root.panels.launcher // qmllint disable incompatible-type - - startX: (root.width - wrapper.width) / 2 - rounding - startY: root.height - } - - Dashboard.Background { - wrapper: root.panels.dashboard // qmllint disable incompatible-type - - startX: (root.width - wrapper.width) / 2 - rounding - startY: 0 - } - - BarPopouts.Background { - wrapper: root.panels.popouts // qmllint disable incompatible-type - invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height - - startX: wrapper.x - startY: wrapper.y - rounding * sideRounding - } - - Utilities.Background { - wrapper: root.panels.utilities // qmllint disable incompatible-type - sidebar: sidebar - rounding: root.borderRounding - - startX: root.width - startY: root.height - } - - Sidebar.Background { - id: sidebar - - wrapper: root.panels.sidebar // qmllint disable incompatible-type - panels: root.panels - rounding: root.borderRounding - - startX: root.width - startY: root.panels.notifications.height - } -} diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml deleted file mode 100644 index a638479df..000000000 --- a/modules/drawers/Border.qml +++ /dev/null @@ -1,45 +0,0 @@ -pragma ComponentBehavior: Bound - -import QtQuick -import QtQuick.Effects -import qs.components -import qs.services - -Item { - id: root - - required property Item bar - required property real borderThickness - required property real borderRounding - - anchors.fill: parent - - StyledRect { - anchors.fill: parent - color: Colours.palette.m3surface - - layer.enabled: true - layer.effect: MultiEffect { - maskSource: mask - maskEnabled: true - maskInverted: true - maskThresholdMin: 0.5 - maskSpreadAtMin: 1 - } - } - - Item { - id: mask - - anchors.fill: parent - layer.enabled: true - visible: false - - Rectangle { - anchors.fill: parent - anchors.margins: root.borderThickness - anchors.leftMargin: root.bar.implicitWidth - radius: root.borderRounding - } - } -} diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 7171c8c57..c161869a6 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -130,15 +130,6 @@ StyledWindow { shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity)) } - // Border { - // bar: bar - // } - - // Backgrounds { - // panels: panels - // bar: bar - // } - BlobGroup { id: blobGroup diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml deleted file mode 100644 index 508c75d4b..000000000 --- a/modules/launcher/Background.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - readonly property real rounding: Config.border.rounding - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: -(root.wrapper.height - root.roundingY * 2) - } - PathArc { - relativeX: root.rounding - relativeY: -root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: root.wrapper.width - root.rounding * 2 - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml deleted file mode 100644 index 740cda105..000000000 --- a/modules/notifications/Background.qml +++ /dev/null @@ -1,53 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services - -ShapePath { - id: root - - required property Wrapper wrapper - required property var sidebar - required property real rounding - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathLine { - relativeX: -(root.wrapper.width + root.rounding) - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.roundingY * 2 - } - PathArc { - relativeX: root.rounding - relativeY: root.roundingY - radiusX: root.rounding - radiusY: Math.min(root.rounding, root.wrapper.height) - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width - relativeY: 0 - } - PathArc { - relativeX: root.rounding - relativeY: root.rounding - radiusX: root.rounding - radiusY: root.rounding - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml deleted file mode 100644 index 330c703e7..000000000 --- a/modules/osd/Background.qml +++ /dev/null @@ -1,59 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services - -ShapePath { - id: root - - required property Wrapper wrapper - required property real rounding - readonly property bool flatten: wrapper.width < rounding * 2 - readonly property real roundingX: flatten ? wrapper.width / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - PathLine { - relativeX: -(root.wrapper.width - root.roundingX * 2) - relativeY: 0 - } - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.width - root.roundingX * 2 - relativeY: 0 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/session/Background.qml b/modules/session/Background.qml deleted file mode 100644 index a609f4601..000000000 --- a/modules/session/Background.qml +++ /dev/null @@ -1,60 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services -import qs.config - -ShapePath { - id: root - - required property Wrapper wrapper - readonly property real rounding: Config.border.rounding - readonly property bool flatten: wrapper.width < rounding * 2 - readonly property real roundingX: flatten ? wrapper.width / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - PathLine { - relativeX: -(root.wrapper.width - root.roundingX * 2) - relativeY: 0 - } - PathArc { - relativeX: -root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - direction: PathArc.Counterclockwise - } - PathLine { - relativeX: root.wrapper.width - root.roundingX * 2 - relativeY: 0 - } - PathArc { - relativeX: root.roundingX - relativeY: root.rounding - radiusX: Math.min(root.rounding, root.wrapper.width) - radiusY: root.rounding - } - - Behavior on fillColor { - CAnim {} - } -} diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml deleted file mode 100644 index c7d42123e..000000000 --- a/modules/sidebar/Background.qml +++ /dev/null @@ -1,50 +0,0 @@ -import QtQuick -import QtQuick.Shapes -import qs.components -import qs.services - -ShapePath { - id: root - - required property Wrapper wrapper - required property var panels - required property real rounding - - readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width - readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding - - readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width - readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding - - strokeWidth: -1 - fillColor: Colours.palette.m3surface - - PathLine { - relativeX: -root.wrapper.width - root.notifsRoundingX - relativeY: 0 - } - PathArc { - relativeX: root.notifsRoundingX - relativeY: root.rounding - radiusX: root.notifsRoundingX - radiusY: root.rounding - } - PathLine { - relativeX: 0 - relativeY: root.wrapper.height - root.rounding * 2 - } - PathArc { - relativeX: -root.utilsRoundingX - relativeY: root.rounding - radiusX: root.utilsRoundingX - radiusY: root.rounding - } - PathLine { - relativeX: root.wrapper.width + root.utilsRoundingX - relativeY: 0 - } - - Behavior on fillColor { - CAnim {} - } -} From ed33b3d8fffd01454f24c76d3daaaaf55f442dee Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 03:13:07 +1000 Subject: [PATCH 234/409] dev: update direnv watch paths --- .envrc | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.envrc b/.envrc index c90b500c9..5a259560a 100644 --- a/.envrc +++ b/.envrc @@ -3,10 +3,11 @@ if has nix; then fi shopt -s globstar -watch_file assets/cpp/**/*.cpp -watch_file assets/cpp/**/*.hpp watch_file plugin/**/*.cpp watch_file plugin/**/*.hpp +watch_file plugin/**/*.qml +watch_file plugin/**/*.vert +watch_file plugin/**/*.frag watch_file **/CMakeLists.txt cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv From 31e29c16a8cc1d25d326a968b768f1ebfef6c334 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:01:41 +1000 Subject: [PATCH 235/409] fix: blob sdf rect-rect edge sink --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 90 ++++++++++---------- 1 file changed, 43 insertions(+), 47 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index 808cf3091..fd412e1ca 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -143,60 +143,56 @@ void main() { d *= scale; } - // Rect-to-rect edge sink: indent this rect's edge where another - // rect is slightly past it, fading once past threshold. - if (rectCount > 1) { - vec2 iSh = sh.xy; - float sinkT = smoothFactor * 0.75; - float sinkOff = smoothFactor * (1.0 / 6.0); - float rectSinkVal = 0.0; + // Rect-to-rect edge sinks: track the same edge of neighboring rects + { + float rectSinkValue = 0.0; + vec2 iHalf = sh.xy; + float preOff = smoothFactor * (1.0/6.0); for (int j = 0; j < rectCount; j++) { if (j == i) continue; - vec4 jR = rectData[j * 5]; - vec4 jP = rectData[j * 5 + 1]; + vec4 jRect = rectData[j * 5]; + vec4 jProps = rectData[j * 5 + 1]; vec2 jSh = rectData[j * 5 + 3].xy; - vec2 jC = jR.xy + jP.yz; - - // Skip non-adjacent rects - float sinkRange = smoothFactor * 1.5; - if (abs(center.x - jC.x) > iSh.x + jSh.x + sinkRange || - abs(center.y - jC.y) > iSh.y + jSh.y + sinkRange) - continue; - - // Penetration of j past i's edges (positive = past) - float pT = (jC.y + jSh.y) - (center.y - iSh.y) - sinkOff; - float pB = (center.y + iSh.y) - (jC.y - jSh.y) - sinkOff; - float pL = (jC.x + jSh.x) - (center.x - iSh.x) - sinkOff; - float pR = (center.x + iSh.x) - (jC.x - jSh.x) - sinkOff; - - // Smooth bump: rises then falls, zero outside [0, sinkT] - float aT = smoothstep(0.0, sinkT * 0.4, pT) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pT)); - float aB = smoothstep(0.0, sinkT * 0.4, pB) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pB)); - float aL = smoothstep(0.0, sinkT * 0.4, pL) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pL)); - float aR = smoothstep(0.0, sinkT * 0.4, pR) * (1.0 - smoothstep(sinkT * 0.5, sinkT, pR)); - - // Lateral falloff from rect j's extent - float hLat = max(abs(pixel.x - jC.x) - jSh.x, 0.0); - float vLat = max(abs(pixel.y - jC.y) - jSh.y, 0.0); - float latF = smoothFactor * 2.0; - - // Perpendicular zone (near rect i's edge only) - float zT = 1.0 - smoothstep(center.y - iSh.y, center.y - iSh.y + smoothFactor, pixel.y); - float zB = smoothstep(center.y + iSh.y - smoothFactor, center.y + iSh.y, pixel.y); - float zL = 1.0 - smoothstep(center.x - iSh.x, center.x - iSh.x + smoothFactor, pixel.x); - float zR = smoothstep(center.x + iSh.x - smoothFactor, center.x + iSh.x, pixel.x); - - float s = max( - max(aT * smoothstep(latF, 0.0, hLat) * zT, - aB * smoothstep(latF, 0.0, hLat) * zB), - max(aL * smoothstep(latF, 0.0, vLat) * zL, - aR * smoothstep(latF, 0.0, vLat) * zR) + vec2 jCtr = jRect.xy + jProps.yz; + + // Per-edge containment: the other rect's full span on the + // perpendicular axis must be inside this rect for that edge. + bool hInside = (jCtr.y - jSh.y) >= (center.y - iHalf.y) && + (jCtr.y + jSh.y) <= (center.y + iHalf.y); + bool vInside = (jCtr.x - jSh.x) >= (center.x - iHalf.x) && + (jCtr.x + jSh.x) <= (center.x + iHalf.x); + + // Top/Bottom: other rect's height must be inside this rect + float topPen = hInside ? clamp((center.y - iHalf.y) - (jCtr.y - jSh.y) - preOff, 0.0, smoothFactor) : 0.0; + float botPen = hInside ? clamp((jCtr.y + jSh.y) - (center.y + iHalf.y) - preOff, 0.0, smoothFactor) : 0.0; + + // Left/Right: other rect's width must be inside this rect + float leftPen = vInside ? clamp((center.x - iHalf.x) - (jCtr.x - jSh.x) - preOff, 0.0, smoothFactor) : 0.0; + float rightPen = vInside ? clamp((jCtr.x + jSh.x) - (center.x + iHalf.x) - preOff, 0.0, smoothFactor) : 0.0; + + // Lateral distance from pixel to other rect's extent along each edge + float hLat = max(abs(pixel.x - jCtr.x) - jSh.x, 0.0); + float vLat = max(abs(pixel.y - jCtr.y) - jSh.y, 0.0); + + // Perpendicular proximity: full strength at edge, fade inside + float topZone = 1.0 - smoothstep(center.y - iHalf.y, center.y - iHalf.y + smoothFactor, pixel.y); + float botZone = smoothstep(center.y + iHalf.y - smoothFactor, center.y + iHalf.y, pixel.y); + float leftZone = 1.0 - smoothstep(center.x - iHalf.x, center.x - iHalf.x + smoothFactor, pixel.x); + float rightZone = smoothstep(center.x + iHalf.x - smoothFactor, center.x + iHalf.x, pixel.x); + + float s = smoothFactor * 2.0; + float sink = max( + max(topPen * smoothstep(s, 0.0, hLat) * topZone, + botPen * smoothstep(s, 0.0, hLat) * botZone), + max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, + rightPen * smoothstep(s, 0.0, vLat) * rightZone) ); - rectSinkVal = max(rectSinkVal, s); + rectSinkValue = max(rectSinkValue, sink); } - d += rectSinkVal * smoothFactor * 0.25; + + d -= rectSinkValue; } mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); From 9ce0224d0adf89ca05f540122a4f65d6a17fc46c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:02:17 +1000 Subject: [PATCH 236/409] fix: reduce deform amount --- modules/drawers/ContentWindow.qml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index c161869a6..cc09bbbd8 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -169,7 +169,7 @@ StyledWindow { id: sessionBg panel: panels.sessionWrapper - deformAmount: 0.25 + deformAmount: 0.2 x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth implicitWidth: panels.session.width } @@ -178,7 +178,7 @@ StyledWindow { id: sidebarBg panel: panels.sidebar - deformAmount: 0.05 + deformAmount: 0.03 implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius @@ -188,7 +188,7 @@ StyledWindow { id: osdBg panel: panels.osdWrapper - deformAmount: 0.3 + deformAmount: 0.25 x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth implicitWidth: panels.osd.width } From d778734c679404595377959fd1885052f4cd880d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 17:14:50 +1000 Subject: [PATCH 237/409] fix: remove sminNoBulge cause useless --- plugin/src/Caelestia/Blobs/shaders/blob.frag | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index fd412e1ca..e7a33496a 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -46,14 +46,6 @@ float smin(float a, float b, float k) { return min(a, b) - h * h * h * k * (1.0/6.0); } -float sminNoBulge(float a, float b, float k) { - // Cubic smooth min with reduced outward expansion when shapes overlap - float h = max(k - abs(a - b), 0.0) / k; - float blend = h * h * h * k * (1.0/6.0); - blend *= smoothstep(-k, 0.0, min(a, b)); - return min(a, b) - blend; -} - float smax(float a, float b, float k) { float h = max(k - abs(a - b), 0.0) / k; return max(a, b) + h * h * h * k * (1.0/6.0); @@ -195,7 +187,7 @@ void main() { d -= rectSinkValue; } - mergedSdf = sminNoBulge(mergedSdf, d, smoothFactor); + mergedSdf = smin(mergedSdf, d, smoothFactor); if (d < smoothFactor && d < minDist) { minDist = d; owner = i; From 77ab4b835013b93bce50805894aef089156773d3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 18:39:48 +1000 Subject: [PATCH 238/409] fix: blobs ignoring updates if moving too slow --- plugin/src/Caelestia/Blobs/blobshape.cpp | 11 +++++++---- plugin/src/Caelestia/Blobs/blobshape.hpp | 2 ++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index 966621500..718c03e7f 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -69,13 +69,16 @@ void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeome QQuickItem::geometryChange(newGeometry, oldGeometry); updateCenteredDeformMatrix(); if (m_group) { - // Only trigger redraw if the change is visually meaningful - const auto dx = std::abs(newGeometry.x() - oldGeometry.x()); - const auto dy = std::abs(newGeometry.y() - oldGeometry.y()); + // Accumulate sub-pixel drift so slow movements don't desync the shader + m_pendingDx += static_cast(newGeometry.x() - oldGeometry.x()); + m_pendingDy += static_cast(newGeometry.y() - oldGeometry.y()); const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); - if (dx > 0.5 || dy > 0.5 || dw > 0.5 || dh > 0.5) + if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) { + m_pendingDx = 0; + m_pendingDy = 0; m_group->markShapeDirty(this); + } } } diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp index c9d985040..c05a40d86 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.hpp +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -70,6 +70,8 @@ class BlobShape : public QQuickItem { QRectF m_localPaddedRect; QVector m_cachedRects; int m_cachedMyIndex = -2; + float m_pendingDx = 0; + float m_pendingDy = 0; bool m_cachedHasInverted = false; float m_cachedInvertedRadius = 0; float m_cachedInvertedOuter[4] = {}; From 293b6f4b2bdc3e43dd4323c7d547612d13b60163 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Tue, 7 Apr 2026 22:00:38 +1000 Subject: [PATCH 239/409] feat: expressive effects -> slow spatial We don't use the effect curve anyways, standard is better --- config/AppearanceConfig.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/config/AppearanceConfig.qml b/config/AppearanceConfig.qml index 3d590dca2..cee140f66 100644 --- a/config/AppearanceConfig.qml +++ b/config/AppearanceConfig.qml @@ -65,7 +65,7 @@ JsonObject { property list standardDecel: [0, 0, 0, 1, 1, 1] property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] - property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] + property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] } component AnimDurations: JsonObject { @@ -76,7 +76,7 @@ JsonObject { property int extraLarge: 1000 * scale property int expressiveFastSpatial: 350 * scale property int expressiveDefaultSpatial: 500 * scale - property int expressiveEffects: 200 * scale + property int expressiveSlowSpatial: 650 * scale } component Anim: JsonObject { From df27c93224f3577b9d9e0e31d5ba1e662c499a90 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:06:37 +1000 Subject: [PATCH 240/409] feat: improve popouts Fix bg tracking, use slow spatial for detach, default spatial for everything else, adjust deform, move instead of width scale --- modules/bar/popouts/ClipWrapper.qml | 55 +++++++++++++++++++ modules/bar/popouts/Wrapper.qml | 70 +++++++++---------------- modules/controlcenter/ControlCenter.qml | 3 +- modules/controlcenter/WindowFactory.qml | 5 +- modules/drawers/ContentWindow.qml | 21 ++------ modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 21 ++++---- modules/drawers/Regions.qml | 3 +- 8 files changed, 101 insertions(+), 79 deletions(-) create mode 100644 modules/bar/popouts/ClipWrapper.qml diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml new file mode 100644 index 000000000..74dec8416 --- /dev/null +++ b/modules/bar/popouts/ClipWrapper.qml @@ -0,0 +1,55 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.components +import qs.config + +Item { + id: root + + required property ShellScreen screen + + readonly property alias content: content + property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 + + visible: width > 0 && height > 0 + clip: true + + implicitWidth: content.implicitWidth * (1 - offsetScale) + implicitHeight: content.implicitHeight + + Behavior on offsetScale { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on x { + Anim { + duration: content.animLength + easing.bezierCurve: content.animCurve + } + } + + Behavior on y { + enabled: root.offsetScale < 1 + + Anim { + duration: content.animLength + easing.bezierCurve: content.animCurve + } + } + + Wrapper { + id: content + + screen: root.screen + offsetScale: root.offsetScale + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale + } +} diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 99440865c..a04a5e8ae 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -14,46 +14,50 @@ Item { id: root required property ShellScreen screen + required property real offsetScale - readonly property real shownWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth - readonly property real nonAnimWidth: x > 0 || hasCurrent ? shownWidth : 0 + readonly property alias content: content + readonly property alias winfo: winfo + readonly property alias controlCenter: controlCenter + + readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight readonly property Item current: (content.item as Content)?.current ?? null + readonly property bool isDetached: detachedMode.length > 0 property alias currentName: popoutState.currentName - property real currentCenter property alias hasCurrent: popoutState.hasCurrent - readonly property PopoutState popState: popoutState + property real currentCenter property string detachedMode property string queuedMode - readonly property bool isDetached: detachedMode.length > 0 - property int animLength: Appearance.anim.durations.normal - property list animCurve: Appearance.anim.curves.emphasized + property int animLength: Appearance.anim.durations.expressiveDefaultSpatial + property list animCurve: Appearance.anim.curves.expressiveDefaultSpatial + + function setAnims(detach: bool): void { + const type = `expressive${detach ? "Slow" : "Default"}Spatial`; + animLength = Appearance.anim.durations[type]; + animCurve = Appearance.anim.curves[type]; + } function detach(mode: string): void { - animLength = Appearance.anim.durations.large; + setAnims(true); if (mode === "winfo") { detachedMode = mode; } else { queuedMode = mode; detachedMode = "any"; } + setAnims(false); focus = true; } function close(): void { hasCurrent = false; - animCurve = Appearance.anim.curves.emphasizedAccel; - animLength = Appearance.anim.durations.normal; detachedMode = ""; - animCurve = Appearance.anim.curves.emphasized; } - visible: width > 0 && height > 0 - clip: true - implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight @@ -90,15 +94,7 @@ Item { } Binding { - when: root.isDetached - - target: QsWindow.window - property: "WlrLayershell.keyboardFocus" - value: WlrKeyboardFocus.OnDemand - } - - Binding { - when: root.hasCurrent && root.currentName === "wirelesspassword" + when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword") target: QsWindow.window property: "WlrLayershell.keyboardFocus" @@ -118,6 +114,8 @@ Item { } Comp { + id: winfo + shouldBeActive: root.detachedMode === "winfo" anchors.centerIn: parent @@ -128,32 +126,15 @@ Item { } Comp { + id: controlCenter + shouldBeActive: root.detachedMode === "any" anchors.centerIn: parent sourceComponent: ControlCenter { - function close(): void { - root.close(); - } - screen: root.screen active: root.queuedMode - } - } - - Behavior on x { - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve - } - } - - Behavior on y { - enabled: root.implicitWidth > 0 - - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve + onClose: root.close() } } @@ -165,7 +146,7 @@ Item { } Behavior on implicitHeight { - enabled: root.implicitWidth > 0 + enabled: root.offsetScale < 1 Anim { duration: root.animLength @@ -181,6 +162,7 @@ Item { active: false opacity: 0 + // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set states: State { name: "active" when: comp.shouldBeActive diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index f542b5970..5b067fc0d 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -25,8 +25,7 @@ Item { root: root } - function close(): void { - } + signal close implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio implicitHeight: screen.height * Config.controlCenter.sizes.heightMult diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index 266af9095..dc0dc4a07 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -45,12 +45,9 @@ Singleton { ControlCenter { id: cc - function close(): void { - win.destroy(); - } - anchors.fill: parent screen: win.screen + onClose: win.destroy() floating: true } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index cc09bbbd8..f914be2d0 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -211,23 +211,10 @@ StyledWindow { PanelBg { id: popoutBg - panel: panels.popouts - x: bar.implicitWidth - (panels.popouts.isDetached ? -(root.width - panels.popouts.shownWidth) / 2 : panels.popouts.hasCurrent ? 0 : panels.popouts.shownWidth + 5) - implicitWidth: panels.popouts.shownWidth - - Behavior on x { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on implicitWidth { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } + panel: panels.popoutsWrapper + deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 + x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth + implicitWidth: panels.popouts.width } } diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index aeabf4018..ad17a0f2f 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -211,7 +211,7 @@ CustomMouseArea { // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); - } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { + } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) { popouts.hasCurrent = false; bar.closeTray(); } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index b018b94d0..3cfabaab8 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -28,13 +28,14 @@ Item { readonly property alias sessionWrapper: sessionWrapper readonly property alias launcher: launcher readonly property alias dashboard: dashboard - readonly property alias popouts: popouts + readonly property alias popouts: popoutsWrapper.content + readonly property alias popoutsWrapper: popoutsWrapper readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar anchors.fill: parent - anchors.margins: root.borderThickness + anchors.margins: borderThickness anchors.leftMargin: bar.implicitWidth Item { @@ -114,18 +115,18 @@ Item { anchors.top: parent.top } - BarPopouts.Wrapper { - id: popouts + BarPopouts.ClipWrapper { + id: popoutsWrapper screen: root.screen - x: isDetached ? (root.width - nonAnimWidth) / 2 : 0 + x: content.isDetached ? (root.width - content.nonAnimWidth) / 2 : 0 y: { - if (isDetached) - return (root.height - nonAnimHeight) / 2; + if (content.isDetached) + return (root.height - content.nonAnimHeight) / 2; - const off = currentCenter - root.borderThickness - nonAnimHeight / 2; - const diff = root.height - Math.floor(off + nonAnimHeight); + const off = content.currentCenter - root.borderThickness - content.nonAnimHeight / 2; + const diff = root.height - Math.floor(off + content.nonAnimHeight); if (diff < 0) return off + diff; return Math.max(off, 0); @@ -137,7 +138,7 @@ Item { visibilities: root.visibilities sidebar: sidebar - popouts: popouts + popouts: popoutsWrapper.content anchors.bottom: parent.bottom anchors.right: parent.right diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml index c646809fd..d42817624 100644 --- a/modules/drawers/Regions.qml +++ b/modules/drawers/Regions.qml @@ -65,7 +65,8 @@ Region { } R { - panel: root.panels.popouts + panel: root.panels.popoutsWrapper + width: panel.width * (1 - root.panels.popoutsWrapper.offsetScale) } component R: Region { From ec4a15c1d17853064228ac228f7b336d805bfd84 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:06:49 +1000 Subject: [PATCH 241/409] fix: undef warning in winfo --- modules/windowinfo/Buttons.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 15f71956c..3f0bf4de3 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -120,7 +120,7 @@ ColumnLayout { Loader { asynchronous: true - active: root.client?.lastIpcObject.floating + active: root.client?.lastIpcObject.floating ?? false Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing Layout.rightMargin: active ? 0 : -parent.spacing From ff5c46a4294e1bd99809635797d9c763c191f1f5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:07:35 +1000 Subject: [PATCH 242/409] fix: don't exclude panels from group based on size Most panels do not change size either way, and doing this removes the close sink effect --- modules/drawers/ContentWindow.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index f914be2d0..113a3fea8 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -289,7 +289,7 @@ StyledWindow { required property Item panel property real deformAmount: 0.15 - group: panel.width > 0 && panel.height > 0 ? blobGroup : null + group: blobGroup x: panel.x + bar.implicitWidth y: panel.y + root.borderThickness implicitWidth: panel.width From b1380235ebe549865b235b1b3433c4a00d16ea8b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 17:20:58 +1000 Subject: [PATCH 243/409] fix: prevent deform gap between bar and popouts --- modules/drawers/ContentWindow.qml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 113a3fea8..ff0e847aa 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -211,10 +211,20 @@ StyledWindow { PanelBg { id: popoutBg + // Extra width to prevent vertical movement deformation partially detaching panel from bar + property real extraWidth: panels.popouts.isDetached ? 0 : 0.2 + panel: panels.popoutsWrapper deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 - x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - implicitWidth: panels.popouts.width + x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth + implicitWidth: panels.popouts.width * (1 + extraWidth) + + Behavior on extraWidth { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } } } From eaa6aefa6c7b9f526829a62d7357bf1b1724b48a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 18:44:38 +1000 Subject: [PATCH 244/409] fix: popout transition anim --- modules/bar/popouts/Content.qml | 5 +---- modules/bar/popouts/Wrapper.qml | 3 +-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 1c4792e1e..71a8c5ebf 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -14,8 +14,6 @@ Item { readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null - anchors.centerIn: parent - implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2 implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2 @@ -171,8 +169,7 @@ Item { required property string name readonly property bool shouldBeActive: root.popouts.currentName === name - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right + anchors.centerIn: parent opacity: 0 scale: 0.8 diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index a04a5e8ae..04212e28f 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -105,8 +105,7 @@ Item { id: content shouldBeActive: root.hasCurrent && !root.detachedMode - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent sourceComponent: Content { popouts: popoutState From e1e0c314c656e0f8d623a81b4999876c5bcc593b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:35:57 +1000 Subject: [PATCH 245/409] refactor: move popout position logic to wrapper --- modules/bar/popouts/ClipWrapper.qml | 13 +++++++++++++ modules/drawers/Panels.qml | 13 +------------ 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index 74dec8416..a69dff124 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -9,6 +9,7 @@ Item { id: root required property ShellScreen screen + required property real borderThickness readonly property alias content: content property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 @@ -19,6 +20,18 @@ Item { implicitWidth: content.implicitWidth * (1 - offsetScale) implicitHeight: content.implicitHeight + x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0 + y: { + if (content.isDetached) + return (parent.height - content.nonAnimHeight) / 2; + + const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2; + const diff = parent.height - Math.floor(off + content.nonAnimHeight); + if (diff < 0) + return off + diff; + return Math.max(off, 0); + } + Behavior on offsetScale { Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 3cfabaab8..0d5674144 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -119,18 +119,7 @@ Item { id: popoutsWrapper screen: root.screen - - x: content.isDetached ? (root.width - content.nonAnimWidth) / 2 : 0 - y: { - if (content.isDetached) - return (root.height - content.nonAnimHeight) / 2; - - const off = content.currentCenter - root.borderThickness - content.nonAnimHeight / 2; - const diff = root.height - Math.floor(off + content.nonAnimHeight); - if (diff < 0) - return off + diff; - return Math.max(off, 0); - } + borderThickness: root.borderThickness } Utilities.Wrapper { From 7f0acf15b3afebe05f866a3c253354c6ce9492c5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 8 Apr 2026 19:42:11 +1000 Subject: [PATCH 246/409] chore: fix linter warnings --- modules/bar/popouts/ClipWrapper.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index a69dff124..bf2a00493 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import qs.components import qs.config +import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others Item { id: root From c623b9b688b8e8ef557af4f01c5801ceafd61460 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 00:34:26 +1000 Subject: [PATCH 247/409] fix: osd and session bulges Also remove rect-rect edge sink cause it doesn't work --- modules/osd/Wrapper.qml | 3 +- modules/session/Wrapper.qml | 3 +- plugin/src/Caelestia/Blobs/shaders/blob.frag | 52 -------------------- 3 files changed, 4 insertions(+), 54 deletions(-) diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 585fb959d..487b611fc 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -17,6 +17,7 @@ Item { readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0 property real volume property bool muted @@ -38,7 +39,7 @@ Item { } visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5) * offsetScale + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight opacity: 1 - offsetScale diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index e48fff606..0d2d3b42a 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -13,9 +13,10 @@ Item { readonly property bool shouldBeActive: visibilities.session && Config.session.enabled property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: sidebarVisible ? 14 : 0 visible: offsetScale < 1 - anchors.rightMargin: (-implicitWidth - 5) * offsetScale + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open opacity: 1 - offsetScale diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index e7a33496a..e78531b65 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -135,58 +135,6 @@ void main() { d *= scale; } - // Rect-to-rect edge sinks: track the same edge of neighboring rects - { - float rectSinkValue = 0.0; - vec2 iHalf = sh.xy; - float preOff = smoothFactor * (1.0/6.0); - - for (int j = 0; j < rectCount; j++) { - if (j == i) continue; - - vec4 jRect = rectData[j * 5]; - vec4 jProps = rectData[j * 5 + 1]; - vec2 jSh = rectData[j * 5 + 3].xy; - vec2 jCtr = jRect.xy + jProps.yz; - - // Per-edge containment: the other rect's full span on the - // perpendicular axis must be inside this rect for that edge. - bool hInside = (jCtr.y - jSh.y) >= (center.y - iHalf.y) && - (jCtr.y + jSh.y) <= (center.y + iHalf.y); - bool vInside = (jCtr.x - jSh.x) >= (center.x - iHalf.x) && - (jCtr.x + jSh.x) <= (center.x + iHalf.x); - - // Top/Bottom: other rect's height must be inside this rect - float topPen = hInside ? clamp((center.y - iHalf.y) - (jCtr.y - jSh.y) - preOff, 0.0, smoothFactor) : 0.0; - float botPen = hInside ? clamp((jCtr.y + jSh.y) - (center.y + iHalf.y) - preOff, 0.0, smoothFactor) : 0.0; - - // Left/Right: other rect's width must be inside this rect - float leftPen = vInside ? clamp((center.x - iHalf.x) - (jCtr.x - jSh.x) - preOff, 0.0, smoothFactor) : 0.0; - float rightPen = vInside ? clamp((jCtr.x + jSh.x) - (center.x + iHalf.x) - preOff, 0.0, smoothFactor) : 0.0; - - // Lateral distance from pixel to other rect's extent along each edge - float hLat = max(abs(pixel.x - jCtr.x) - jSh.x, 0.0); - float vLat = max(abs(pixel.y - jCtr.y) - jSh.y, 0.0); - - // Perpendicular proximity: full strength at edge, fade inside - float topZone = 1.0 - smoothstep(center.y - iHalf.y, center.y - iHalf.y + smoothFactor, pixel.y); - float botZone = smoothstep(center.y + iHalf.y - smoothFactor, center.y + iHalf.y, pixel.y); - float leftZone = 1.0 - smoothstep(center.x - iHalf.x, center.x - iHalf.x + smoothFactor, pixel.x); - float rightZone = smoothstep(center.x + iHalf.x - smoothFactor, center.x + iHalf.x, pixel.x); - - float s = smoothFactor * 2.0; - float sink = max( - max(topPen * smoothstep(s, 0.0, hLat) * topZone, - botPen * smoothstep(s, 0.0, hLat) * botZone), - max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, - rightPen * smoothstep(s, 0.0, vLat) * rightZone) - ); - rectSinkValue = max(rectSinkValue, sink); - } - - d -= rectSinkValue; - } - mergedSdf = smin(mergedSdf, d, smoothFactor); if (d < smoothFactor && d < minDist) { minDist = d; From c7a943b1c779872309380d1d968fd252c17544b5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 16:14:36 +1000 Subject: [PATCH 248/409] fix: close notif group notifs in batches --- modules/sidebar/NotifActionList.qml | 2 +- modules/sidebar/NotifDockList.qml | 29 +++++++++++++++++++---------- 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index dfc83bd2f..84e6103c3 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -150,7 +150,7 @@ Item { id: actionInner anchors.centerIn: parent - sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp + sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp } Component { diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index a799cf1ae..11dde2912 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -62,16 +62,7 @@ LazyListView { property int startY function closeAll(): void { - for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) - n.close(); - } - - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } + clearTimer.start(); } LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight @@ -111,6 +102,24 @@ LazyListView { closeAll(); } + Timer { + id: clearTimer + + interval: 15 + repeat: true + triggeredOnStart: true + onTriggered: { + const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData); + if (notifs.length === 0) { + stop(); + return; + } + + for (const n of notifs.slice(0, 30)) + n.close(); + } + } + NotifGroup { id: notifInner From 06823973844090ea13a15b6139c40c1089b1d93f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:36:05 +1000 Subject: [PATCH 249/409] fix: watch transform for changes + async dock list --- modules/sidebar/NotifDockList.qml | 1 + modules/sidebar/NotifGroup.qml | 2 +- modules/sidebar/NotifGroupList.qml | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 11dde2912..3c6a53981 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -20,6 +20,7 @@ LazyListView { spacing: Appearance.spacing.small cacheBuffer: 200 + asynchronous: true useCustomViewport: true viewport: Qt.rect(0, container.contentY, width, container.height) diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index f1d1502f9..0efea5427 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -52,7 +52,7 @@ StyledRect { readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0); - const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; + const columnHeight = headerHeight + notifList.layoutHeight + column.Layout.topMargin + column.Layout.bottomMargin; return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index a54a13b63..a44016972 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -17,8 +17,6 @@ LazyListView { required property Flickable container required property DrawerVisibilities visibilities - readonly property real nonAnimHeight: layoutHeight - signal requestToggleExpand(expand: bool) Layout.fillWidth: true @@ -27,10 +25,14 @@ LazyListView { spacing: Math.round(Appearance.spacing.small / 2) asynchronous: true + cacheBuffer: 200 removeDuration: Appearance.anim.durations.normal useCustomViewport: true - viewport: Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height) + viewport: { + tWatcher.transform; // mapToItem is not reactive so use this to trigger updates + return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height); + } model: ScriptModel { values: root.expanded ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) @@ -145,4 +147,11 @@ LazyListView { } } } + + TransformWatcher { + id: tWatcher + + a: root.container.contentItem + b: root + } } From 3f72dde918a1180adb23ff4e0041fe3f149d8aaa Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:49:03 +1000 Subject: [PATCH 250/409] fix: lazy list view overshoot --- plugin/src/Caelestia/Components/lazylistview.cpp | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index d05ca095e..14fb4bb85 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -492,10 +492,22 @@ void LazyListView::relayout() { } QRectF LazyListView::effectiveViewport() const { + QRectF vp; if (m_useCustomViewport) - return m_viewport.adjusted(0, -m_cacheBuffer, 0, m_cacheBuffer); + vp = m_viewport; + else + vp = QRectF(0, m_contentY, width(), height()); + + // During Flickable overshoot the viewport can extend entirely beyond content bounds, + // causing all delegates to be culled. Clamp so it always overlaps [0, layoutHeight]. + if (m_layoutHeight > 0) { + const qreal top = std::min(vp.y(), m_layoutHeight); + const qreal bottom = std::max(vp.y() + vp.height(), 0.0); + if (bottom > top) + vp = QRectF(vp.x(), top, vp.width(), bottom - top); + } - return QRectF(0, m_contentY - m_cacheBuffer, width(), height() + 2 * m_cacheBuffer); + return vp.adjusted(0, -m_cacheBuffer, 0, m_cacheBuffer); } std::pair LazyListView::computeVisibleRange() const { From 1223b5338bd8d97c31bf02e60bb6ff65792ad371 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:10:34 +1000 Subject: [PATCH 251/409] fix: prevent cacheBuffer from extending viewport over height --- plugin/src/Caelestia/Components/lazylistview.cpp | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 14fb4bb85..edcef17d5 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -507,7 +507,19 @@ QRectF LazyListView::effectiveViewport() const { vp = QRectF(vp.x(), top, vp.width(), bottom - top); } - return vp.adjusted(0, -m_cacheBuffer, 0, m_cacheBuffer); + vp.adjust(0, -m_cacheBuffer, 0, m_cacheBuffer); + + // Trim the cache-buffered viewport to [0, layoutHeight]. No items exist outside + // those bounds, so extending past them wastes budget and can cause edge thrashing + // when a large cache buffer reaches the opposite end of the content. + if (m_layoutHeight > 0) { + const qreal top = std::max(vp.y(), 0.0); + const qreal bottom = std::min(vp.y() + vp.height(), m_layoutHeight); + if (top < bottom) + vp = QRectF(vp.x(), top, vp.width(), bottom - top); + } + + return vp; } std::pair LazyListView::computeVisibleRange() const { From 82f177fa3fe0aca87a91c711a325b97be3bd1285 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:11:46 +1000 Subject: [PATCH 252/409] fix: increase cache buffers to reduce visual glitches Also set initial notif state and notif content non async --- modules/sidebar/Notif.qml | 4 ++-- modules/sidebar/NotifDockList.qml | 2 +- modules/sidebar/NotifGroupList.qml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index d646c4f01..e23460f11 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -26,9 +26,10 @@ StyledRect { return expanded ? c : Qt.alpha(c, 0); } + state: expanded ? "expanded" : "" + states: State { name: "expanded" - when: root.expanded PropertyChanges { summary.anchors.margins: Appearance.padding.normal @@ -156,7 +157,6 @@ StyledRect { component WrappedLoader: Loader { required property bool shouldBeActive - asynchronous: true opacity: shouldBeActive ? 1 : 0 active: opacity > 0 diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 3c6a53981..ef60c4f81 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -19,7 +19,7 @@ LazyListView { implicitHeight: contentHeight spacing: Appearance.spacing.small - cacheBuffer: 200 + cacheBuffer: 400 asynchronous: true useCustomViewport: true diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index a44016972..78c6ce59a 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -25,7 +25,7 @@ LazyListView { spacing: Math.round(Appearance.spacing.small / 2) asynchronous: true - cacheBuffer: 200 + cacheBuffer: 800 removeDuration: Appearance.anim.durations.normal useCustomViewport: true From 8fbf85da86efc1607807304c11ef4cd604b2a533 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 19:52:54 +1000 Subject: [PATCH 253/409] fix: notif list viewport jumping around on delegate creation --- modules/sidebar/NotifDockList.qml | 2 ++ .../src/Caelestia/Components/lazylistview.cpp | 33 +++++++++++++++++++ .../src/Caelestia/Components/lazylistview.hpp | 7 ++++ 3 files changed, 42 insertions(+) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index ef60c4f81..4e2ae6e8d 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -22,6 +22,7 @@ LazyListView { cacheBuffer: 400 asynchronous: true + onViewportAdjustNeeded: d => container.contentY += d useCustomViewport: true viewport: Qt.rect(0, container.contentY, width, container.height) @@ -66,6 +67,7 @@ LazyListView { clearTimer.start(); } + LazyListView.trackViewport: notifInner.expanded || notifInner.nonAnimHeight < notifInner.implicitHeight LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight LazyListView.visibleHeight: notifInner.implicitHeight implicitHeight: notifInner.implicitHeight diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index edcef17d5..c35dbb6be 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -62,6 +62,17 @@ void LazyListViewAttached::setRemoving(bool removing) { emit removingChanged(); } +bool LazyListViewAttached::trackViewport() const { + return m_trackViewport; +} + +void LazyListViewAttached::setTrackViewport(bool track) { + if (m_trackViewport == track) + return; + m_trackViewport = track; + emit trackViewportChanged(); +} + // --- LazyListView --- LazyListView::LazyListView(QQuickItem* parent) @@ -628,11 +639,21 @@ void LazyListView::syncDelegates() { if (entry.item) { const qreal h = delegateHeight(entry.item); if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height + 1.0, h + 1.0)) { + const qreal oldLayoutH = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); if (m_layout[i].heightKnown) untrackHeight(m_layout[i].height); m_layout[i].height = h; m_layout[i].heightKnown = true; trackHeight(h); + + // Compensate if tracked item materializes above viewport + auto* att = + qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); + if (att && att->trackViewport()) { + const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; + if (m_layout[i].targetY < vpTop) + emit viewportAdjustNeeded(h - oldLayoutH); + } layoutChanged = true; } entry.item->setY(m_layout[i].targetY - m_contentY); @@ -745,6 +766,18 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { if (wasKnown) untrackHeight(oldH); trackHeight(h); + + // If this tracked item is above the viewport, emit a + // compensation delta so the consumer can adjust scroll. + if (wasKnown) { + auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (att && att->trackViewport()) { + const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; + if (m_layout[idx].targetY < vpTop) + emit viewportAdjustNeeded(h - oldH); + } + } + if (!m_relayoutPending) { m_relayoutPending = true; QTimer::singleShot(0, this, [this] { diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index f4f28c631..891960d3b 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -21,6 +21,7 @@ class LazyListViewAttached : public QObject { Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged) Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) + Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged) public: explicit LazyListViewAttached(QObject* parent = nullptr); @@ -37,17 +38,22 @@ class LazyListViewAttached : public QObject { [[nodiscard]] bool removing() const; void setRemoving(bool removing); + [[nodiscard]] bool trackViewport() const; + void setTrackViewport(bool track); + signals: void preferredHeightChanged(); void visibleHeightChanged(); void addingChanged(); void removingChanged(); + void trackViewportChanged(); private: qreal m_preferredHeight = -1; qreal m_visibleHeight = -1; bool m_adding = false; bool m_removing = false; + bool m_trackViewport = false; }; class LazyListView : public QQuickItem { @@ -198,6 +204,7 @@ class LazyListView : public QQuickItem { void moveCurveChanged(); void countChanged(); void settledChanged(); + void viewportAdjustNeeded(qreal delta); protected: void componentComplete() override; From e3b3a6687719870dd3f92761e42184cbf6817200 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:56:51 +1000 Subject: [PATCH 254/409] fix: notif fileview warnings on first launch --- services/Notifs.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/services/Notifs.qml b/services/Notifs.qml index 92e609be9..9ea5beffa 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -105,6 +105,7 @@ Singleton { FileView { id: storage + printErrors: false path: `${Paths.state}/notifs.json` onLoaded: { const data = JSON.parse(text()); @@ -116,7 +117,7 @@ Singleton { onLoadFailed: err => { if (err === FileViewError.FileNotFound) { root.loaded = true; - setText("[]"); + Qt.callLater(() => setText("[]")); } } } From 9145f83639254d9d067815298c19ea7aa37ce80b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:06:46 +1000 Subject: [PATCH 255/409] fix: remove dead list view anim code --- modules/sidebar/NotifDockList.qml | 14 - .../src/Caelestia/Components/lazylistview.cpp | 255 +----------------- .../src/Caelestia/Components/lazylistview.hpp | 76 ------ 3 files changed, 1 insertion(+), 344 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 4e2ae6e8d..b0e529c81 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -26,21 +26,7 @@ LazyListView { useCustomViewport: true viewport: Qt.rect(0, container.contentY, width, container.height) - addDuration: Appearance.anim.durations.expressiveDefaultSpatial - addCurve.type: Easing.BezierSpline - addCurve.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - addFromOpacity: 0 - addFromScale: 0 - removeDuration: Appearance.anim.durations.normal - removeCurve.type: Easing.BezierSpline - removeCurve.bezierCurve: Appearance.anim.curves.standard - removeToOpacity: 0 - removeToScale: 0.6 - - moveDuration: Appearance.anim.durations.expressiveDefaultSpatial - moveCurve.type: Easing.BezierSpline - moveCurve.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial model: ScriptModel { values: { diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index c35dbb6be..9e03b03e9 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -1,7 +1,6 @@ #include "lazylistview.hpp" #include -#include #include namespace { @@ -268,52 +267,6 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) { return item->implicitHeight(); } -// --- Add Animation --- - -int LazyListView::addDuration() const { - return m_addDuration; -} - -void LazyListView::setAddDuration(int duration) { - if (m_addDuration == duration) - return; - m_addDuration = duration; - emit addDurationChanged(); -} - -QEasingCurve LazyListView::addCurve() const { - return m_addCurve; -} - -void LazyListView::setAddCurve(const QEasingCurve& curve) { - if (m_addCurve == curve) - return; - m_addCurve = curve; - emit addCurveChanged(); -} - -qreal LazyListView::addFromOpacity() const { - return m_addFromOpacity; -} - -void LazyListView::setAddFromOpacity(qreal opacity) { - if (qFuzzyCompare(m_addFromOpacity, opacity)) - return; - m_addFromOpacity = opacity; - emit addFromOpacityChanged(); -} - -qreal LazyListView::addFromScale() const { - return m_addFromScale; -} - -void LazyListView::setAddFromScale(qreal scale) { - if (qFuzzyCompare(m_addFromScale, scale)) - return; - m_addFromScale = scale; - emit addFromScaleChanged(); -} - // --- Remove Animation --- int LazyListView::removeDuration() const { @@ -327,73 +280,12 @@ void LazyListView::setRemoveDuration(int duration) { emit removeDurationChanged(); } -QEasingCurve LazyListView::removeCurve() const { - return m_removeCurve; -} - -void LazyListView::setRemoveCurve(const QEasingCurve& curve) { - if (m_removeCurve == curve) - return; - m_removeCurve = curve; - emit removeCurveChanged(); -} - -qreal LazyListView::removeToOpacity() const { - return m_removeToOpacity; -} - -void LazyListView::setRemoveToOpacity(qreal opacity) { - if (qFuzzyCompare(m_removeToOpacity, opacity)) - return; - m_removeToOpacity = opacity; - emit removeToOpacityChanged(); -} - -qreal LazyListView::removeToScale() const { - return m_removeToScale; -} - -void LazyListView::setRemoveToScale(qreal scale) { - if (qFuzzyCompare(m_removeToScale, scale)) - return; - m_removeToScale = scale; - emit removeToScaleChanged(); -} - -// --- Move Animation --- - -int LazyListView::moveDuration() const { - return m_moveDuration; -} - -void LazyListView::setMoveDuration(int duration) { - if (m_moveDuration == duration) - return; - m_moveDuration = duration; - emit moveDurationChanged(); -} - -QEasingCurve LazyListView::moveCurve() const { - return m_moveCurve; -} - -void LazyListView::setMoveCurve(const QEasingCurve& curve) { - if (m_moveCurve == curve) - return; - m_moveCurve = curve; - emit moveCurveChanged(); -} - // --- State --- int LazyListView::count() const { return m_model ? m_model->rowCount() : 0; } -bool LazyListView::settled() const { - return m_activeAnimations == 0; -} - // --- QQuickItem Overrides --- void LazyListView::componentComplete() { @@ -589,7 +481,7 @@ void LazyListView::syncDelegates() { const auto vp = effectiveViewport(); QList toRemove; for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { - if (visibleIndices.contains(it.key()) || it->animation) + if (visibleIndices.contains(it.key())) continue; if (!it->item) { toRemove.append(it.key()); @@ -806,15 +698,6 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { } void LazyListView::destroyDelegate(DelegateEntry& entry) { - if (entry.animation) { - // Disconnect before stopping to prevent re-entrant onAnimationFinished - disconnect(entry.animation, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); - entry.animation->stop(); - entry.animation = nullptr; - --m_activeAnimations; - if (m_activeAnimations == 0) - emit settledChanged(); - } if (entry.attachedConnection) disconnect(entry.attachedConnection); if (entry.item) { @@ -908,15 +791,9 @@ void LazyListView::resetContent() { destroyDelegate(entry); m_dyingDelegates.clear(); - if (m_activeAnimations != 0) { - m_activeAnimations = 0; - emit settledChanged(); - } - // Reset pending state m_knownHeightSum = 0; m_knownHeightCount = 0; - m_pendingAddAnimations.clear(); // Rebuild layout from model m_layout.clear(); @@ -955,10 +832,6 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last } m_delegates = std::move(shifted); - // Queue add animations and mark displacement - for (int i = first; i <= last; ++i) - m_pendingAddAnimations.insert(i); - emit countChanged(); polish(); } @@ -975,7 +848,6 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, if (entry.item) m_itemToIndex.remove(entry.item); entry.pendingRemoval = true; - stopAnimation(entry); if (m_removeDuration > 0 && entry.item) { auto* attached = @@ -1129,129 +1001,4 @@ void LazyListView::onModelReset() { resetContent(); } -// --- Animation --- - -void LazyListView::startAddAnimation(DelegateEntry& entry) { - if (!entry.item || m_addDuration <= 0) - return; - - stopAnimation(entry); - - auto* group = new QParallelAnimationGroup(this); - - if (!qFuzzyCompare(m_addFromOpacity, 1.0)) { - auto* opacityAnim = new QPropertyAnimation(entry.item, "opacity"); - opacityAnim->setDuration(m_addDuration); - opacityAnim->setEasingCurve(m_addCurve); - opacityAnim->setStartValue(m_addFromOpacity); - opacityAnim->setEndValue(1.0); - group->addAnimation(opacityAnim); - entry.item->setOpacity(m_addFromOpacity); - } - - if (!qFuzzyCompare(m_addFromScale, 1.0)) { - auto* scaleAnim = new QPropertyAnimation(entry.item, "scale"); - scaleAnim->setDuration(m_addDuration); - scaleAnim->setEasingCurve(m_addCurve); - scaleAnim->setStartValue(m_addFromScale); - scaleAnim->setEndValue(1.0); - group->addAnimation(scaleAnim); - entry.item->setScale(m_addFromScale); - } - - if (group->animationCount() == 0) { - delete group; - return; - } - - entry.animation = group; - ++m_activeAnimations; - if (m_activeAnimations == 1) - emit settledChanged(); - - connect(group, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); - group->start(QAbstractAnimation::DeleteWhenStopped); -} - -void LazyListView::startRemoveAnimation(DelegateEntry& entry) { - if (!entry.item || m_removeDuration <= 0) - return; - - stopAnimation(entry); - - auto* group = new QParallelAnimationGroup(this); - - if (!qFuzzyCompare(m_removeToOpacity, 1.0)) { - auto* opacityAnim = new QPropertyAnimation(entry.item, "opacity"); - opacityAnim->setDuration(m_removeDuration); - opacityAnim->setEasingCurve(m_removeCurve); - opacityAnim->setStartValue(entry.item->opacity()); - opacityAnim->setEndValue(m_removeToOpacity); - group->addAnimation(opacityAnim); - } - - if (!qFuzzyCompare(m_removeToScale, 1.0)) { - auto* scaleAnim = new QPropertyAnimation(entry.item, "scale"); - scaleAnim->setDuration(m_removeDuration); - scaleAnim->setEasingCurve(m_removeCurve); - scaleAnim->setStartValue(entry.item->scale()); - scaleAnim->setEndValue(m_removeToScale); - group->addAnimation(scaleAnim); - } - - if (group->animationCount() == 0) { - delete group; - return; - } - - entry.animation = group; - ++m_activeAnimations; - if (m_activeAnimations == 1) - emit settledChanged(); - - connect(group, &QAbstractAnimation::finished, this, &LazyListView::onAnimationFinished); - group->start(QAbstractAnimation::DeleteWhenStopped); -} - -void LazyListView::stopAnimation(DelegateEntry& entry) { - if (!entry.animation) - return; - - entry.animation->stop(); - entry.animation = nullptr; - - --m_activeAnimations; - if (m_activeAnimations == 0) - emit settledChanged(); -} - -void LazyListView::onAnimationFinished() { - auto* group = qobject_cast(sender()); - - // Clear animation pointer from live delegates - for (auto& entry : m_delegates) { - if (entry.animation == group) - entry.animation = nullptr; - } - - // Clean up dying delegates whose animation finished - m_dyingDelegates.erase(std::remove_if(m_dyingDelegates.begin(), m_dyingDelegates.end(), - [this, group](DelegateEntry& entry) { - if (entry.animation == group) { - entry.animation = nullptr; - destroyDelegate(entry); - return true; - } - return false; - }), - m_dyingDelegates.end()); - - --m_activeAnimations; - if (m_activeAnimations == 0) - emit settledChanged(); - - // Re-sync in case viewport changed during animation - polish(); -} - } // namespace caelestia::components diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index 891960d3b..1d0e8433d 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -1,10 +1,8 @@ #pragma once #include -#include #include #include -#include #include #include #include @@ -82,25 +80,11 @@ class LazyListView : public QQuickItem { // Async Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged) - // Add Animation - Q_PROPERTY(int addDuration READ addDuration WRITE setAddDuration NOTIFY addDurationChanged) - Q_PROPERTY(QEasingCurve addCurve READ addCurve WRITE setAddCurve NOTIFY addCurveChanged) - Q_PROPERTY(qreal addFromOpacity READ addFromOpacity WRITE setAddFromOpacity NOTIFY addFromOpacityChanged) - Q_PROPERTY(qreal addFromScale READ addFromScale WRITE setAddFromScale NOTIFY addFromScaleChanged) - // Remove Animation Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged) - Q_PROPERTY(QEasingCurve removeCurve READ removeCurve WRITE setRemoveCurve NOTIFY removeCurveChanged) - Q_PROPERTY(qreal removeToOpacity READ removeToOpacity WRITE setRemoveToOpacity NOTIFY removeToOpacityChanged) - Q_PROPERTY(qreal removeToScale READ removeToScale WRITE setRemoveToScale NOTIFY removeToScaleChanged) - - // Move/Displaced Animation - Q_PROPERTY(int moveDuration READ moveDuration WRITE setMoveDuration NOTIFY moveDurationChanged) - Q_PROPERTY(QEasingCurve moveCurve READ moveCurve WRITE setMoveCurve NOTIFY moveCurveChanged) // State Q_PROPERTY(int count READ count NOTIFY countChanged) - Q_PROPERTY(bool settled READ settled NOTIFY settledChanged) public: explicit LazyListView(QQuickItem* parent = nullptr); @@ -143,43 +127,12 @@ class LazyListView : public QQuickItem { [[nodiscard]] bool asynchronous() const; void setAsynchronous(bool async); - // Add Animation - [[nodiscard]] int addDuration() const; - void setAddDuration(int duration); - - [[nodiscard]] QEasingCurve addCurve() const; - void setAddCurve(const QEasingCurve& curve); - - [[nodiscard]] qreal addFromOpacity() const; - void setAddFromOpacity(qreal opacity); - - [[nodiscard]] qreal addFromScale() const; - void setAddFromScale(qreal scale); - // Remove Animation [[nodiscard]] int removeDuration() const; void setRemoveDuration(int duration); - [[nodiscard]] QEasingCurve removeCurve() const; - void setRemoveCurve(const QEasingCurve& curve); - - [[nodiscard]] qreal removeToOpacity() const; - void setRemoveToOpacity(qreal opacity); - - [[nodiscard]] qreal removeToScale() const; - void setRemoveToScale(qreal scale); - - // Move Animation - [[nodiscard]] int moveDuration() const; - void setMoveDuration(int duration); - - [[nodiscard]] QEasingCurve moveCurve() const; - void setMoveCurve(const QEasingCurve& curve); - // State [[nodiscard]] int count() const; - [[nodiscard]] bool settled() const; - signals: void modelChanged(); void delegateChanged(); @@ -192,18 +145,8 @@ class LazyListView : public QQuickItem { void cacheBufferChanged(); void estimatedHeightChanged(); void asynchronousChanged(); - void addDurationChanged(); - void addCurveChanged(); - void addFromOpacityChanged(); - void addFromScaleChanged(); void removeDurationChanged(); - void removeCurveChanged(); - void removeToOpacityChanged(); - void removeToScaleChanged(); - void moveDurationChanged(); - void moveCurveChanged(); void countChanged(); - void settledChanged(); void viewportAdjustNeeded(qreal delta); protected: @@ -223,7 +166,6 @@ class LazyListView : public QQuickItem { QQuickItem* item = nullptr; QQmlContext* context = nullptr; bool pendingRemoval = false; - QParallelAnimationGroup* animation = nullptr; QMetaObject::Connection attachedConnection; }; @@ -254,11 +196,6 @@ class LazyListView : public QQuickItem { void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles); void onModelReset(); - // Animation - void startAddAnimation(DelegateEntry& entry); - void startRemoveAnimation(DelegateEntry& entry); - void stopAnimation(DelegateEntry& entry); - void onAnimationFinished(); // Members QAbstractItemModel* m_model = nullptr; @@ -278,28 +215,15 @@ class LazyListView : public QQuickItem { int m_knownHeightCount = 0; bool m_asynchronous = false; - int m_addDuration = 300; - QEasingCurve m_addCurve; - qreal m_addFromOpacity = 0; - qreal m_addFromScale = 1; - int m_removeDuration = 300; - QEasingCurve m_removeCurve; - qreal m_removeToOpacity = 0; - qreal m_removeToScale = 1; - - int m_moveDuration = 300; - QEasingCurve m_moveCurve; QVector m_layout; QHash m_delegates; QHash m_itemToIndex; QVector m_dyingDelegates; - int m_activeAnimations = 0; bool m_componentComplete = false; bool m_relayoutPending = false; - QSet m_pendingAddAnimations; QList m_modelConnections; }; From cc54d9bc5bafa255e62409ddc4990fc47b309d68 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 22:20:38 +1000 Subject: [PATCH 256/409] fix: remove unnecessary qml context creation --- .../src/Caelestia/Components/lazylistview.cpp | 92 +++++++------------ .../src/Caelestia/Components/lazylistview.hpp | 3 - 2 files changed, 32 insertions(+), 63 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 9e03b03e9..94e376877 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -1,6 +1,7 @@ #include "lazylistview.hpp" #include +#include #include namespace { @@ -572,57 +573,43 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { return entry; const auto roleNames = m_model->roleNames(); - const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); - // Use the delegate component's creation context directly for beginCreate + // Use the delegate component's creation context for beginCreate // so bound components (pragma ComponentBehavior: Bound) are accepted. - // A per-delegate child context is kept for data updates. auto* compContext = m_delegate->creationContext(); - auto* parentContext = compContext ? compContext : qmlContext(this); - if (!parentContext) + if (!compContext) + compContext = qmlContext(this); + if (!compContext) return entry; - entry.context = new QQmlContext(parentContext, this); + auto* obj = m_delegate->beginCreate(compContext); + entry.item = qobject_cast(obj); - // Build property map for both context properties and initial properties + if (!entry.item) { + if (obj) + m_delegate->completeCreate(); + delete obj; + return entry; + } + + // Build initial properties from model data const auto index = m_model->index(modelIndex, 0); QVariantMap initialProps; - bool hasModelData = false; + for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { const auto name = QString::fromUtf8(it.value()); - const auto value = m_model->data(index, it.key()); - entry.context->setContextProperty(name, value); - initialProps.insert(name, value); + initialProps.insert(name, m_model->data(index, it.key())); if (name == QStringLiteral("modelData")) hasModelData = true; } - entry.context->setContextProperty(QStringLiteral("index"), modelIndex); initialProps.insert(QStringLiteral("index"), modelIndex); - // Provide modelData for single-role models or if not already provided by role names if (!hasModelData) { - const auto value = m_model->data(index, role); - entry.context->setContextProperty(QStringLiteral("modelData"), value); - initialProps.insert(QStringLiteral("modelData"), value); - } - - // Use the creation context for beginCreate to satisfy bound component checks - // (pragma ComponentBehavior: Bound). Data is passed via setInitialProperties. - auto* creationCtx = compContext ? compContext : parentContext; - auto* obj = m_delegate->beginCreate(creationCtx); - entry.item = qobject_cast(obj); - - if (!entry.item) { - if (obj) - m_delegate->completeCreate(); - delete obj; - delete entry.context; - entry.context = nullptr; - return entry; + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + initialProps.insert(QStringLiteral("modelData"), m_model->data(index, role)); } - // Set initial properties to satisfy required property declarations m_delegate->setInitialProperties(entry.item, initialProps); entry.item->setParentItem(this); @@ -706,14 +693,10 @@ void LazyListView::destroyDelegate(DelegateEntry& entry) { entry.item->deleteLater(); entry.item = nullptr; } - if (entry.context) { - entry.context->deleteLater(); - entry.context = nullptr; - } } void LazyListView::updateDelegateData(DelegateEntry& entry) { - if (!m_model) + if (!m_model || !entry.item) return; const auto roleNames = m_model->roleNames(); @@ -722,27 +705,16 @@ void LazyListView::updateDelegateData(DelegateEntry& entry) { for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { const auto name = QString::fromUtf8(it.value()); - const auto value = m_model->data(index, it.key()); - if (entry.context) - entry.context->setContextProperty(name, value); - if (entry.item) - entry.item->setProperty(name.toUtf8().constData(), value); + entry.item->setProperty(name.toUtf8().constData(), m_model->data(index, it.key())); if (name == QStringLiteral("modelData")) hasModelData = true; } - if (entry.context) - entry.context->setContextProperty(QStringLiteral("index"), entry.modelIndex); - if (entry.item) - entry.item->setProperty("index", entry.modelIndex); + entry.item->setProperty("index", entry.modelIndex); if (!hasModelData) { const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); - const auto value = m_model->data(index, role); - if (entry.context) - entry.context->setContextProperty(QStringLiteral("modelData"), value); - if (entry.item) - entry.item->setProperty("modelData", value); + entry.item->setProperty("modelData", m_model->data(index, role)); } } @@ -824,10 +796,10 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last int newIdx = it.key() >= first ? it.key() + insertCount : it.key(); auto entry = std::move(it.value()); entry.modelIndex = newIdx; - if (entry.context) - entry.context->setContextProperty(QStringLiteral("index"), newIdx); - if (entry.item) + if (entry.item) { + entry.item->setProperty("index", newIdx); m_itemToIndex[entry.item] = newIdx; + } shifted.insert(newIdx, std::move(entry)); } m_delegates = std::move(shifted); @@ -894,10 +866,10 @@ void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) int newIdx = it.key() > last ? it.key() - removeCount : it.key(); auto entry = std::move(it.value()); entry.modelIndex = newIdx; - if (entry.context) - entry.context->setContextProperty(QStringLiteral("index"), newIdx); - if (entry.item) + if (entry.item) { + entry.item->setProperty("index", newIdx); m_itemToIndex[entry.item] = newIdx; + } shifted.insert(newIdx, std::move(entry)); } m_delegates = std::move(shifted); @@ -939,10 +911,10 @@ void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, co auto entry = std::move(it.value()); entry.modelIndex = newIdx; - if (entry.context) - entry.context->setContextProperty(QStringLiteral("index"), newIdx); - if (entry.item) + if (entry.item) { + entry.item->setProperty("index", newIdx); m_itemToIndex[entry.item] = newIdx; + } remapped.insert(newIdx, std::move(entry)); } m_delegates = std::move(remapped); diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index 1d0e8433d..395d022ed 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -4,7 +4,6 @@ #include #include #include -#include #include #include #include @@ -164,7 +163,6 @@ class LazyListView : public QQuickItem { struct DelegateEntry { int modelIndex = -1; QQuickItem* item = nullptr; - QQmlContext* context = nullptr; bool pendingRemoval = false; QMetaObject::Connection attachedConnection; }; @@ -196,7 +194,6 @@ class LazyListView : public QQuickItem { void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles); void onModelReset(); - // Members QAbstractItemModel* m_model = nullptr; QQmlComponent* m_delegate = nullptr; From e6c1248bef68c273ceb6306db1fa0a64d440e28e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 9 Apr 2026 23:29:12 +1000 Subject: [PATCH 257/409] fix: no need to disconnect signal Signal will auto disconnect on item destruction --- plugin/src/Caelestia/Components/lazylistview.cpp | 5 +---- plugin/src/Caelestia/Components/lazylistview.hpp | 1 - 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 94e376877..1df131235 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -674,8 +674,7 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { // Watch attached properties if the delegate uses them auto* attached = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); if (attached) { - entry.attachedConnection = - connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); + connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] { polish(); }); @@ -685,8 +684,6 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { } void LazyListView::destroyDelegate(DelegateEntry& entry) { - if (entry.attachedConnection) - disconnect(entry.attachedConnection); if (entry.item) { entry.item->setParentItem(nullptr); entry.item->setVisible(false); diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index 395d022ed..0098fb03a 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -164,7 +164,6 @@ class LazyListView : public QQuickItem { int modelIndex = -1; QQuickItem* item = nullptr; bool pendingRemoval = false; - QMetaObject::Connection attachedConnection; }; // Layout From 2efe1a93a686960ded85e4b161b7418810c76fa5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 00:19:16 +1000 Subject: [PATCH 258/409] fix: layout delegates after configurable delay Fixes glitches due to delegate sizes changing on creation Also use the loader state pattern to ensure size is set on same frame as active --- modules/sidebar/Notif.qml | 46 ++++++- modules/sidebar/NotifDockList.qml | 1 + modules/sidebar/NotifGroupList.qml | 1 + .../src/Caelestia/Components/lazylistview.cpp | 130 +++++++++++++----- .../src/Caelestia/Components/lazylistview.hpp | 18 ++- 5 files changed, 158 insertions(+), 38 deletions(-) diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index e23460f11..5ccdc97e1 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -155,13 +155,51 @@ StyledRect { } component WrappedLoader: Loader { + id: comp + required property bool shouldBeActive - opacity: shouldBeActive ? 1 : 0 - active: opacity > 0 + active: false + opacity: 0 + + // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set + states: State { + name: "active" + when: comp.shouldBeActive - Behavior on opacity { - Anim {} + PropertyChanges { + comp.opacity: 1 + comp.active: true + } } + + transitions: [ + Transition { + from: "" + to: "active" + + SequentialAnimation { + PropertyAction { + property: "active" + } + Anim { + property: "opacity" + } + } + }, + Transition { + from: "active" + to: "" + + SequentialAnimation { + Anim { + property: "opacity" + } + PropertyAction { + property: "active" + } + } + } + ] } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index b0e529c81..d2dc49722 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -19,6 +19,7 @@ LazyListView { implicitHeight: contentHeight spacing: Appearance.spacing.small + readyDelay: 1 cacheBuffer: 400 asynchronous: true diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 78c6ce59a..21692587c 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -25,6 +25,7 @@ LazyListView { spacing: Math.round(Appearance.spacing.small / 2) asynchronous: true + readyDelay: 1 cacheBuffer: 800 removeDuration: Appearance.anim.durations.normal diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 1df131235..27a8a92da 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -40,6 +40,17 @@ void LazyListViewAttached::setVisibleHeight(qreal height) { emit visibleHeightChanged(); } +bool LazyListViewAttached::ready() const { + return m_ready; +} + +void LazyListViewAttached::setReady(bool ready) { + if (m_ready == ready) + return; + m_ready = ready; + emit readyChanged(); +} + bool LazyListViewAttached::adding() const { return m_adding; } @@ -268,7 +279,14 @@ qreal LazyListView::delegateVisibleHeight(QQuickItem* item) { return item->implicitHeight(); } -// --- Remove Animation --- +bool LazyListView::isDelegateReady(QQuickItem* item) { + if (!item) + return false; + auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); + return !att || att->ready(); +} + +// --- Animation Durations --- int LazyListView::removeDuration() const { return m_removeDuration; @@ -281,6 +299,17 @@ void LazyListView::setRemoveDuration(int duration) { emit removeDurationChanged(); } +int LazyListView::readyDelay() const { + return m_readyDelay; +} + +void LazyListView::setReadyDelay(int delay) { + if (m_readyDelay == delay) + return; + m_readyDelay = delay; + emit readyDelayChanged(); +} + // --- State --- int LazyListView::count() const { @@ -315,12 +344,32 @@ void LazyListView::updatePolish() { if (!m_componentComplete || !m_model || !m_delegate) return; + // Flush pending inserts from the previous frame — make items visible + // and clear the adding flag so enter animations begin. + for (auto& entry : m_delegates) { + if (!entry.pendingInsert || !entry.item) + continue; + entry.pendingInsert = false; + entry.item->setVisible(true); + auto* att = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); + if (att) { + att->setAdding(false); + if (m_readyDelay > 0) { + QTimer::singleShot(m_readyDelay, att, [att] { + att->setReady(true); + }); + } else { + att->setReady(true); + } + } + } + relayout(); syncDelegates(); // Position delegates — QML Behavior on y handles the animation for (auto& entry : m_delegates) { - if (!entry.item || entry.pendingRemoval) + if (!entry.item || entry.pendingRemoval || entry.pendingInsert) continue; const int idx = entry.modelIndex; @@ -523,32 +572,15 @@ void LazyListView::syncDelegates() { // Batch create const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast(toCreate.size()); int created = 0; - bool layoutChanged = false; for (int i : toCreate) { if (created >= createBudget) break; auto entry = createDelegate(i); if (entry.item) { - const qreal h = delegateHeight(entry.item); - if (!m_layout[i].heightKnown || !qFuzzyCompare(m_layout[i].height + 1.0, h + 1.0)) { - const qreal oldLayoutH = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); - if (m_layout[i].heightKnown) - untrackHeight(m_layout[i].height); - m_layout[i].height = h; - m_layout[i].heightKnown = true; - trackHeight(h); - - // Compensate if tracked item materializes above viewport - auto* att = - qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); - if (att && att->trackViewport()) { - const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; - if (m_layout[i].targetY < vpTop) - emit viewportAdjustNeeded(h - oldLayoutH); - } - layoutChanged = true; - } + // Height tracking and viewport compensation are deferred + // until the delegate signals ready via readyChanged. + entry.pendingInsert = true; entry.item->setY(m_layout[i].targetY - m_contentY); m_itemToIndex.insert(entry.item, i); m_delegates.insert(i, std::move(entry)); @@ -556,12 +588,10 @@ void LazyListView::syncDelegates() { } } - if (layoutChanged) - relayout(); - - // If async and there's remaining work, schedule another pass - if (m_asynchronous && - (destroyed < static_cast(toRemove.size()) || created < static_cast(toCreate.size()))) + // Pending inserts need to become visible on the next frame, and + // async mode may have remaining create/destroy work. + if (created > 0 || (m_asynchronous && (destroyed < static_cast(toRemove.size()) || + created < static_cast(toCreate.size())))) polish(); } @@ -616,7 +646,7 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.item->setWidth(width()); // Set adding = true before completeCreate so bindings see it during initial evaluation. - // Cleared after creation so the transition from true→false triggers QML Behaviors. + // Cleared on the next frame in updatePolish when the item becomes visible. auto* addingAttached = qobject_cast(qmlAttachedPropertiesObject(entry.item, true)); if (addingAttached) @@ -624,11 +654,14 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { m_delegate->completeCreate(); - if (addingAttached) - addingAttached->setAdding(false); + // Keep adding=true and hide — flushed on the next frame in updatePolish + entry.item->setVisible(false); - // Height-change handler — uses m_itemToIndex for O(1) lookup + // Height-change handler — uses m_itemToIndex for O(1) lookup. + // Ignored while the delegate is not yet ready. auto onHeightChanged = [this, item = entry.item] { + if (!isDelegateReady(item)) + return; auto indexIt = m_itemToIndex.find(item); if (indexIt == m_itemToIndex.end()) return; @@ -678,6 +711,33 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] { polish(); }); + connect(attached, &LazyListViewAttached::readyChanged, this, [this, item = entry.item] { + auto indexIt = m_itemToIndex.find(item); + if (indexIt == m_itemToIndex.end()) + return; + const int idx = indexIt.value(); + if (idx >= static_cast(m_layout.size())) + return; + auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (!att || !att->ready()) + return; + + const qreal h = delegateHeight(item); + const qreal oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight(); + if (m_layout[idx].heightKnown) + untrackHeight(m_layout[idx].height); + m_layout[idx].height = h; + m_layout[idx].heightKnown = true; + trackHeight(h); + + if (att->trackViewport() && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) { + const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; + if (m_layout[idx].targetY < vpTop) + emit viewportAdjustNeeded(h - oldLayoutH); + } + + polish(); + }); } return entry; @@ -818,6 +878,12 @@ void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, m_itemToIndex.remove(entry.item); entry.pendingRemoval = true; + // Never made visible — skip remove animation + if (entry.pendingInsert) { + destroyDelegate(entry); + continue; + } + if (m_removeDuration > 0 && entry.item) { auto* attached = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index 0098fb03a..feb501c68 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -16,6 +16,7 @@ class LazyListViewAttached : public QObject { Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged) + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged) @@ -29,6 +30,9 @@ class LazyListViewAttached : public QObject { [[nodiscard]] qreal visibleHeight() const; void setVisibleHeight(qreal height); + [[nodiscard]] bool ready() const; + void setReady(bool ready); + [[nodiscard]] bool adding() const; void setAdding(bool adding); @@ -41,6 +45,7 @@ class LazyListViewAttached : public QObject { signals: void preferredHeightChanged(); void visibleHeightChanged(); + void readyChanged(); void addingChanged(); void removingChanged(); void trackViewportChanged(); @@ -48,6 +53,7 @@ class LazyListViewAttached : public QObject { private: qreal m_preferredHeight = -1; qreal m_visibleHeight = -1; + bool m_ready = false; bool m_adding = false; bool m_removing = false; bool m_trackViewport = false; @@ -79,8 +85,9 @@ class LazyListView : public QQuickItem { // Async Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged) - // Remove Animation + // Animation Durations Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged) + Q_PROPERTY(int readyDelay READ readyDelay WRITE setReadyDelay NOTIFY readyDelayChanged) // State Q_PROPERTY(int count READ count NOTIFY countChanged) @@ -126,10 +133,13 @@ class LazyListView : public QQuickItem { [[nodiscard]] bool asynchronous() const; void setAsynchronous(bool async); - // Remove Animation + // Animation Durations [[nodiscard]] int removeDuration() const; void setRemoveDuration(int duration); + [[nodiscard]] int readyDelay() const; + void setReadyDelay(int delay); + // State [[nodiscard]] int count() const; signals: @@ -145,6 +155,7 @@ class LazyListView : public QQuickItem { void estimatedHeightChanged(); void asynchronousChanged(); void removeDurationChanged(); + void readyDelayChanged(); void countChanged(); void viewportAdjustNeeded(qreal delta); @@ -164,6 +175,7 @@ class LazyListView : public QQuickItem { int modelIndex = -1; QQuickItem* item = nullptr; bool pendingRemoval = false; + bool pendingInsert = false; }; // Layout @@ -173,6 +185,7 @@ class LazyListView : public QQuickItem { [[nodiscard]] qreal effectiveEstimatedHeight() const; [[nodiscard]] static qreal delegateHeight(QQuickItem* item); [[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item); + [[nodiscard]] static bool isDelegateReady(QQuickItem* item); void trackHeight(qreal height); void untrackHeight(qreal height); @@ -212,6 +225,7 @@ class LazyListView : public QQuickItem { bool m_asynchronous = false; int m_removeDuration = 300; + int m_readyDelay = 0; QVector m_layout; QHash m_delegates; From b0aeee8b8077b596da06b06ccdfe83f02786d39d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:10:05 +1000 Subject: [PATCH 259/409] fix: allow viewport to be outside of bounds --- plugin/src/Caelestia/Components/lazylistview.cpp | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 27a8a92da..145455780 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -453,7 +453,9 @@ QRectF LazyListView::effectiveViewport() const { // During Flickable overshoot the viewport can extend entirely beyond content bounds, // causing all delegates to be culled. Clamp so it always overlaps [0, layoutHeight]. - if (m_layoutHeight > 0) { + // Only needed for the built-in viewport — custom viewports represent the actual + // visible area and may legitimately lie entirely outside the content. + if (!m_useCustomViewport && m_layoutHeight > 0) { const qreal top = std::min(vp.y(), m_layoutHeight); const qreal bottom = std::max(vp.y() + vp.height(), 0.0); if (bottom > top) @@ -470,6 +472,8 @@ QRectF LazyListView::effectiveViewport() const { const qreal bottom = std::min(vp.y() + vp.height(), m_layoutHeight); if (top < bottom) vp = QRectF(vp.x(), top, vp.width(), bottom - top); + else + return {}; } return vp; @@ -480,6 +484,9 @@ std::pair LazyListView::computeVisibleRange() const { return { -1, -1 }; const auto vp = effectiveViewport(); + if (vp.isEmpty()) + return { -1, -1 }; + const qreal vpTop = vp.y(); const qreal vpBottom = vp.y() + vp.height(); @@ -533,7 +540,7 @@ void LazyListView::syncDelegates() { for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { if (visibleIndices.contains(it.key())) continue; - if (!it->item) { + if (!it->item || vp.isEmpty()) { toRemove.append(it.key()); continue; } From ac65b78588bf48a8adfd282cc609a68e5a0d2e7f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:10:21 +1000 Subject: [PATCH 260/409] fix: reduce notif group list cache buffer --- modules/sidebar/NotifGroupList.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 21692587c..31aa44b5b 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -26,7 +26,7 @@ LazyListView { asynchronous: true readyDelay: 1 - cacheBuffer: 800 + cacheBuffer: 400 removeDuration: Appearance.anim.durations.normal useCustomViewport: true From 7a82ca4765713c1d688d9fad35ccba8f5ff8c460 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:22:16 +1000 Subject: [PATCH 261/409] fix: account for closing notifs in group preview --- modules/sidebar/NotifGroupList.qml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 31aa44b5b..112db493b 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -36,7 +36,21 @@ LazyListView { } model: ScriptModel { - values: root.expanded ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) + values: { + if (root.expanded) + return root.notifs; + + let count = 0; + let i = 0; + const previewNum = Config.notifs.groupPreviewNum + 1; + while (i < root.notifs.length && count < previewNum) { + if (!(root.notifs[i]?.closed ?? true)) + count++; + i++; + } + + return root.notifs.slice(0, i); + } } delegate: Component { From e234990c52f20d31c3b038dbc1d69c94b813ba95 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:25:09 +1000 Subject: [PATCH 262/409] fix: don't play add/remove anim on old delegates --- modules/sidebar/NotifGroupList.qml | 4 ++-- .../src/Caelestia/Components/lazylistview.cpp | 20 +++++++++++++------ .../src/Caelestia/Components/lazylistview.hpp | 1 + 3 files changed, 17 insertions(+), 8 deletions(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 112db493b..493306125 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -80,8 +80,8 @@ LazyListView { LazyListView.visibleHeight: modelData?.closed || previewHidden ? 0 : notifInner.implicitHeight implicitHeight: notifInner.implicitHeight - opacity: LazyListView.removing || modelData?.closed || previewHidden || LazyListView.adding ? 0 : 1 - scale: LazyListView.removing || previewHidden ? 0.7 : LazyListView.adding ? 0.7 : 1 + opacity: previewHidden || LazyListView.adding ? 0 : 1 + scale: previewHidden || LazyListView.adding ? 0.7 : 1 hoverEnabled: true cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 145455780..653fe97b5 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -367,6 +367,12 @@ void LazyListView::updatePolish() { relayout(); syncDelegates(); + // Clear isNew flags — the add animation only plays for items created + // during the same polish cycle as their model insertion, not for + // delegates created later when scrolling items into the viewport. + for (auto& record : m_layout) + record.isNew = false; + // Position delegates — QML Behavior on y handles the animation for (auto& entry : m_delegates) { if (!entry.item || entry.pendingRemoval || entry.pendingInsert) @@ -652,12 +658,14 @@ LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { entry.item->setParentItem(this); entry.item->setWidth(width()); - // Set adding = true before completeCreate so bindings see it during initial evaluation. + // Only set adding = true for genuinely new model items (not viewport entries). // Cleared on the next frame in updatePolish when the item becomes visible. - auto* addingAttached = - qobject_cast(qmlAttachedPropertiesObject(entry.item, true)); - if (addingAttached) - addingAttached->setAdding(true); + if (modelIndex < static_cast(m_layout.size()) && m_layout[modelIndex].isNew) { + auto* addingAttached = + qobject_cast(qmlAttachedPropertiesObject(entry.item, true)); + if (addingAttached) + addingAttached->setAdding(true); + } m_delegate->completeCreate(); @@ -852,7 +860,7 @@ void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last const int insertCount = last - first + 1; // Insert new layout records - m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false }); + m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false, true }); // Shift existing delegate indices QHash shifted; diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index feb501c68..a027d19a6 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -169,6 +169,7 @@ class LazyListView : public QQuickItem { qreal targetY = 0; qreal height = 0; bool heightKnown = false; + bool isNew = false; }; struct DelegateEntry { From c629eaaa08c9a97540f830973dfa6bd4ceb06ba2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 01:38:12 +1000 Subject: [PATCH 263/409] fix: don't add delegate to layout until ready delay finish Also disable y anim if not ready --- modules/sidebar/NotifDockList.qml | 2 + modules/sidebar/NotifGroupList.qml | 2 + .../src/Caelestia/Components/lazylistview.cpp | 47 +++++++++++++++---- .../src/Caelestia/Components/lazylistview.hpp | 1 + 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index d2dc49722..08752edc6 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -120,6 +120,8 @@ LazyListView { } Behavior on y { + enabled: notif.LazyListView.ready + Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 493306125..5f081a361 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -140,6 +140,8 @@ LazyListView { } Behavior on y { + enabled: notif.LazyListView.ready + Anim { duration: Appearance.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index 653fe97b5..c9291cfd7 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -344,23 +344,52 @@ void LazyListView::updatePolish() { if (!m_componentComplete || !m_model || !m_delegate) return; - // Flush pending inserts from the previous frame — make items visible - // and clear the adding flag so enter animations begin. + // Flush pending inserts — make items visible and clear the adding flag + // so enter animations begin. When readyDelay > 0 the entire insert is + // deferred so delegates have time to lay out before appearing. for (auto& entry : m_delegates) { if (!entry.pendingInsert || !entry.item) continue; + + if (m_readyDelay > 0) { + if (!entry.readyDelayStarted) { + entry.readyDelayStarted = true; + auto* item = entry.item; + QTimer::singleShot(m_readyDelay, this, [this, item] { + auto indexIt = m_itemToIndex.find(item); + if (indexIt == m_itemToIndex.end()) + return; + const int idx = indexIt.value(); + auto it = m_delegates.find(idx); + if (it == m_delegates.end() || it->item != item || !it->pendingInsert) + return; + + it->pendingInsert = false; + it->readyDelayStarted = false; + + // Position correctly before making visible + if (idx >= 0 && idx < static_cast(m_layout.size())) + item->setY(m_layout[idx].targetY - m_contentY); + + item->setVisible(true); + auto* att = + qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (att) { + att->setAdding(false); + att->setReady(true); + } + polish(); + }); + } + continue; + } + entry.pendingInsert = false; entry.item->setVisible(true); auto* att = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); if (att) { att->setAdding(false); - if (m_readyDelay > 0) { - QTimer::singleShot(m_readyDelay, att, [att] { - att->setReady(true); - }); - } else { - att->setReady(true); - } + att->setReady(true); } } diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp index a027d19a6..e0746db2c 100644 --- a/plugin/src/Caelestia/Components/lazylistview.hpp +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -177,6 +177,7 @@ class LazyListView : public QQuickItem { QQuickItem* item = nullptr; bool pendingRemoval = false; bool pendingInsert = false; + bool readyDelayStarted = false; }; // Layout From e3d2ffd9ae931acce68c12493164710c23b5a62a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 02:10:52 +1000 Subject: [PATCH 264/409] fix: remove previewHidden from notif groups Not needed anymore, and was causing a visual bug where when adding a notif, some notifs would overlap --- modules/sidebar/NotifGroupList.qml | 21 +++++---------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 5f081a361..fdcaf30a7 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -42,7 +42,7 @@ LazyListView { let count = 0; let i = 0; - const previewNum = Config.notifs.groupPreviewNum + 1; + const previewNum = Config.notifs.groupPreviewNum; while (i < root.notifs.length && count < previewNum) { if (!(root.notifs[i]?.closed ?? true)) count++; @@ -60,28 +60,17 @@ LazyListView { required property int index required property NotifData modelData - readonly property bool previewHidden: { - if (root.expanded) - return false; - - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i]?.closed) - extraHidden++; - - return index >= Config.notifs.groupPreviewNum + extraHidden; - } property int startY Component.onCompleted: modelData?.lock(this) Component.onDestruction: modelData?.unlock(this) - LazyListView.preferredHeight: modelData?.closed || previewHidden ? 0 : notifInner.nonAnimHeight - LazyListView.visibleHeight: modelData?.closed || previewHidden ? 0 : notifInner.implicitHeight + LazyListView.preferredHeight: modelData?.closed ? 0 : notifInner.nonAnimHeight + LazyListView.visibleHeight: modelData?.closed ? 0 : notifInner.implicitHeight implicitHeight: notifInner.implicitHeight - opacity: previewHidden || LazyListView.adding ? 0 : 1 - scale: previewHidden || LazyListView.adding ? 0.7 : 1 + opacity: LazyListView.removing || LazyListView.adding ? 0 : 1 + scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1 hoverEnabled: true cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined From 6cd5758234729f6f5473a3536d517f15c82829fb Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 02:34:35 +1000 Subject: [PATCH 265/409] fix: animate viewport tracking and fix condition --- modules/sidebar/NotifDockList.qml | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 08752edc6..c40a1e01f 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -23,7 +23,13 @@ LazyListView { cacheBuffer: 400 asynchronous: true - onViewportAdjustNeeded: d => container.contentY += d + onViewportAdjustNeeded: d => { + if (contentYAnim.running) + contentYAnim.complete(); + contentYAnim.to = Math.max(0, container.contentY + d); + contentYAnim.start(); + } + useCustomViewport: true viewport: Qt.rect(0, container.contentY, width, container.height) @@ -54,7 +60,7 @@ LazyListView { clearTimer.start(); } - LazyListView.trackViewport: notifInner.expanded || notifInner.nonAnimHeight < notifInner.implicitHeight + LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight LazyListView.visibleHeight: notifInner.implicitHeight implicitHeight: notifInner.implicitHeight @@ -147,4 +153,13 @@ LazyListView { } } } + + Anim { + id: contentYAnim + + target: root.container + property: "contentY" + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } } From 0844013ca7a4186bf27bb85a7650d9d42a5f5abe Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Fri, 10 Apr 2026 02:36:56 +1000 Subject: [PATCH 266/409] feat: animate delegates in from visual position Instead of suddenly appearing, create them at their visual position then animate them to the layout pos --- modules/sidebar/NotifGroupList.qml | 4 +-- .../src/Caelestia/Components/lazylistview.cpp | 31 +++++++++++++++++-- 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index fdcaf30a7..aeec4c628 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -65,8 +65,8 @@ LazyListView { Component.onCompleted: modelData?.lock(this) Component.onDestruction: modelData?.unlock(this) - LazyListView.preferredHeight: modelData?.closed ? 0 : notifInner.nonAnimHeight - LazyListView.visibleHeight: modelData?.closed ? 0 : notifInner.implicitHeight + LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight + LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight implicitHeight: notifInner.implicitHeight opacity: LazyListView.removing || LazyListView.adding ? 0 : 1 diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp index c9291cfd7..36b49301f 100644 --- a/plugin/src/Caelestia/Components/lazylistview.cpp +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -367,9 +367,29 @@ void LazyListView::updatePolish() { it->pendingInsert = false; it->readyDelayStarted = false; - // Position correctly before making visible - if (idx >= 0 && idx < static_cast(m_layout.size())) - item->setY(m_layout[idx].targetY - m_contentY); + // Set initial y to visual position (based on current visible heights) + if (idx >= 0 && idx < static_cast(m_layout.size())) { + qreal visualY = 0; + bool hasVisItem = false; + for (int i = 0; i < static_cast(m_layout.size()); ++i) { + qreal h; + auto dit = m_delegates.find(i); + if (dit != m_delegates.end() && dit->item) + h = delegateVisibleHeight(dit->item); + else + h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); + if (h > 0) { + if (hasVisItem) + visualY += m_spacing; + hasVisItem = true; + } + if (i == idx) + break; + if (h > 0) + visualY += h; + } + item->setY(visualY - m_contentY); + } item->setVisible(true); auto* att = @@ -378,6 +398,11 @@ void LazyListView::updatePolish() { att->setAdding(false); att->setReady(true); } + + // Animate from visual position to layout position + if (idx >= 0 && idx < static_cast(m_layout.size())) + item->setProperty("y", m_layout[idx].targetY - m_contentY); + polish(); }); } From d530e209b37fd4881a490bbfa8af39cd70fd325a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:10:20 +1000 Subject: [PATCH 267/409] feat: add c++ settings --- plugin/src/Caelestia/CMakeLists.txt | 4 +- plugin/src/Caelestia/Config/CMakeLists.txt | 27 ++ .../src/Caelestia/Config/advancedconfig.hpp | 235 ++++++++++++++++++ .../src/Caelestia/Config/appearanceconfig.hpp | 207 +++++++++++++++ .../src/Caelestia/Config/backgroundconfig.hpp | 74 ++++++ plugin/src/Caelestia/Config/barconfig.hpp | 133 ++++++++++ plugin/src/Caelestia/Config/borderconfig.hpp | 26 ++ plugin/src/Caelestia/Config/config.cpp | 138 ++++++++++ plugin/src/Caelestia/Config/config.hpp | 94 +++++++ plugin/src/Caelestia/Config/configobject.cpp | 107 ++++++++ plugin/src/Caelestia/Config/configobject.hpp | 63 +++++ .../Caelestia/Config/controlcenterconfig.hpp | 17 ++ .../src/Caelestia/Config/dashboardconfig.hpp | 36 +++ plugin/src/Caelestia/Config/generalconfig.hpp | 60 +++++ .../src/Caelestia/Config/launcherconfig.hpp | 46 ++++ plugin/src/Caelestia/Config/lockconfig.hpp | 19 ++ plugin/src/Caelestia/Config/notifsconfig.hpp | 25 ++ plugin/src/Caelestia/Config/osdconfig.hpp | 19 ++ plugin/src/Caelestia/Config/serviceconfig.hpp | 32 +++ plugin/src/Caelestia/Config/sessionconfig.hpp | 49 ++++ plugin/src/Caelestia/Config/sidebarconfig.hpp | 17 ++ plugin/src/Caelestia/Config/userpaths.hpp | 26 ++ .../src/Caelestia/Config/utilitiesconfig.hpp | 56 +++++ plugin/src/Caelestia/Config/winfoconfig.hpp | 17 ++ 24 files changed, 1526 insertions(+), 1 deletion(-) create mode 100644 plugin/src/Caelestia/Config/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Config/advancedconfig.hpp create mode 100644 plugin/src/Caelestia/Config/appearanceconfig.hpp create mode 100644 plugin/src/Caelestia/Config/backgroundconfig.hpp create mode 100644 plugin/src/Caelestia/Config/barconfig.hpp create mode 100644 plugin/src/Caelestia/Config/borderconfig.hpp create mode 100644 plugin/src/Caelestia/Config/config.cpp create mode 100644 plugin/src/Caelestia/Config/config.hpp create mode 100644 plugin/src/Caelestia/Config/configobject.cpp create mode 100644 plugin/src/Caelestia/Config/configobject.hpp create mode 100644 plugin/src/Caelestia/Config/controlcenterconfig.hpp create mode 100644 plugin/src/Caelestia/Config/dashboardconfig.hpp create mode 100644 plugin/src/Caelestia/Config/generalconfig.hpp create mode 100644 plugin/src/Caelestia/Config/launcherconfig.hpp create mode 100644 plugin/src/Caelestia/Config/lockconfig.hpp create mode 100644 plugin/src/Caelestia/Config/notifsconfig.hpp create mode 100644 plugin/src/Caelestia/Config/osdconfig.hpp create mode 100644 plugin/src/Caelestia/Config/serviceconfig.hpp create mode 100644 plugin/src/Caelestia/Config/sessionconfig.hpp create mode 100644 plugin/src/Caelestia/Config/sidebarconfig.hpp create mode 100644 plugin/src/Caelestia/Config/userpaths.hpp create mode 100644 plugin/src/Caelestia/Config/utilitiesconfig.hpp create mode 100644 plugin/src/Caelestia/Config/winfoconfig.hpp diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 211cbb567..01f43b212 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -12,12 +12,13 @@ set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") qt_standard_project_setup(REQUIRES 6.9) function(qml_module arg_TARGET) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES") + cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;LIBRARIES") qt_add_qml_module(${arg_TARGET} URI ${arg_URI} VERSION ${VERSION} SOURCES ${arg_SOURCES} + QML_FILES ${arg_QML_FILES} ) qt_query_qml_module(${arg_TARGET} @@ -58,6 +59,7 @@ qml_module(caelestia ) add_subdirectory(Components) +add_subdirectory(Config) add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt new file mode 100644 index 000000000..9351ec414 --- /dev/null +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -0,0 +1,27 @@ +qml_module(caelestia-config + URI Caelestia.Config + SOURCES + config.hpp config.cpp + configobject.hpp + advancedconfig.hpp + appearanceconfig.hpp + backgroundconfig.hpp + barconfig.hpp + borderconfig.hpp + controlcenterconfig.hpp + dashboardconfig.hpp + generalconfig.hpp + launcherconfig.hpp + lockconfig.hpp + notifsconfig.hpp + osdconfig.hpp + serviceconfig.hpp + sessionconfig.hpp + sidebarconfig.hpp + userpaths.hpp + utilitiesconfig.hpp + winfoconfig.hpp + configobject.cpp + LIBRARIES + Qt::Quick +) diff --git a/plugin/src/Caelestia/Config/advancedconfig.hpp b/plugin/src/Caelestia/Config/advancedconfig.hpp new file mode 100644 index 000000000..57352a32e --- /dev/null +++ b/plugin/src/Caelestia/Config/advancedconfig.hpp @@ -0,0 +1,235 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class AnimCurves : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QList, emphasized) + CONFIG_PROPERTY(QList, emphasizedAccel) + CONFIG_PROPERTY(QList, emphasizedDecel) + CONFIG_PROPERTY(QList, standard) + CONFIG_PROPERTY(QList, standardAccel) + CONFIG_PROPERTY(QList, standardDecel) + CONFIG_PROPERTY(QList, expressiveFastSpatial) + CONFIG_PROPERTY(QList, expressiveDefaultSpatial) + CONFIG_PROPERTY(QList, expressiveSlowSpatial) + +public: + explicit AnimCurves(QObject* parent = nullptr) + : ConfigObject(parent) + , m_emphasized({ 0.05, 0, 2.0 / 15.0, 0.06, 1.0 / 6.0, 0.4, 5.0 / 24.0, 0.82, 0.25, 1, 1, 1 }) + , m_emphasizedAccel({ 0.3, 0, 0.8, 0.15, 1, 1 }) + , m_emphasizedDecel({ 0.05, 0.7, 0.1, 1, 1, 1 }) + , m_standard({ 0.2, 0, 0, 1, 1, 1 }) + , m_standardAccel({ 0.3, 0, 1, 1, 1, 1 }) + , m_standardDecel({ 0, 0, 0, 1, 1, 1 }) + , m_expressiveFastSpatial({ 0.42, 1.67, 0.21, 0.9, 1, 1 }) + , m_expressiveDefaultSpatial({ 0.38, 1.21, 0.22, 1, 1, 1 }) + , m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) {} +}; + +class AdvancedAppearance : public ConfigObject { + Q_OBJECT + CONFIG_SUBOBJECT(AnimCurves, curves) + +public: + explicit AdvancedAppearance(QObject* parent = nullptr) + : ConfigObject(parent) + , m_curves(new AnimCurves(this)) {} +}; + +class BarSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, innerWidth, 40) + CONFIG_PROPERTY(int, windowPreviewSize, 400) + CONFIG_PROPERTY(int, trayMenuWidth, 300) + CONFIG_PROPERTY(int, batteryWidth, 250) + CONFIG_PROPERTY(int, networkWidth, 320) + CONFIG_PROPERTY(int, kbLayoutWidth, 320) + +public: + explicit BarSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class AdvancedDashboard : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, showDashboard, true) + CONFIG_PROPERTY(bool, showMedia, true) + CONFIG_PROPERTY(bool, showPerformance, true) + CONFIG_PROPERTY(bool, showWeather, true) + + Q_PROPERTY(int tabIndicatorHeight READ tabIndicatorHeight CONSTANT) + Q_PROPERTY(int tabIndicatorSpacing READ tabIndicatorSpacing CONSTANT) + Q_PROPERTY(int infoWidth READ infoWidth CONSTANT) + Q_PROPERTY(int infoIconSize READ infoIconSize CONSTANT) + Q_PROPERTY(int dateTimeWidth READ dateTimeWidth CONSTANT) + Q_PROPERTY(int mediaWidth READ mediaWidth CONSTANT) + Q_PROPERTY(int mediaProgressSweep READ mediaProgressSweep CONSTANT) + Q_PROPERTY(int mediaProgressThickness READ mediaProgressThickness CONSTANT) + Q_PROPERTY(int resourceProgessThickness READ resourceProgessThickness CONSTANT) + Q_PROPERTY(int weatherWidth READ weatherWidth CONSTANT) + Q_PROPERTY(int mediaCoverArtSize READ mediaCoverArtSize CONSTANT) + Q_PROPERTY(int mediaVisualiserSize READ mediaVisualiserSize CONSTANT) + Q_PROPERTY(int resourceSize READ resourceSize CONSTANT) + +public: + explicit AdvancedDashboard(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] static int tabIndicatorHeight() { return 3; } + + [[nodiscard]] static int tabIndicatorSpacing() { return 5; } + + [[nodiscard]] static int infoWidth() { return 200; } + + [[nodiscard]] static int infoIconSize() { return 25; } + + [[nodiscard]] static int dateTimeWidth() { return 110; } + + [[nodiscard]] static int mediaWidth() { return 200; } + + [[nodiscard]] static int mediaProgressSweep() { return 180; } + + [[nodiscard]] static int mediaProgressThickness() { return 8; } + + [[nodiscard]] static int resourceProgessThickness() { return 10; } + + [[nodiscard]] static int weatherWidth() { return 250; } + + [[nodiscard]] static int mediaCoverArtSize() { return 150; } + + [[nodiscard]] static int mediaVisualiserSize() { return 80; } + + [[nodiscard]] static int resourceSize() { return 200; } +}; + +class LauncherSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, itemWidth, 600) + CONFIG_PROPERTY(int, itemHeight, 57) + CONFIG_PROPERTY(int, wallpaperWidth, 280) + CONFIG_PROPERTY(int, wallpaperHeight, 200) + +public: + explicit LauncherSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class NotifsSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, width, 400) + CONFIG_PROPERTY(int, image, 41) + CONFIG_PROPERTY(int, badge, 20) + +public: + explicit NotifsSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class OsdSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, sliderWidth, 30) + CONFIG_PROPERTY(int, sliderHeight, 150) + +public: + explicit OsdSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SessionSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, button, 80) + +public: + explicit SessionSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SidebarSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, width, 430) + +public: + explicit SidebarSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class UtilitiesSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, width, 430) + CONFIG_PROPERTY(int, toastWidth, 430) + +public: + explicit UtilitiesSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class LockSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, heightMult, 0.7) + CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) + CONFIG_PROPERTY(int, centerWidth, 600) + +public: + explicit LockSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class WInfoSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, heightMult, 0.7) + CONFIG_PROPERTY(qreal, detailsWidth, 500) + +public: + explicit WInfoSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class ControlCenterSizes : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, heightMult, 0.7) + CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) + +public: + explicit ControlCenterSizes(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class AdvancedConfig : public ConfigObject { + Q_OBJECT + CONFIG_SUBOBJECT(AdvancedAppearance, appearance) + CONFIG_SUBOBJECT(BarSizes, bar) + CONFIG_SUBOBJECT(AdvancedDashboard, dashboard) + CONFIG_SUBOBJECT(LauncherSizes, launcher) + CONFIG_SUBOBJECT(NotifsSizes, notifs) + CONFIG_SUBOBJECT(OsdSizes, osd) + CONFIG_SUBOBJECT(SessionSizes, session) + CONFIG_SUBOBJECT(SidebarSizes, sidebar) + CONFIG_SUBOBJECT(UtilitiesSizes, utilities) + CONFIG_SUBOBJECT(LockSizes, lock) + CONFIG_SUBOBJECT(WInfoSizes, winfo) + CONFIG_SUBOBJECT(ControlCenterSizes, controlCenter) + +public: + explicit AdvancedConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_appearance(new AdvancedAppearance(this)) + , m_bar(new BarSizes(this)) + , m_dashboard(new AdvancedDashboard(this)) + , m_launcher(new LauncherSizes(this)) + , m_notifs(new NotifsSizes(this)) + , m_osd(new OsdSizes(this)) + , m_session(new SessionSizes(this)) + , m_sidebar(new SidebarSizes(this)) + , m_utilities(new UtilitiesSizes(this)) + , m_lock(new LockSizes(this)) + , m_winfo(new WInfoSizes(this)) + , m_controlCenter(new ControlCenterSizes(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp new file mode 100644 index 000000000..4d4acc084 --- /dev/null +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -0,0 +1,207 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class AppearanceRounding : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY scaleChanged) + Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) + Q_PROPERTY(int large READ large NOTIFY scaleChanged) + Q_PROPERTY(int full READ full NOTIFY scaleChanged) + +public: + explicit AppearanceRounding(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] int small() const { return static_cast(12 * m_scale); } + + [[nodiscard]] int normal() const { return static_cast(17 * m_scale); } + + [[nodiscard]] int large() const { return static_cast(25 * m_scale); } + + [[nodiscard]] int full() const { return static_cast(1000 * m_scale); } +}; + +class AppearanceSpacing : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY scaleChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged) + Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) + Q_PROPERTY(int larger READ larger NOTIFY scaleChanged) + Q_PROPERTY(int large READ large NOTIFY scaleChanged) + +public: + explicit AppearanceSpacing(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] int small() const { return static_cast(7 * m_scale); } + + [[nodiscard]] int smaller() const { return static_cast(10 * m_scale); } + + [[nodiscard]] int normal() const { return static_cast(12 * m_scale); } + + [[nodiscard]] int larger() const { return static_cast(15 * m_scale); } + + [[nodiscard]] int large() const { return static_cast(20 * m_scale); } +}; + +class AppearancePadding : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY scaleChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged) + Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) + Q_PROPERTY(int larger READ larger NOTIFY scaleChanged) + Q_PROPERTY(int large READ large NOTIFY scaleChanged) + +public: + explicit AppearancePadding(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] int small() const { return static_cast(5 * m_scale); } + + [[nodiscard]] int smaller() const { return static_cast(7 * m_scale); } + + [[nodiscard]] int normal() const { return static_cast(10 * m_scale); } + + [[nodiscard]] int larger() const { return static_cast(12 * m_scale); } + + [[nodiscard]] int large() const { return static_cast(15 * m_scale); } +}; + +class FontFamily : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QString, sans, QStringLiteral("Rubik")) + CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF")) + CONFIG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) + CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) + +public: + explicit FontFamily(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class FontSize : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY scaleChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged) + Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) + Q_PROPERTY(int larger READ larger NOTIFY scaleChanged) + Q_PROPERTY(int large READ large NOTIFY scaleChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged) + +public: + explicit FontSize(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] int small() const { return static_cast(11 * m_scale); } + + [[nodiscard]] int smaller() const { return static_cast(12 * m_scale); } + + [[nodiscard]] int normal() const { return static_cast(13 * m_scale); } + + [[nodiscard]] int larger() const { return static_cast(15 * m_scale); } + + [[nodiscard]] int large() const { return static_cast(18 * m_scale); } + + [[nodiscard]] int extraLarge() const { return static_cast(28 * m_scale); } +}; + +class AppearanceFont : public ConfigObject { + Q_OBJECT + CONFIG_SUBOBJECT(FontFamily, family) + CONFIG_SUBOBJECT(FontSize, size) + +public: + explicit AppearanceFont(QObject* parent = nullptr) + : ConfigObject(parent) + , m_family(new FontFamily(this)) + , m_size(new FontSize(this)) {} +}; + +class AnimDurations : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY scaleChanged) + Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) + Q_PROPERTY(int large READ large NOTIFY scaleChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged) + Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY scaleChanged) + Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY scaleChanged) + Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY scaleChanged) + +public: + explicit AnimDurations(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] int small() const { return static_cast(200 * m_scale); } + + [[nodiscard]] int normal() const { return static_cast(400 * m_scale); } + + [[nodiscard]] int large() const { return static_cast(600 * m_scale); } + + [[nodiscard]] int extraLarge() const { return static_cast(1000 * m_scale); } + + [[nodiscard]] int expressiveFastSpatial() const { return static_cast(350 * m_scale); } + + [[nodiscard]] int expressiveDefaultSpatial() const { return static_cast(500 * m_scale); } + + [[nodiscard]] int expressiveSlowSpatial() const { return static_cast(650 * m_scale); } +}; + +class AppearanceAnim : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) + CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) + CONFIG_SUBOBJECT(AnimDurations, durations) + +public: + explicit AppearanceAnim(QObject* parent = nullptr) + : ConfigObject(parent) + , m_durations(new AnimDurations(this)) {} +}; + +class AppearanceTransparency : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(qreal, base, 0.85) + CONFIG_PROPERTY(qreal, layers, 0.4) + +public: + explicit AppearanceTransparency(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class AppearanceConfig : public ConfigObject { + Q_OBJECT + CONFIG_SUBOBJECT(AppearanceRounding, rounding) + CONFIG_SUBOBJECT(AppearanceSpacing, spacing) + CONFIG_SUBOBJECT(AppearancePadding, padding) + CONFIG_SUBOBJECT(AppearanceFont, font) + CONFIG_SUBOBJECT(AppearanceAnim, anim) + CONFIG_SUBOBJECT(AppearanceTransparency, transparency) + +public: + explicit AppearanceConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_rounding(new AppearanceRounding(this)) + , m_spacing(new AppearanceSpacing(this)) + , m_padding(new AppearancePadding(this)) + , m_font(new AppearanceFont(this)) + , m_anim(new AppearanceAnim(this)) + , m_transparency(new AppearanceTransparency(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/backgroundconfig.hpp b/plugin/src/Caelestia/Config/backgroundconfig.hpp new file mode 100644 index 000000000..32440cc3c --- /dev/null +++ b/plugin/src/Caelestia/Config/backgroundconfig.hpp @@ -0,0 +1,74 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class DesktopClockBackground : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(qreal, opacity, 0.7) + CONFIG_PROPERTY(bool, blur, true) + +public: + explicit DesktopClockBackground(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class DesktopClockShadow : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(qreal, opacity, 0.7) + CONFIG_PROPERTY(qreal, blur, 0.4) + +public: + explicit DesktopClockShadow(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class DesktopClock : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(qreal, scale, 1.0) + CONFIG_PROPERTY(QString, position, QStringLiteral("bottom-right")) + CONFIG_PROPERTY(bool, invertColors, false) + CONFIG_SUBOBJECT(DesktopClockBackground, background) + CONFIG_SUBOBJECT(DesktopClockShadow, shadow) + +public: + explicit DesktopClock(QObject* parent = nullptr) + : ConfigObject(parent) + , m_background(new DesktopClockBackground(this)) + , m_shadow(new DesktopClockShadow(this)) {} +}; + +class BackgroundVisualiser : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(bool, autoHide, true) + CONFIG_PROPERTY(bool, blur, false) + CONFIG_PROPERTY(qreal, rounding, 1) + CONFIG_PROPERTY(qreal, spacing, 1) + +public: + explicit BackgroundVisualiser(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BackgroundConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(bool, wallpaperEnabled, true) + CONFIG_SUBOBJECT(DesktopClock, desktopClock) + CONFIG_SUBOBJECT(BackgroundVisualiser, visualiser) + +public: + explicit BackgroundConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_desktopClock(new DesktopClock(this)) + , m_visualiser(new BackgroundVisualiser(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp new file mode 100644 index 000000000..712c047f7 --- /dev/null +++ b/plugin/src/Caelestia/Config/barconfig.hpp @@ -0,0 +1,133 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +class BarScrollActions : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, workspaces, true) + CONFIG_PROPERTY(bool, volume, true) + CONFIG_PROPERTY(bool, brightness, true) + +public: + explicit BarScrollActions(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarPopouts : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, activeWindow, true) + CONFIG_PROPERTY(bool, tray, true) + CONFIG_PROPERTY(bool, statusIcons, true) + +public: + explicit BarPopouts(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarWorkspaces : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, shown, 5) + CONFIG_PROPERTY(bool, activeIndicator, true) + CONFIG_PROPERTY(bool, occupiedBg, false) + CONFIG_PROPERTY(bool, showWindows, true) + CONFIG_PROPERTY(bool, showWindowsOnSpecialWorkspaces, true) + CONFIG_PROPERTY(int, maxWindowIcons, 0) + CONFIG_PROPERTY(bool, activeTrail, false) + CONFIG_PROPERTY(bool, perMonitorWorkspaces, true) + CONFIG_PROPERTY(QString, label, QStringLiteral(" ")) + CONFIG_PROPERTY(QString, occupiedLabel, QStringLiteral("\U000f06af")) + CONFIG_PROPERTY(QString, activeLabel, QStringLiteral("\U000f06af")) + CONFIG_PROPERTY(QString, capitalisation, QStringLiteral("preserve")) + CONFIG_PROPERTY(QVariantList, specialWorkspaceIcons) + CONFIG_PROPERTY(QVariantList, windowIcons) + +public: + explicit BarWorkspaces(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarActiveWindow : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, compact, false) + CONFIG_PROPERTY(bool, inverted, false) + CONFIG_PROPERTY(bool, showOnHover, true) + +public: + explicit BarActiveWindow(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarTray : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, background, false) + CONFIG_PROPERTY(bool, recolour, false) + CONFIG_PROPERTY(bool, compact, false) + CONFIG_PROPERTY(QVariantList, iconSubs) + CONFIG_PROPERTY(QStringList, hiddenIcons) + +public: + explicit BarTray(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarStatus : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, showAudio, false) + CONFIG_PROPERTY(bool, showMicrophone, false) + CONFIG_PROPERTY(bool, showKbLayout, false) + CONFIG_PROPERTY(bool, showNetwork, true) + CONFIG_PROPERTY(bool, showWifi, true) + CONFIG_PROPERTY(bool, showBluetooth, true) + CONFIG_PROPERTY(bool, showBattery, true) + CONFIG_PROPERTY(bool, showLockStatus, true) + +public: + explicit BarStatus(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarClock : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, background, false) + CONFIG_PROPERTY(bool, showDate, false) + CONFIG_PROPERTY(bool, showIcon, true) + +public: + explicit BarClock(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, persistent, true) + CONFIG_PROPERTY(bool, showOnHover, true) + CONFIG_PROPERTY(int, dragThreshold, 20) + CONFIG_SUBOBJECT(BarScrollActions, scrollActions) + CONFIG_SUBOBJECT(BarPopouts, popouts) + CONFIG_SUBOBJECT(BarWorkspaces, workspaces) + CONFIG_SUBOBJECT(BarActiveWindow, activeWindow) + CONFIG_SUBOBJECT(BarTray, tray) + CONFIG_SUBOBJECT(BarStatus, status) + CONFIG_SUBOBJECT(BarClock, clock) + CONFIG_PROPERTY(QVariantList, entries) + CONFIG_PROPERTY(QStringList, excludedScreens) + +public: + explicit BarConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_scrollActions(new BarScrollActions(this)) + , m_popouts(new BarPopouts(this)) + , m_workspaces(new BarWorkspaces(this)) + , m_activeWindow(new BarActiveWindow(this)) + , m_tray(new BarTray(this)) + , m_status(new BarStatus(this)) + , m_clock(new BarClock(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/borderconfig.hpp b/plugin/src/Caelestia/Config/borderconfig.hpp new file mode 100644 index 000000000..9bc2966c5 --- /dev/null +++ b/plugin/src/Caelestia/Config/borderconfig.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class BorderConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, thickness, 10) + CONFIG_PROPERTY(int, rounding, 25) + + Q_PROPERTY(int minThickness READ minThickness CONSTANT) + Q_PROPERTY(int clampedThickness READ clampedThickness NOTIFY thicknessChanged) + +public: + explicit BorderConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] static int minThickness() { return 2; } + + [[nodiscard]] int clampedThickness() const { return std::max(minThickness(), m_thickness); } +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp new file mode 100644 index 000000000..01df37044 --- /dev/null +++ b/plugin/src/Caelestia/Config/config.cpp @@ -0,0 +1,138 @@ +#include "config.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace caelestia::config { + +static GlobalConfig* s_instance = nullptr; + +GlobalConfig::GlobalConfig(QObject* parent) + : ConfigObject(parent) + , m_appearance(new AppearanceConfig(this)) + , m_general(new GeneralConfig(this)) + , m_background(new BackgroundConfig(this)) + , m_bar(new BarConfig(this)) + , m_border(new BorderConfig(this)) + , m_dashboard(new DashboardConfig(this)) + , m_controlCenter(new ControlCenterConfig(this)) + , m_launcher(new LauncherConfig(this)) + , m_notifs(new NotifsConfig(this)) + , m_osd(new OsdConfig(this)) + , m_session(new SessionConfig(this)) + , m_winfo(new WInfoConfig(this)) + , m_lock(new LockConfig(this)) + , m_utilities(new UtilitiesConfig(this)) + , m_sidebar(new SidebarConfig(this)) + , m_services(new ServiceConfig(this)) + , m_paths(new UserPaths(this)) + , m_advanced(new AdvancedConfig(this)) { + s_instance = this; + + m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + + QStringLiteral("/caelestia/shell.json"); + + m_saveTimer.setSingleShot(true); + m_saveTimer.setInterval(500); + connect(&m_saveTimer, &QTimer::timeout, this, &GlobalConfig::writeToFile); + + m_cooldownTimer.setSingleShot(true); + m_cooldownTimer.setInterval(2000); + connect(&m_cooldownTimer, &QTimer::timeout, this, [this] { + m_recentlySaved = false; + emit recentlySavedChanged(); + }); + + connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &GlobalConfig::onFileChanged); + + reload(); + + if (QFile::exists(m_configPath)) { + m_watcher.addPath(m_configPath); + } +} + +GlobalConfig* GlobalConfig::instance() { + return s_instance; +} + +bool GlobalConfig::recentlySaved() const { + return m_recentlySaved; +} + +void GlobalConfig::save() { + m_saveTimer.start(); + m_recentlySaved = true; + emit recentlySavedChanged(); + m_cooldownTimer.start(); +} + +void GlobalConfig::reload() { + QFile file(m_configPath); + + if (!file.open(QIODevice::ReadOnly)) { + qWarning() << "Config: failed to open" << m_configPath; + return; + } + + load(file.readAll()); +} + +void GlobalConfig::load(const QByteArray& contents) { + QJsonParseError error{}; + auto doc = QJsonDocument::fromJson(contents, &error); + + if (error.error != QJsonParseError::NoError) { + qWarning() << "Config: failed to parse JSON:" << error.errorString(); + return; + } + + loadFromJson(doc.object()); +} + +void GlobalConfig::onFileChanged() { + if (!m_watcher.files().contains(m_configPath)) { + m_watcher.addPath(m_configPath); + } + + if (!m_recentlySaved) { + reload(); + } +} + +void GlobalConfig::writeToFile() { + QDir().mkpath(QFileInfo(m_configPath).absolutePath()); + + QFile file(m_configPath); + + if (!file.open(QIODevice::WriteOnly)) { + qWarning() << "Config: failed to write" << m_configPath; + return; + } + + file.write(QJsonDocument(toJsonObject()).toJson(QJsonDocument::Indented)); +} + +// Config + +Config::Config(QQuickItem* parent) + : QQuickItem(parent) {} + +Config* Config::qmlAttachedProperties(QObject* object) { + auto* item = qobject_cast(object); + + while (item) { + if (auto* config = qobject_cast(item)) + return config; + item = item->parentItem(); + } + + return nullptr; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp new file mode 100644 index 000000000..e989d4c30 --- /dev/null +++ b/plugin/src/Caelestia/Config/config.hpp @@ -0,0 +1,94 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +#include "advancedconfig.hpp" +#include "appearanceconfig.hpp" +#include "backgroundconfig.hpp" +#include "barconfig.hpp" +#include "borderconfig.hpp" +#include "controlcenterconfig.hpp" +#include "dashboardconfig.hpp" +#include "generalconfig.hpp" +#include "launcherconfig.hpp" +#include "lockconfig.hpp" +#include "notifsconfig.hpp" +#include "osdconfig.hpp" +#include "serviceconfig.hpp" +#include "sessionconfig.hpp" +#include "sidebarconfig.hpp" +#include "userpaths.hpp" +#include "utilitiesconfig.hpp" +#include "winfoconfig.hpp" + +namespace caelestia::config { + +class GlobalConfig : public ConfigObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + CONFIG_SUBOBJECT(AppearanceConfig, appearance) + CONFIG_SUBOBJECT(GeneralConfig, general) + CONFIG_SUBOBJECT(BackgroundConfig, background) + CONFIG_SUBOBJECT(BarConfig, bar) + CONFIG_SUBOBJECT(BorderConfig, border) + CONFIG_SUBOBJECT(DashboardConfig, dashboard) + CONFIG_SUBOBJECT(ControlCenterConfig, controlCenter) + CONFIG_SUBOBJECT(LauncherConfig, launcher) + CONFIG_SUBOBJECT(NotifsConfig, notifs) + CONFIG_SUBOBJECT(OsdConfig, osd) + CONFIG_SUBOBJECT(SessionConfig, session) + CONFIG_SUBOBJECT(WInfoConfig, winfo) + CONFIG_SUBOBJECT(LockConfig, lock) + CONFIG_SUBOBJECT(UtilitiesConfig, utilities) + CONFIG_SUBOBJECT(SidebarConfig, sidebar) + CONFIG_SUBOBJECT(ServiceConfig, services) + CONFIG_SUBOBJECT(UserPaths, paths) + CONFIG_SUBOBJECT(AdvancedConfig, advanced) + + Q_PROPERTY(bool recentlySaved READ recentlySaved NOTIFY recentlySavedChanged) + +public: + explicit GlobalConfig(QObject* parent = nullptr); + + static GlobalConfig* instance(); + + [[nodiscard]] bool recentlySaved() const; + + Q_INVOKABLE void save(); + Q_INVOKABLE void reload(); + +signals: + void recentlySavedChanged(); + +private: + void load(const QByteArray& contents); + void onFileChanged(); + void writeToFile(); + + bool m_recentlySaved = false; + QString m_configPath; + QFileSystemWatcher m_watcher; + QTimer m_saveTimer; + QTimer m_cooldownTimer; +}; + +class Config : public QQuickItem { + Q_OBJECT + QML_ELEMENT + QML_ATTACHED(Config) + +public: + explicit Config(QQuickItem* parent = nullptr); + + static Config* qmlAttachedProperties(QObject* object); +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp new file mode 100644 index 000000000..7533b0f57 --- /dev/null +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -0,0 +1,107 @@ +#include "configobject.hpp" + +#include +#include +#include +#include +#include +#include + +namespace caelestia::config { + +Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg) + +ConfigObject::ConfigObject(QObject* parent) + : QObject(parent) {} + +void ConfigObject::loadFromJson(const QJsonObject& obj) { + const auto* meta = metaObject(); + + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + const auto key = QString::fromUtf8(prop.name()); + + if (!obj.contains(key)) + continue; + + const auto jsonVal = obj.value(key); + + // Recurse into sub-objects + auto current = prop.read(this); + auto* subObj = current.value(); + + if (subObj) { + subObj->loadFromJson(jsonVal.toObject()); + continue; + } + + // Skip read-only properties + if (!prop.isWritable()) + continue; + + // Handle QStringList explicitly (QJsonArray → QStringList needs manual conversion) + if (prop.metaType().id() == QMetaType::QStringList) { + QStringList list; + const auto jsonArr = jsonVal.toArray(); + for (const auto& v : jsonArr) + list.append(v.toString()); + prop.write(this, QVariant::fromValue(list)); + continue; + } + + // For all other types, let Qt's variant conversion handle it + prop.write(this, jsonVal.toVariant()); + } +} + +QJsonObject ConfigObject::toJsonObject() const { + QJsonObject obj; + const auto* meta = metaObject(); + + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + const auto prop = meta->property(i); + + if (!prop.isReadable()) + continue; + + const auto key = QString::fromUtf8(prop.name()); + const auto value = prop.read(this); + + // Recurse into sub-objects + if (value.canView()) { + auto* const subObj = value.value(); + if (subObj) + obj.insert(key, subObj->toJsonObject()); + else + qCWarning(lcConfig, "Unable to get sub-object when serializing config object"); + continue; + } + + // Skip read-only properties (computed values) + if (!prop.isWritable()) + continue; + + // Handle QStringList explicitly + if (prop.metaType().id() == QMetaType::QStringList) { + QJsonArray arr; + const auto strList = value.toStringList(); + for (const auto& s : strList) + arr.append(s); + obj.insert(key, arr); + continue; + } + + // Handle QVariantList explicitly + if (prop.metaType().id() == QMetaType::QVariantList) { + obj.insert(key, QJsonArray::fromVariantList(value.toList())); + continue; + } + + // Default case + obj.insert(key, QJsonValue::fromVariant(value)); + } + + return obj; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp new file mode 100644 index 000000000..2f8f443e9 --- /dev/null +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include +#include +#include + +#include + +// Declares a serialized config property with getter, setter (change-detected), signal, and member. +#define CONFIG_PROPERTY(Type, name, ...) \ + Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ + \ +public: \ + [[nodiscard]] Type name() const { \ + return m_##name; \ + } \ + void set_##name(const Type& val) { \ + if (caelestia::config::ConfigObject::updateMember(m_##name, val)) \ + emit name##Changed(); \ + } \ + Q_SIGNAL void name##Changed(); \ + \ +private: \ + Type m_##name __VA_OPT__(= __VA_ARGS__); + +// Declares a CONSTANT sub-object property. Initialize the member in the constructor. +#define CONFIG_SUBOBJECT(Type, name) \ + Q_PROPERTY(caelestia::config::Type* name READ name CONSTANT) \ + \ +public: \ + [[nodiscard]] Type* name() const { \ + return m_##name; \ + } \ + \ +private: \ + Type* m_##name = nullptr; + +namespace caelestia::config { + +class ConfigObject : public QObject { + Q_OBJECT + QML_ANONYMOUS + +public: + explicit ConfigObject(QObject* parent = nullptr); + + void loadFromJson(const QJsonObject& obj); + [[nodiscard]] QJsonObject toJsonObject() const; + + template static bool updateMember(T& member, const T& value) { + if constexpr (std::is_floating_point_v) { + if (qFuzzyCompare(member + 1.0, value + 1.0)) + return false; + } else { + if (member == value) + return false; + } + member = value; + return true; + } +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/controlcenterconfig.hpp b/plugin/src/Caelestia/Config/controlcenterconfig.hpp new file mode 100644 index 000000000..d827f7cc2 --- /dev/null +++ b/plugin/src/Caelestia/Config/controlcenterconfig.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +// ControlCenterConfig has no serialized properties (serializer returns {}) +// All properties are in AdvancedConfig.controlCenter +class ControlCenterConfig : public ConfigObject { + Q_OBJECT + +public: + explicit ControlCenterConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/dashboardconfig.hpp b/plugin/src/Caelestia/Config/dashboardconfig.hpp new file mode 100644 index 000000000..f6b97ac6e --- /dev/null +++ b/plugin/src/Caelestia/Config/dashboardconfig.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class DashboardPerformance : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, showBattery, true) + CONFIG_PROPERTY(bool, showGpu, true) + CONFIG_PROPERTY(bool, showCpu, true) + CONFIG_PROPERTY(bool, showMemory, true) + CONFIG_PROPERTY(bool, showStorage, true) + CONFIG_PROPERTY(bool, showNetwork, true) + +public: + explicit DashboardPerformance(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class DashboardConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(bool, showOnHover, true) + CONFIG_PROPERTY(int, mediaUpdateInterval, 500) + CONFIG_PROPERTY(int, resourceUpdateInterval, 1000) + CONFIG_PROPERTY(int, dragThreshold, 50) + CONFIG_SUBOBJECT(DashboardPerformance, performance) + +public: + explicit DashboardConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_performance(new DashboardPerformance(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp new file mode 100644 index 000000000..8ccc90899 --- /dev/null +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -0,0 +1,60 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +class GeneralApps : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QStringList, terminal, { QStringLiteral("foot") }) + CONFIG_PROPERTY(QStringList, audio, { QStringLiteral("pavucontrol") }) + CONFIG_PROPERTY(QStringList, playback, { QStringLiteral("mpv") }) + CONFIG_PROPERTY(QStringList, explorer, { QStringLiteral("thunar") }) + +public: + explicit GeneralApps(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class GeneralIdle : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, lockBeforeSleep, true) + CONFIG_PROPERTY(bool, inhibitWhenAudio, true) + CONFIG_PROPERTY(QVariantList, timeouts) + +public: + explicit GeneralIdle(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class GeneralBattery : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QVariantList, warnLevels) + CONFIG_PROPERTY(int, criticalLevel, 3) + +public: + explicit GeneralBattery(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class GeneralConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QString, logo) + CONFIG_PROPERTY(QStringList, excludedScreens) + CONFIG_SUBOBJECT(GeneralApps, apps) + CONFIG_SUBOBJECT(GeneralIdle, idle) + CONFIG_SUBOBJECT(GeneralBattery, battery) + +public: + explicit GeneralConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_apps(new GeneralApps(this)) + , m_idle(new GeneralIdle(this)) + , m_battery(new GeneralBattery(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp new file mode 100644 index 000000000..d430ae604 --- /dev/null +++ b/plugin/src/Caelestia/Config/launcherconfig.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +class LauncherUseFuzzy : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, apps, false) + CONFIG_PROPERTY(bool, actions, false) + CONFIG_PROPERTY(bool, schemes, false) + CONFIG_PROPERTY(bool, variants, false) + CONFIG_PROPERTY(bool, wallpapers, false) + +public: + explicit LauncherUseFuzzy(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class LauncherConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(bool, showOnHover, false) + CONFIG_PROPERTY(int, maxShown, 7) + CONFIG_PROPERTY(int, maxWallpapers, 9) + CONFIG_PROPERTY(QString, specialPrefix, QStringLiteral("@")) + CONFIG_PROPERTY(QString, actionPrefix, QStringLiteral(">")) + CONFIG_PROPERTY(bool, enableDangerousActions, false) + CONFIG_PROPERTY(int, dragThreshold, 50) + CONFIG_PROPERTY(bool, vimKeybinds, false) + CONFIG_PROPERTY(QStringList, favouriteApps) + CONFIG_PROPERTY(QStringList, hiddenApps) + CONFIG_SUBOBJECT(LauncherUseFuzzy, useFuzzy) + CONFIG_PROPERTY(QVariantList, actions) + +public: + explicit LauncherConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_useFuzzy(new LauncherUseFuzzy(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp new file mode 100644 index 000000000..a15c722a7 --- /dev/null +++ b/plugin/src/Caelestia/Config/lockconfig.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class LockConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, recolourLogo, false) + CONFIG_PROPERTY(bool, enableFprint, true) + CONFIG_PROPERTY(int, maxFprintTries, 3) + CONFIG_PROPERTY(bool, hideNotifs, false) + +public: + explicit LockConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/notifsconfig.hpp b/plugin/src/Caelestia/Config/notifsconfig.hpp new file mode 100644 index 000000000..61c9bb9e2 --- /dev/null +++ b/plugin/src/Caelestia/Config/notifsconfig.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class NotifsConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, expire, true) + CONFIG_PROPERTY(QString, fullscreen, QStringLiteral("on")) + CONFIG_PROPERTY(int, defaultExpireTimeout, 5000) + CONFIG_PROPERTY(qreal, clearThreshold, 0.3) + CONFIG_PROPERTY(int, expandThreshold, 20) + CONFIG_PROPERTY(bool, actionOnClick, false) + CONFIG_PROPERTY(int, groupPreviewNum, 3) + CONFIG_PROPERTY(bool, openExpanded, false) + +public: + explicit NotifsConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/osdconfig.hpp b/plugin/src/Caelestia/Config/osdconfig.hpp new file mode 100644 index 000000000..71cc27b44 --- /dev/null +++ b/plugin/src/Caelestia/Config/osdconfig.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class OsdConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, hideDelay, 2000) + CONFIG_PROPERTY(bool, enableBrightness, true) + CONFIG_PROPERTY(bool, enableMicrophone, false) + +public: + explicit OsdConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp new file mode 100644 index 000000000..131314c12 --- /dev/null +++ b/plugin/src/Caelestia/Config/serviceconfig.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include + +namespace caelestia::config { + +class ServiceConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QString, weatherLocation) + CONFIG_PROPERTY(bool, useFahrenheit, false) + CONFIG_PROPERTY(bool, useFahrenheitPerformance, false) + CONFIG_PROPERTY(bool, useTwelveHourClock, false) + CONFIG_PROPERTY(QString, gpuType) + CONFIG_PROPERTY(int, visualiserBars, 45) + CONFIG_PROPERTY(qreal, audioIncrement, 0.1) + CONFIG_PROPERTY(qreal, brightnessIncrement, 0.1) + CONFIG_PROPERTY(qreal, maxVolume, 1.0) + CONFIG_PROPERTY(bool, smartScheme, true) + CONFIG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify")) + CONFIG_PROPERTY(QVariantList, playerAliases) + CONFIG_PROPERTY(bool, showLyrics, false) + CONFIG_PROPERTY(QString, lyricsBackend, QStringLiteral("Auto")) + +public: + explicit ServiceConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/sessionconfig.hpp b/plugin/src/Caelestia/Config/sessionconfig.hpp new file mode 100644 index 000000000..af609512d --- /dev/null +++ b/plugin/src/Caelestia/Config/sessionconfig.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include + +namespace caelestia::config { + +class SessionIcons : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QString, logout, QStringLiteral("logout")) + CONFIG_PROPERTY(QString, shutdown, QStringLiteral("power_settings_new")) + CONFIG_PROPERTY(QString, hibernate, QStringLiteral("downloading")) + CONFIG_PROPERTY(QString, reboot, QStringLiteral("cached")) + +public: + explicit SessionIcons(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SessionCommands : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QStringList, logout, { QStringLiteral("loginctl"), QStringLiteral("terminate-user"), QString() }) + CONFIG_PROPERTY(QStringList, shutdown, { QStringLiteral("systemctl"), QStringLiteral("poweroff") }) + CONFIG_PROPERTY(QStringList, hibernate, { QStringLiteral("systemctl"), QStringLiteral("hibernate") }) + CONFIG_PROPERTY(QStringList, reboot, { QStringLiteral("systemctl"), QStringLiteral("reboot") }) + +public: + explicit SessionCommands(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SessionConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, dragThreshold, 30) + CONFIG_PROPERTY(bool, vimKeybinds, false) + CONFIG_SUBOBJECT(SessionIcons, icons) + CONFIG_SUBOBJECT(SessionCommands, commands) + +public: + explicit SessionConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_icons(new SessionIcons(this)) + , m_commands(new SessionCommands(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/sidebarconfig.hpp b/plugin/src/Caelestia/Config/sidebarconfig.hpp new file mode 100644 index 000000000..71041cfbd --- /dev/null +++ b/plugin/src/Caelestia/Config/sidebarconfig.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class SidebarConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, dragThreshold, 80) + +public: + explicit SidebarConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/userpaths.hpp b/plugin/src/Caelestia/Config/userpaths.hpp new file mode 100644 index 000000000..ec0b4880a --- /dev/null +++ b/plugin/src/Caelestia/Config/userpaths.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +class UserPaths : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(QString, wallpaperDir, + QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QStringLiteral("/Wallpapers")) + CONFIG_PROPERTY(QString, lyricsDir, QDir::homePath() + QStringLiteral("/Music/lyrics/")) + CONFIG_PROPERTY(QString, sessionGif, QStringLiteral("root:/assets/kurukuru.gif")) + CONFIG_PROPERTY(QString, mediaGif, QStringLiteral("root:/assets/bongocat.gif")) + CONFIG_PROPERTY(QString, noNotifsPic, QStringLiteral("root:/assets/dino.png")) + CONFIG_PROPERTY(QString, lockNoNotifsPic, QStringLiteral("root:/assets/dino.png")) + +public: + explicit UserPaths(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/utilitiesconfig.hpp b/plugin/src/Caelestia/Config/utilitiesconfig.hpp new file mode 100644 index 000000000..98da47ede --- /dev/null +++ b/plugin/src/Caelestia/Config/utilitiesconfig.hpp @@ -0,0 +1,56 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include + +namespace caelestia::config { + +class UtilitiesToasts : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, configLoaded, true) + CONFIG_PROPERTY(QString, fullscreen, QStringLiteral("off")) + CONFIG_PROPERTY(bool, chargingChanged, true) + CONFIG_PROPERTY(bool, gameModeChanged, true) + CONFIG_PROPERTY(bool, dndChanged, true) + CONFIG_PROPERTY(bool, audioOutputChanged, true) + CONFIG_PROPERTY(bool, audioInputChanged, true) + CONFIG_PROPERTY(bool, capsLockChanged, true) + CONFIG_PROPERTY(bool, numLockChanged, true) + CONFIG_PROPERTY(bool, kbLayoutChanged, true) + CONFIG_PROPERTY(bool, kbLimit, true) + CONFIG_PROPERTY(bool, vpnChanged, true) + CONFIG_PROPERTY(bool, nowPlaying, false) + +public: + explicit UtilitiesToasts(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class UtilitiesVpn : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(QVariantList, provider) + +public: + explicit UtilitiesVpn(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class UtilitiesConfig : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, maxToasts, 4) + CONFIG_SUBOBJECT(UtilitiesToasts, toasts) + CONFIG_SUBOBJECT(UtilitiesVpn, vpn) + CONFIG_PROPERTY(QVariantList, quickToggles) + +public: + explicit UtilitiesConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_toasts(new UtilitiesToasts(this)) + , m_vpn(new UtilitiesVpn(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/winfoconfig.hpp b/plugin/src/Caelestia/Config/winfoconfig.hpp new file mode 100644 index 000000000..91f2f234b --- /dev/null +++ b/plugin/src/Caelestia/Config/winfoconfig.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +// WInfoConfig has no serialized properties (serializer returns {}) +// All properties are in AdvancedConfig.winfo +class WInfoConfig : public ConfigObject { + Q_OBJECT + +public: + explicit WInfoConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config From 8d73d6037deb78a4abc74a15adf705a93c324cd4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:38:03 +1000 Subject: [PATCH 268/409] fix: move scale back to normal config --- plugin/src/Caelestia/Config/CMakeLists.txt | 7 +- .../src/Caelestia/Config/advancedconfig.hpp | 79 ++++++++- .../src/Caelestia/Config/appearanceconfig.cpp | 167 ++++++++++++++++++ .../src/Caelestia/Config/appearanceconfig.hpp | 142 ++++++++------- plugin/src/Caelestia/Config/config.cpp | 97 ++-------- plugin/src/Caelestia/Config/config.hpp | 25 +-- plugin/src/Caelestia/Config/configobject.cpp | 74 ++++++++ plugin/src/Caelestia/Config/configobject.hpp | 24 ++- 8 files changed, 446 insertions(+), 169 deletions(-) create mode 100644 plugin/src/Caelestia/Config/appearanceconfig.cpp diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 9351ec414..93f94b065 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -1,10 +1,10 @@ qml_module(caelestia-config URI Caelestia.Config SOURCES - config.hpp config.cpp - configobject.hpp + config.cpp + configobject.cpp + appearanceconfig.cpp advancedconfig.hpp - appearanceconfig.hpp backgroundconfig.hpp barconfig.hpp borderconfig.hpp @@ -21,7 +21,6 @@ qml_module(caelestia-config userpaths.hpp utilitiesconfig.hpp winfoconfig.hpp - configobject.cpp LIBRARIES Qt::Quick ) diff --git a/plugin/src/Caelestia/Config/advancedconfig.hpp b/plugin/src/Caelestia/Config/advancedconfig.hpp index 57352a32e..181ba0bc8 100644 --- a/plugin/src/Caelestia/Config/advancedconfig.hpp +++ b/plugin/src/Caelestia/Config/advancedconfig.hpp @@ -32,14 +32,91 @@ class AnimCurves : public ConfigObject { , m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) {} }; +class RoundingTokens : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, small, 12) + CONFIG_PROPERTY(int, normal, 17) + CONFIG_PROPERTY(int, large, 25) + CONFIG_PROPERTY(int, full, 1000) + +public: + explicit RoundingTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SpacingTokens : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, small, 7) + CONFIG_PROPERTY(int, smaller, 10) + CONFIG_PROPERTY(int, normal, 12) + CONFIG_PROPERTY(int, larger, 15) + CONFIG_PROPERTY(int, large, 20) + +public: + explicit SpacingTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class PaddingTokens : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, small, 5) + CONFIG_PROPERTY(int, smaller, 7) + CONFIG_PROPERTY(int, normal, 10) + CONFIG_PROPERTY(int, larger, 12) + CONFIG_PROPERTY(int, large, 15) + +public: + explicit PaddingTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class FontSizeTokens : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, small, 11) + CONFIG_PROPERTY(int, smaller, 12) + CONFIG_PROPERTY(int, normal, 13) + CONFIG_PROPERTY(int, larger, 15) + CONFIG_PROPERTY(int, large, 18) + CONFIG_PROPERTY(int, extraLarge, 28) + +public: + explicit FontSizeTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class AnimDurationTokens : public ConfigObject { + Q_OBJECT + CONFIG_PROPERTY(int, small, 200) + CONFIG_PROPERTY(int, normal, 400) + CONFIG_PROPERTY(int, large, 600) + CONFIG_PROPERTY(int, extraLarge, 1000) + CONFIG_PROPERTY(int, expressiveFastSpatial, 350) + CONFIG_PROPERTY(int, expressiveDefaultSpatial, 500) + CONFIG_PROPERTY(int, expressiveSlowSpatial, 650) + +public: + explicit AnimDurationTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + class AdvancedAppearance : public ConfigObject { Q_OBJECT CONFIG_SUBOBJECT(AnimCurves, curves) + CONFIG_SUBOBJECT(RoundingTokens, rounding) + CONFIG_SUBOBJECT(SpacingTokens, spacing) + CONFIG_SUBOBJECT(PaddingTokens, padding) + CONFIG_SUBOBJECT(FontSizeTokens, fontSize) + CONFIG_SUBOBJECT(AnimDurationTokens, animDurations) public: explicit AdvancedAppearance(QObject* parent = nullptr) : ConfigObject(parent) - , m_curves(new AnimCurves(this)) {} + , m_curves(new AnimCurves(this)) + , m_rounding(new RoundingTokens(this)) + , m_spacing(new SpacingTokens(this)) + , m_padding(new PaddingTokens(this)) + , m_fontSize(new FontSizeTokens(this)) + , m_animDurations(new AnimDurationTokens(this)) {} }; class BarSizes : public ConfigObject { diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp new file mode 100644 index 000000000..1b45f8f65 --- /dev/null +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -0,0 +1,167 @@ +#include "appearanceconfig.hpp" +#include "advancedconfig.hpp" + +#include + +namespace caelestia::config { + +// Helper: connect all changed signals from a token object to a single valuesChanged signal, +// plus connect the local scaleChanged signal. +template static void connectTokenSignals(Source* source, Target* target) { + const auto* meta = source->metaObject(); + + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + + if (prop.hasNotifySignal()) + QObject::connect(source, prop.notifySignal(), target, + target->metaObject()->method(target->metaObject()->indexOfSignal("valuesChanged()"))); + } + + QObject::connect(target, &Target::scaleChanged, target, &Target::valuesChanged); +} + +// AppearanceRounding + +void AppearanceRounding::bindTokens(RoundingTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AppearanceRounding::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AppearanceRounding::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AppearanceRounding::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +int AppearanceRounding::full() const { + return m_tokens ? static_cast(m_tokens->full() * m_scale) : 0; +} + +// AppearanceSpacing + +void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AppearanceSpacing::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AppearanceSpacing::smaller() const { + return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; +} + +int AppearanceSpacing::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AppearanceSpacing::larger() const { + return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +} + +int AppearanceSpacing::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +// AppearancePadding + +void AppearancePadding::bindTokens(PaddingTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AppearancePadding::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AppearancePadding::smaller() const { + return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; +} + +int AppearancePadding::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AppearancePadding::larger() const { + return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +} + +int AppearancePadding::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +// FontSize + +void FontSize::bindTokens(FontSizeTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int FontSize::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int FontSize::smaller() const { + return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; +} + +int FontSize::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int FontSize::larger() const { + return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +} + +int FontSize::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +int FontSize::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +// AnimDurations + +void AnimDurations::bindTokens(AnimDurationTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AnimDurations::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AnimDurations::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AnimDurations::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +int AnimDurations::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +int AnimDurations::expressiveFastSpatial() const { + return m_tokens ? static_cast(m_tokens->expressiveFastSpatial() * m_scale) : 0; +} + +int AnimDurations::expressiveDefaultSpatial() const { + return m_tokens ? static_cast(m_tokens->expressiveDefaultSpatial() * m_scale) : 0; +} + +int AnimDurations::expressiveSlowSpatial() const { + return m_tokens ? static_cast(m_tokens->expressiveSlowSpatial() * m_scale) : 0; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index 4d4acc084..c0094b26a 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -6,76 +6,93 @@ namespace caelestia::config { +// Forward declare token types from advancedconfig.hpp +class RoundingTokens; +class SpacingTokens; +class PaddingTokens; +class FontSizeTokens; +class AnimDurationTokens; + class AppearanceRounding : public ConfigObject { Q_OBJECT CONFIG_PROPERTY(qreal, scale, 1) - Q_PROPERTY(int small READ small NOTIFY scaleChanged) - Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) - Q_PROPERTY(int large READ large NOTIFY scaleChanged) - Q_PROPERTY(int full READ full NOTIFY scaleChanged) + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int full READ full NOTIFY valuesChanged) public: explicit AppearanceRounding(QObject* parent = nullptr) : ConfigObject(parent) {} - [[nodiscard]] int small() const { return static_cast(12 * m_scale); } + void bindTokens(RoundingTokens* tokens); - [[nodiscard]] int normal() const { return static_cast(17 * m_scale); } + [[nodiscard]] int small() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int large() const; + [[nodiscard]] int full() const; - [[nodiscard]] int large() const { return static_cast(25 * m_scale); } + Q_SIGNAL void valuesChanged(); - [[nodiscard]] int full() const { return static_cast(1000 * m_scale); } +private: + RoundingTokens* m_tokens = nullptr; }; class AppearanceSpacing : public ConfigObject { Q_OBJECT CONFIG_PROPERTY(qreal, scale, 1) - Q_PROPERTY(int small READ small NOTIFY scaleChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged) - Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) - Q_PROPERTY(int larger READ larger NOTIFY scaleChanged) - Q_PROPERTY(int large READ large NOTIFY scaleChanged) + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) public: explicit AppearanceSpacing(QObject* parent = nullptr) : ConfigObject(parent) {} - [[nodiscard]] int small() const { return static_cast(7 * m_scale); } - - [[nodiscard]] int smaller() const { return static_cast(10 * m_scale); } + void bindTokens(SpacingTokens* tokens); - [[nodiscard]] int normal() const { return static_cast(12 * m_scale); } + [[nodiscard]] int small() const; + [[nodiscard]] int smaller() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int larger() const; + [[nodiscard]] int large() const; - [[nodiscard]] int larger() const { return static_cast(15 * m_scale); } + Q_SIGNAL void valuesChanged(); - [[nodiscard]] int large() const { return static_cast(20 * m_scale); } +private: + SpacingTokens* m_tokens = nullptr; }; class AppearancePadding : public ConfigObject { Q_OBJECT CONFIG_PROPERTY(qreal, scale, 1) - Q_PROPERTY(int small READ small NOTIFY scaleChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged) - Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) - Q_PROPERTY(int larger READ larger NOTIFY scaleChanged) - Q_PROPERTY(int large READ large NOTIFY scaleChanged) + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) public: explicit AppearancePadding(QObject* parent = nullptr) : ConfigObject(parent) {} - [[nodiscard]] int small() const { return static_cast(5 * m_scale); } + void bindTokens(PaddingTokens* tokens); - [[nodiscard]] int smaller() const { return static_cast(7 * m_scale); } + [[nodiscard]] int small() const; + [[nodiscard]] int smaller() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int larger() const; + [[nodiscard]] int large() const; - [[nodiscard]] int normal() const { return static_cast(10 * m_scale); } + Q_SIGNAL void valuesChanged(); - [[nodiscard]] int larger() const { return static_cast(12 * m_scale); } - - [[nodiscard]] int large() const { return static_cast(15 * m_scale); } +private: + PaddingTokens* m_tokens = nullptr; }; class FontFamily : public ConfigObject { @@ -94,28 +111,30 @@ class FontSize : public ConfigObject { Q_OBJECT CONFIG_PROPERTY(qreal, scale, 1) - Q_PROPERTY(int small READ small NOTIFY scaleChanged) - Q_PROPERTY(int smaller READ smaller NOTIFY scaleChanged) - Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) - Q_PROPERTY(int larger READ larger NOTIFY scaleChanged) - Q_PROPERTY(int large READ large NOTIFY scaleChanged) - Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged) + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) public: explicit FontSize(QObject* parent = nullptr) : ConfigObject(parent) {} - [[nodiscard]] int small() const { return static_cast(11 * m_scale); } - - [[nodiscard]] int smaller() const { return static_cast(12 * m_scale); } + void bindTokens(FontSizeTokens* tokens); - [[nodiscard]] int normal() const { return static_cast(13 * m_scale); } + [[nodiscard]] int small() const; + [[nodiscard]] int smaller() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int larger() const; + [[nodiscard]] int large() const; + [[nodiscard]] int extraLarge() const; - [[nodiscard]] int larger() const { return static_cast(15 * m_scale); } + Q_SIGNAL void valuesChanged(); - [[nodiscard]] int large() const { return static_cast(18 * m_scale); } - - [[nodiscard]] int extraLarge() const { return static_cast(28 * m_scale); } +private: + FontSizeTokens* m_tokens = nullptr; }; class AppearanceFont : public ConfigObject { @@ -134,31 +153,32 @@ class AnimDurations : public ConfigObject { Q_OBJECT CONFIG_PROPERTY(qreal, scale, 1) - Q_PROPERTY(int small READ small NOTIFY scaleChanged) - Q_PROPERTY(int normal READ normal NOTIFY scaleChanged) - Q_PROPERTY(int large READ large NOTIFY scaleChanged) - Q_PROPERTY(int extraLarge READ extraLarge NOTIFY scaleChanged) - Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY scaleChanged) - Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY scaleChanged) - Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY scaleChanged) + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY valuesChanged) + Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY valuesChanged) + Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY valuesChanged) public: explicit AnimDurations(QObject* parent = nullptr) : ConfigObject(parent) {} - [[nodiscard]] int small() const { return static_cast(200 * m_scale); } - - [[nodiscard]] int normal() const { return static_cast(400 * m_scale); } - - [[nodiscard]] int large() const { return static_cast(600 * m_scale); } - - [[nodiscard]] int extraLarge() const { return static_cast(1000 * m_scale); } + void bindTokens(AnimDurationTokens* tokens); - [[nodiscard]] int expressiveFastSpatial() const { return static_cast(350 * m_scale); } + [[nodiscard]] int small() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int large() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int expressiveFastSpatial() const; + [[nodiscard]] int expressiveDefaultSpatial() const; + [[nodiscard]] int expressiveSlowSpatial() const; - [[nodiscard]] int expressiveDefaultSpatial() const { return static_cast(500 * m_scale); } + Q_SIGNAL void valuesChanged(); - [[nodiscard]] int expressiveSlowSpatial() const { return static_cast(650 * m_scale); } +private: + AnimDurationTokens* m_tokens = nullptr; }; class AppearanceAnim : public ConfigObject { diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 01df37044..020145774 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -1,17 +1,15 @@ #include "config.hpp" -#include -#include -#include -#include -#include -#include #include namespace caelestia::config { static GlobalConfig* s_instance = nullptr; +static QString configDir() { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); +} + GlobalConfig::GlobalConfig(QObject* parent) : ConfigObject(parent) , m_appearance(new AppearanceConfig(this)) @@ -34,91 +32,32 @@ GlobalConfig::GlobalConfig(QObject* parent) , m_advanced(new AdvancedConfig(this)) { s_instance = this; - m_configPath = QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + - QStringLiteral("/caelestia/shell.json"); - - m_saveTimer.setSingleShot(true); - m_saveTimer.setInterval(500); - connect(&m_saveTimer, &QTimer::timeout, this, &GlobalConfig::writeToFile); - - m_cooldownTimer.setSingleShot(true); - m_cooldownTimer.setInterval(2000); - connect(&m_cooldownTimer, &QTimer::timeout, this, [this] { - m_recentlySaved = false; - emit recentlySavedChanged(); - }); - - connect(&m_watcher, &QFileSystemWatcher::fileChanged, this, &GlobalConfig::onFileChanged); - - reload(); - - if (QFile::exists(m_configPath)) { - m_watcher.addPath(m_configPath); - } + // Bind token base values from advanced config to appearance computed properties + auto* adv = m_advanced->appearance(); + m_appearance->rounding()->bindTokens(adv->rounding()); + m_appearance->spacing()->bindTokens(adv->spacing()); + m_appearance->padding()->bindTokens(adv->padding()); + m_appearance->font()->size()->bindTokens(adv->fontSize()); + m_appearance->anim()->durations()->bindTokens(adv->animDurations()); + + // Each has its own file backend + setupFileBackend(configDir() + QStringLiteral("shell.json")); + m_advanced->setupFileBackend(configDir() + QStringLiteral("advanced.json")); } GlobalConfig* GlobalConfig::instance() { return s_instance; } -bool GlobalConfig::recentlySaved() const { - return m_recentlySaved; -} - void GlobalConfig::save() { - m_saveTimer.start(); - m_recentlySaved = true; - emit recentlySavedChanged(); - m_cooldownTimer.start(); + saveToFile(); } void GlobalConfig::reload() { - QFile file(m_configPath); - - if (!file.open(QIODevice::ReadOnly)) { - qWarning() << "Config: failed to open" << m_configPath; - return; - } - - load(file.readAll()); -} - -void GlobalConfig::load(const QByteArray& contents) { - QJsonParseError error{}; - auto doc = QJsonDocument::fromJson(contents, &error); - - if (error.error != QJsonParseError::NoError) { - qWarning() << "Config: failed to parse JSON:" << error.errorString(); - return; - } - - loadFromJson(doc.object()); -} - -void GlobalConfig::onFileChanged() { - if (!m_watcher.files().contains(m_configPath)) { - m_watcher.addPath(m_configPath); - } - - if (!m_recentlySaved) { - reload(); - } -} - -void GlobalConfig::writeToFile() { - QDir().mkpath(QFileInfo(m_configPath).absolutePath()); - - QFile file(m_configPath); - - if (!file.open(QIODevice::WriteOnly)) { - qWarning() << "Config: failed to write" << m_configPath; - return; - } - - file.write(QJsonDocument(toJsonObject()).toJson(QJsonDocument::Indented)); + reloadFromFile(); } -// Config +// Config (attached type) Config::Config(QQuickItem* parent) : QQuickItem(parent) {} diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index e989d4c30..ecd266343 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -1,12 +1,7 @@ #pragma once -#include -#include -#include -#include #include #include -#include #include "advancedconfig.hpp" #include "appearanceconfig.hpp" @@ -51,33 +46,21 @@ class GlobalConfig : public ConfigObject { CONFIG_SUBOBJECT(SidebarConfig, sidebar) CONFIG_SUBOBJECT(ServiceConfig, services) CONFIG_SUBOBJECT(UserPaths, paths) - CONFIG_SUBOBJECT(AdvancedConfig, advanced) - - Q_PROPERTY(bool recentlySaved READ recentlySaved NOTIFY recentlySavedChanged) + // advanced is NOT a CONFIG_SUBOBJECT — it has its own file backend + Q_PROPERTY(AdvancedConfig* advanced READ advanced CONSTANT) public: explicit GlobalConfig(QObject* parent = nullptr); static GlobalConfig* instance(); - [[nodiscard]] bool recentlySaved() const; + [[nodiscard]] AdvancedConfig* advanced() const { return m_advanced; } Q_INVOKABLE void save(); Q_INVOKABLE void reload(); -signals: - void recentlySavedChanged(); - private: - void load(const QByteArray& contents); - void onFileChanged(); - void writeToFile(); - - bool m_recentlySaved = false; - QString m_configPath; - QFileSystemWatcher m_watcher; - QTimer m_saveTimer; - QTimer m_cooldownTimer; + AdvancedConfig* m_advanced = nullptr; }; class Config : public QQuickItem { diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 7533b0f57..cb6624a1f 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -1,6 +1,10 @@ #include "configobject.hpp" +#include +#include +#include #include +#include #include #include #include @@ -104,4 +108,74 @@ QJsonObject ConfigObject::toJsonObject() const { return obj; } +void ConfigObject::setupFileBackend(const QString& path) { + m_filePath = path; + + m_watcher = new QFileSystemWatcher(this); + m_saveTimer = new QTimer(this); + m_cooldownTimer = new QTimer(this); + + m_saveTimer->setSingleShot(true); + m_saveTimer->setInterval(500); + connect(m_saveTimer, &QTimer::timeout, this, [this] { + QDir().mkpath(QFileInfo(m_filePath).absolutePath()); + + QFile file(m_filePath); + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(lcConfig) << "Failed to write" << m_filePath; + return; + } + + file.write(QJsonDocument(toJsonObject()).toJson(QJsonDocument::Indented)); + }); + + m_cooldownTimer->setSingleShot(true); + m_cooldownTimer->setInterval(2000); + connect(m_cooldownTimer, &QTimer::timeout, this, [this] { + m_recentlySaved = false; + }); + + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ConfigObject::onFileChanged); + + reloadFromFile(); + + if (QFile::exists(m_filePath)) + m_watcher->addPath(m_filePath); +} + +void ConfigObject::saveToFile() { + if (!m_saveTimer) + return; + m_saveTimer->start(); + m_recentlySaved = true; + m_cooldownTimer->start(); +} + +void ConfigObject::reloadFromFile() { + QFile file(m_filePath); + + if (!file.open(QIODevice::ReadOnly)) { + qCDebug(lcConfig) << "Failed to open" << m_filePath; + return; + } + + QJsonParseError error{}; + auto doc = QJsonDocument::fromJson(file.readAll(), &error); + + if (error.error != QJsonParseError::NoError) { + qCWarning(lcConfig) << "Failed to parse" << m_filePath << ":" << error.errorString(); + return; + } + + loadFromJson(doc.object()); +} + +void ConfigObject::onFileChanged() { + if (!m_watcher->files().contains(m_filePath)) + m_watcher->addPath(m_filePath); + + if (!m_recentlySaved) + reloadFromFile(); +} + } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 2f8f443e9..d3ef11e9e 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -1,10 +1,10 @@ #pragma once +#include #include #include #include - -#include +#include // Declares a serialized config property with getter, setter (change-detected), signal, and member. #define CONFIG_PROPERTY(Type, name, ...) \ @@ -16,7 +16,7 @@ public: } \ void set_##name(const Type& val) { \ if (caelestia::config::ConfigObject::updateMember(m_##name, val)) \ - emit name##Changed(); \ + Q_EMIT name##Changed(); \ } \ Q_SIGNAL void name##Changed(); \ \ @@ -47,6 +47,14 @@ class ConfigObject : public QObject { void loadFromJson(const QJsonObject& obj); [[nodiscard]] QJsonObject toJsonObject() const; + // File-backed config support. Call setupFileBackend() to enable + // automatic file watching, debounced saving, and reload. + void setupFileBackend(const QString& path); + void saveToFile(); + void reloadFromFile(); + + [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } + template static bool updateMember(T& member, const T& value) { if constexpr (std::is_floating_point_v) { if (qFuzzyCompare(member + 1.0, value + 1.0)) @@ -58,6 +66,16 @@ class ConfigObject : public QObject { member = value; return true; } + +private: + void onFileChanged(); + + QString m_filePath; + bool m_recentlySaved = false; + // These are heap-allocated only when setupFileBackend is called + QFileSystemWatcher* m_watcher = nullptr; + QTimer* m_saveTimer = nullptr; + QTimer* m_cooldownTimer = nullptr; }; } // namespace caelestia::config From f7aec30ad20536cf945b8f497cdc286a047252b1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 02:52:17 +1000 Subject: [PATCH 269/409] fix: use private constructor for singleton --- plugin/src/Caelestia/Config/config.cpp | 15 ++++++++++----- plugin/src/Caelestia/Config/config.hpp | 5 +++-- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 020145774..dfee35f42 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -4,12 +4,14 @@ namespace caelestia::config { -static GlobalConfig* s_instance = nullptr; +namespace { -static QString configDir() { +QString configDir() { return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); } +} // namespace + GlobalConfig::GlobalConfig(QObject* parent) : ConfigObject(parent) , m_appearance(new AppearanceConfig(this)) @@ -30,8 +32,6 @@ GlobalConfig::GlobalConfig(QObject* parent) , m_services(new ServiceConfig(this)) , m_paths(new UserPaths(this)) , m_advanced(new AdvancedConfig(this)) { - s_instance = this; - // Bind token base values from advanced config to appearance computed properties auto* adv = m_advanced->appearance(); m_appearance->rounding()->bindTokens(adv->rounding()); @@ -46,7 +46,12 @@ GlobalConfig::GlobalConfig(QObject* parent) } GlobalConfig* GlobalConfig::instance() { - return s_instance; + static GlobalConfig instance; + return &instance; +} + +GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { + return instance(); } void GlobalConfig::save() { diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index ecd266343..fda177219 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -50,9 +50,8 @@ class GlobalConfig : public ConfigObject { Q_PROPERTY(AdvancedConfig* advanced READ advanced CONSTANT) public: - explicit GlobalConfig(QObject* parent = nullptr); - static GlobalConfig* instance(); + static GlobalConfig* create(QQmlEngine*, QJSEngine*); [[nodiscard]] AdvancedConfig* advanced() const { return m_advanced; } @@ -60,6 +59,8 @@ class GlobalConfig : public ConfigObject { Q_INVOKABLE void reload(); private: + explicit GlobalConfig(QObject* parent = nullptr); + AdvancedConfig* m_advanced = nullptr; }; From ff1bde855ec42f8d913379fd22bfa10d805e74cd Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 03:07:48 +1000 Subject: [PATCH 270/409] fix: expose config types to qml properly --- .../src/Caelestia/Config/advancedconfig.hpp | 38 +++++++++++++++++++ .../src/Caelestia/Config/appearanceconfig.hpp | 20 ++++++++++ .../src/Caelestia/Config/backgroundconfig.hpp | 10 +++++ plugin/src/Caelestia/Config/barconfig.hpp | 16 ++++++++ plugin/src/Caelestia/Config/borderconfig.hpp | 2 + plugin/src/Caelestia/Config/configobject.hpp | 1 - .../Caelestia/Config/controlcenterconfig.hpp | 1 + .../src/Caelestia/Config/dashboardconfig.hpp | 4 ++ plugin/src/Caelestia/Config/generalconfig.hpp | 8 ++++ .../src/Caelestia/Config/launcherconfig.hpp | 4 ++ plugin/src/Caelestia/Config/lockconfig.hpp | 2 + plugin/src/Caelestia/Config/notifsconfig.hpp | 2 + plugin/src/Caelestia/Config/osdconfig.hpp | 2 + plugin/src/Caelestia/Config/serviceconfig.hpp | 2 + plugin/src/Caelestia/Config/sessionconfig.hpp | 6 +++ plugin/src/Caelestia/Config/sidebarconfig.hpp | 2 + plugin/src/Caelestia/Config/userpaths.hpp | 2 + .../src/Caelestia/Config/utilitiesconfig.hpp | 6 +++ plugin/src/Caelestia/Config/winfoconfig.hpp | 1 + 19 files changed, 128 insertions(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Config/advancedconfig.hpp b/plugin/src/Caelestia/Config/advancedconfig.hpp index 181ba0bc8..3b8956755 100644 --- a/plugin/src/Caelestia/Config/advancedconfig.hpp +++ b/plugin/src/Caelestia/Config/advancedconfig.hpp @@ -8,6 +8,8 @@ namespace caelestia::config { class AnimCurves : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QList, emphasized) CONFIG_PROPERTY(QList, emphasizedAccel) CONFIG_PROPERTY(QList, emphasizedDecel) @@ -34,6 +36,8 @@ class AnimCurves : public ConfigObject { class RoundingTokens : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, small, 12) CONFIG_PROPERTY(int, normal, 17) CONFIG_PROPERTY(int, large, 25) @@ -46,6 +50,8 @@ class RoundingTokens : public ConfigObject { class SpacingTokens : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, small, 7) CONFIG_PROPERTY(int, smaller, 10) CONFIG_PROPERTY(int, normal, 12) @@ -59,6 +65,8 @@ class SpacingTokens : public ConfigObject { class PaddingTokens : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, small, 5) CONFIG_PROPERTY(int, smaller, 7) CONFIG_PROPERTY(int, normal, 10) @@ -72,6 +80,8 @@ class PaddingTokens : public ConfigObject { class FontSizeTokens : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, small, 11) CONFIG_PROPERTY(int, smaller, 12) CONFIG_PROPERTY(int, normal, 13) @@ -86,6 +96,8 @@ class FontSizeTokens : public ConfigObject { class AnimDurationTokens : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, small, 200) CONFIG_PROPERTY(int, normal, 400) CONFIG_PROPERTY(int, large, 600) @@ -101,6 +113,8 @@ class AnimDurationTokens : public ConfigObject { class AdvancedAppearance : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_SUBOBJECT(AnimCurves, curves) CONFIG_SUBOBJECT(RoundingTokens, rounding) CONFIG_SUBOBJECT(SpacingTokens, spacing) @@ -121,6 +135,8 @@ class AdvancedAppearance : public ConfigObject { class BarSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, innerWidth, 40) CONFIG_PROPERTY(int, windowPreviewSize, 400) CONFIG_PROPERTY(int, trayMenuWidth, 300) @@ -135,6 +151,8 @@ class BarSizes : public ConfigObject { class AdvancedDashboard : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, showDashboard, true) CONFIG_PROPERTY(bool, showMedia, true) CONFIG_PROPERTY(bool, showPerformance, true) @@ -187,6 +205,8 @@ class AdvancedDashboard : public ConfigObject { class LauncherSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, itemWidth, 600) CONFIG_PROPERTY(int, itemHeight, 57) CONFIG_PROPERTY(int, wallpaperWidth, 280) @@ -199,6 +219,8 @@ class LauncherSizes : public ConfigObject { class NotifsSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, width, 400) CONFIG_PROPERTY(int, image, 41) CONFIG_PROPERTY(int, badge, 20) @@ -210,6 +232,8 @@ class NotifsSizes : public ConfigObject { class OsdSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, sliderWidth, 30) CONFIG_PROPERTY(int, sliderHeight, 150) @@ -220,6 +244,8 @@ class OsdSizes : public ConfigObject { class SessionSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, button, 80) public: @@ -229,6 +255,8 @@ class SessionSizes : public ConfigObject { class SidebarSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, width, 430) public: @@ -238,6 +266,8 @@ class SidebarSizes : public ConfigObject { class UtilitiesSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, width, 430) CONFIG_PROPERTY(int, toastWidth, 430) @@ -248,6 +278,8 @@ class UtilitiesSizes : public ConfigObject { class LockSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, heightMult, 0.7) CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) CONFIG_PROPERTY(int, centerWidth, 600) @@ -259,6 +291,8 @@ class LockSizes : public ConfigObject { class WInfoSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, heightMult, 0.7) CONFIG_PROPERTY(qreal, detailsWidth, 500) @@ -269,6 +303,8 @@ class WInfoSizes : public ConfigObject { class ControlCenterSizes : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, heightMult, 0.7) CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) @@ -279,6 +315,8 @@ class ControlCenterSizes : public ConfigObject { class AdvancedConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_SUBOBJECT(AdvancedAppearance, appearance) CONFIG_SUBOBJECT(BarSizes, bar) CONFIG_SUBOBJECT(AdvancedDashboard, dashboard) diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index c0094b26a..db395e821 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -15,6 +15,8 @@ class AnimDurationTokens; class AppearanceRounding : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, scale, 1) Q_PROPERTY(int small READ small NOTIFY valuesChanged) @@ -41,6 +43,8 @@ class AppearanceRounding : public ConfigObject { class AppearanceSpacing : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, scale, 1) Q_PROPERTY(int small READ small NOTIFY valuesChanged) @@ -69,6 +73,8 @@ class AppearanceSpacing : public ConfigObject { class AppearancePadding : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, scale, 1) Q_PROPERTY(int small READ small NOTIFY valuesChanged) @@ -97,6 +103,8 @@ class AppearancePadding : public ConfigObject { class FontFamily : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QString, sans, QStringLiteral("Rubik")) CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF")) CONFIG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) @@ -109,6 +117,8 @@ class FontFamily : public ConfigObject { class FontSize : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, scale, 1) Q_PROPERTY(int small READ small NOTIFY valuesChanged) @@ -139,6 +149,8 @@ class FontSize : public ConfigObject { class AppearanceFont : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_SUBOBJECT(FontFamily, family) CONFIG_SUBOBJECT(FontSize, size) @@ -151,6 +163,8 @@ class AppearanceFont : public ConfigObject { class AnimDurations : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, scale, 1) Q_PROPERTY(int small READ small NOTIFY valuesChanged) @@ -183,6 +197,8 @@ class AnimDurations : public ConfigObject { class AppearanceAnim : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) CONFIG_SUBOBJECT(AnimDurations, durations) @@ -195,6 +211,8 @@ class AppearanceAnim : public ConfigObject { class AppearanceTransparency : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, false) CONFIG_PROPERTY(qreal, base, 0.85) CONFIG_PROPERTY(qreal, layers, 0.4) @@ -206,6 +224,8 @@ class AppearanceTransparency : public ConfigObject { class AppearanceConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_SUBOBJECT(AppearanceRounding, rounding) CONFIG_SUBOBJECT(AppearanceSpacing, spacing) CONFIG_SUBOBJECT(AppearancePadding, padding) diff --git a/plugin/src/Caelestia/Config/backgroundconfig.hpp b/plugin/src/Caelestia/Config/backgroundconfig.hpp index 32440cc3c..743cd787a 100644 --- a/plugin/src/Caelestia/Config/backgroundconfig.hpp +++ b/plugin/src/Caelestia/Config/backgroundconfig.hpp @@ -8,6 +8,8 @@ namespace caelestia::config { class DesktopClockBackground : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, false) CONFIG_PROPERTY(qreal, opacity, 0.7) CONFIG_PROPERTY(bool, blur, true) @@ -19,6 +21,8 @@ class DesktopClockBackground : public ConfigObject { class DesktopClockShadow : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(qreal, opacity, 0.7) CONFIG_PROPERTY(qreal, blur, 0.4) @@ -30,6 +34,8 @@ class DesktopClockShadow : public ConfigObject { class DesktopClock : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, false) CONFIG_PROPERTY(qreal, scale, 1.0) CONFIG_PROPERTY(QString, position, QStringLiteral("bottom-right")) @@ -46,6 +52,8 @@ class DesktopClock : public ConfigObject { class BackgroundVisualiser : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, false) CONFIG_PROPERTY(bool, autoHide, true) CONFIG_PROPERTY(bool, blur, false) @@ -59,6 +67,8 @@ class BackgroundVisualiser : public ConfigObject { class BackgroundConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, wallpaperEnabled, true) CONFIG_SUBOBJECT(DesktopClock, desktopClock) diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp index 712c047f7..3ec57b006 100644 --- a/plugin/src/Caelestia/Config/barconfig.hpp +++ b/plugin/src/Caelestia/Config/barconfig.hpp @@ -10,6 +10,8 @@ namespace caelestia::config { class BarScrollActions : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, workspaces, true) CONFIG_PROPERTY(bool, volume, true) CONFIG_PROPERTY(bool, brightness, true) @@ -21,6 +23,8 @@ class BarScrollActions : public ConfigObject { class BarPopouts : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, activeWindow, true) CONFIG_PROPERTY(bool, tray, true) CONFIG_PROPERTY(bool, statusIcons, true) @@ -32,6 +36,8 @@ class BarPopouts : public ConfigObject { class BarWorkspaces : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, shown, 5) CONFIG_PROPERTY(bool, activeIndicator, true) CONFIG_PROPERTY(bool, occupiedBg, false) @@ -54,6 +60,8 @@ class BarWorkspaces : public ConfigObject { class BarActiveWindow : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, compact, false) CONFIG_PROPERTY(bool, inverted, false) CONFIG_PROPERTY(bool, showOnHover, true) @@ -65,6 +73,8 @@ class BarActiveWindow : public ConfigObject { class BarTray : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, background, false) CONFIG_PROPERTY(bool, recolour, false) CONFIG_PROPERTY(bool, compact, false) @@ -78,6 +88,8 @@ class BarTray : public ConfigObject { class BarStatus : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, showAudio, false) CONFIG_PROPERTY(bool, showMicrophone, false) CONFIG_PROPERTY(bool, showKbLayout, false) @@ -94,6 +106,8 @@ class BarStatus : public ConfigObject { class BarClock : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, background, false) CONFIG_PROPERTY(bool, showDate, false) CONFIG_PROPERTY(bool, showIcon, true) @@ -105,6 +119,8 @@ class BarClock : public ConfigObject { class BarConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, persistent, true) CONFIG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(int, dragThreshold, 20) diff --git a/plugin/src/Caelestia/Config/borderconfig.hpp b/plugin/src/Caelestia/Config/borderconfig.hpp index 9bc2966c5..abdd07d94 100644 --- a/plugin/src/Caelestia/Config/borderconfig.hpp +++ b/plugin/src/Caelestia/Config/borderconfig.hpp @@ -8,6 +8,8 @@ namespace caelestia::config { class BorderConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(int, thickness, 10) CONFIG_PROPERTY(int, rounding, 25) diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index d3ef11e9e..bc8271a8d 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -39,7 +39,6 @@ namespace caelestia::config { class ConfigObject : public QObject { Q_OBJECT - QML_ANONYMOUS public: explicit ConfigObject(QObject* parent = nullptr); diff --git a/plugin/src/Caelestia/Config/controlcenterconfig.hpp b/plugin/src/Caelestia/Config/controlcenterconfig.hpp index d827f7cc2..80b40b87a 100644 --- a/plugin/src/Caelestia/Config/controlcenterconfig.hpp +++ b/plugin/src/Caelestia/Config/controlcenterconfig.hpp @@ -8,6 +8,7 @@ namespace caelestia::config { // All properties are in AdvancedConfig.controlCenter class ControlCenterConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS public: explicit ControlCenterConfig(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/dashboardconfig.hpp b/plugin/src/Caelestia/Config/dashboardconfig.hpp index f6b97ac6e..d78bc1af4 100644 --- a/plugin/src/Caelestia/Config/dashboardconfig.hpp +++ b/plugin/src/Caelestia/Config/dashboardconfig.hpp @@ -6,6 +6,8 @@ namespace caelestia::config { class DashboardPerformance : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, showBattery, true) CONFIG_PROPERTY(bool, showGpu, true) CONFIG_PROPERTY(bool, showCpu, true) @@ -20,6 +22,8 @@ class DashboardPerformance : public ConfigObject { class DashboardConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, showOnHover, true) CONFIG_PROPERTY(int, mediaUpdateInterval, 500) diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp index 8ccc90899..88bb7ebfd 100644 --- a/plugin/src/Caelestia/Config/generalconfig.hpp +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -10,6 +10,8 @@ namespace caelestia::config { class GeneralApps : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QStringList, terminal, { QStringLiteral("foot") }) CONFIG_PROPERTY(QStringList, audio, { QStringLiteral("pavucontrol") }) CONFIG_PROPERTY(QStringList, playback, { QStringLiteral("mpv") }) @@ -22,6 +24,8 @@ class GeneralApps : public ConfigObject { class GeneralIdle : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, lockBeforeSleep, true) CONFIG_PROPERTY(bool, inhibitWhenAudio, true) CONFIG_PROPERTY(QVariantList, timeouts) @@ -33,6 +37,8 @@ class GeneralIdle : public ConfigObject { class GeneralBattery : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QVariantList, warnLevels) CONFIG_PROPERTY(int, criticalLevel, 3) @@ -43,6 +49,8 @@ class GeneralBattery : public ConfigObject { class GeneralConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QString, logo) CONFIG_PROPERTY(QStringList, excludedScreens) CONFIG_SUBOBJECT(GeneralApps, apps) diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp index d430ae604..23f71734d 100644 --- a/plugin/src/Caelestia/Config/launcherconfig.hpp +++ b/plugin/src/Caelestia/Config/launcherconfig.hpp @@ -10,6 +10,8 @@ namespace caelestia::config { class LauncherUseFuzzy : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, apps, false) CONFIG_PROPERTY(bool, actions, false) CONFIG_PROPERTY(bool, schemes, false) @@ -23,6 +25,8 @@ class LauncherUseFuzzy : public ConfigObject { class LauncherConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, showOnHover, false) CONFIG_PROPERTY(int, maxShown, 7) diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp index a15c722a7..444b3051b 100644 --- a/plugin/src/Caelestia/Config/lockconfig.hpp +++ b/plugin/src/Caelestia/Config/lockconfig.hpp @@ -6,6 +6,8 @@ namespace caelestia::config { class LockConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, recolourLogo, false) CONFIG_PROPERTY(bool, enableFprint, true) CONFIG_PROPERTY(int, maxFprintTries, 3) diff --git a/plugin/src/Caelestia/Config/notifsconfig.hpp b/plugin/src/Caelestia/Config/notifsconfig.hpp index 61c9bb9e2..745b057b0 100644 --- a/plugin/src/Caelestia/Config/notifsconfig.hpp +++ b/plugin/src/Caelestia/Config/notifsconfig.hpp @@ -8,6 +8,8 @@ namespace caelestia::config { class NotifsConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, expire, true) CONFIG_PROPERTY(QString, fullscreen, QStringLiteral("on")) CONFIG_PROPERTY(int, defaultExpireTimeout, 5000) diff --git a/plugin/src/Caelestia/Config/osdconfig.hpp b/plugin/src/Caelestia/Config/osdconfig.hpp index 71cc27b44..18770294f 100644 --- a/plugin/src/Caelestia/Config/osdconfig.hpp +++ b/plugin/src/Caelestia/Config/osdconfig.hpp @@ -6,6 +6,8 @@ namespace caelestia::config { class OsdConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(int, hideDelay, 2000) CONFIG_PROPERTY(bool, enableBrightness, true) diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp index 131314c12..062bf4513 100644 --- a/plugin/src/Caelestia/Config/serviceconfig.hpp +++ b/plugin/src/Caelestia/Config/serviceconfig.hpp @@ -9,6 +9,8 @@ namespace caelestia::config { class ServiceConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QString, weatherLocation) CONFIG_PROPERTY(bool, useFahrenheit, false) CONFIG_PROPERTY(bool, useFahrenheitPerformance, false) diff --git a/plugin/src/Caelestia/Config/sessionconfig.hpp b/plugin/src/Caelestia/Config/sessionconfig.hpp index af609512d..629b3b6e0 100644 --- a/plugin/src/Caelestia/Config/sessionconfig.hpp +++ b/plugin/src/Caelestia/Config/sessionconfig.hpp @@ -9,6 +9,8 @@ namespace caelestia::config { class SessionIcons : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QString, logout, QStringLiteral("logout")) CONFIG_PROPERTY(QString, shutdown, QStringLiteral("power_settings_new")) CONFIG_PROPERTY(QString, hibernate, QStringLiteral("downloading")) @@ -21,6 +23,8 @@ class SessionIcons : public ConfigObject { class SessionCommands : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QStringList, logout, { QStringLiteral("loginctl"), QStringLiteral("terminate-user"), QString() }) CONFIG_PROPERTY(QStringList, shutdown, { QStringLiteral("systemctl"), QStringLiteral("poweroff") }) CONFIG_PROPERTY(QStringList, hibernate, { QStringLiteral("systemctl"), QStringLiteral("hibernate") }) @@ -33,6 +37,8 @@ class SessionCommands : public ConfigObject { class SessionConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(int, dragThreshold, 30) CONFIG_PROPERTY(bool, vimKeybinds, false) diff --git a/plugin/src/Caelestia/Config/sidebarconfig.hpp b/plugin/src/Caelestia/Config/sidebarconfig.hpp index 71041cfbd..4460872e4 100644 --- a/plugin/src/Caelestia/Config/sidebarconfig.hpp +++ b/plugin/src/Caelestia/Config/sidebarconfig.hpp @@ -6,6 +6,8 @@ namespace caelestia::config { class SidebarConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(int, dragThreshold, 80) diff --git a/plugin/src/Caelestia/Config/userpaths.hpp b/plugin/src/Caelestia/Config/userpaths.hpp index ec0b4880a..188121881 100644 --- a/plugin/src/Caelestia/Config/userpaths.hpp +++ b/plugin/src/Caelestia/Config/userpaths.hpp @@ -10,6 +10,8 @@ namespace caelestia::config { class UserPaths : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(QString, wallpaperDir, QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QStringLiteral("/Wallpapers")) CONFIG_PROPERTY(QString, lyricsDir, QDir::homePath() + QStringLiteral("/Music/lyrics/")) diff --git a/plugin/src/Caelestia/Config/utilitiesconfig.hpp b/plugin/src/Caelestia/Config/utilitiesconfig.hpp index 98da47ede..57b6fccd0 100644 --- a/plugin/src/Caelestia/Config/utilitiesconfig.hpp +++ b/plugin/src/Caelestia/Config/utilitiesconfig.hpp @@ -9,6 +9,8 @@ namespace caelestia::config { class UtilitiesToasts : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, configLoaded, true) CONFIG_PROPERTY(QString, fullscreen, QStringLiteral("off")) CONFIG_PROPERTY(bool, chargingChanged, true) @@ -30,6 +32,8 @@ class UtilitiesToasts : public ConfigObject { class UtilitiesVpn : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, false) CONFIG_PROPERTY(QVariantList, provider) @@ -40,6 +44,8 @@ class UtilitiesVpn : public ConfigObject { class UtilitiesConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS + CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(int, maxToasts, 4) CONFIG_SUBOBJECT(UtilitiesToasts, toasts) diff --git a/plugin/src/Caelestia/Config/winfoconfig.hpp b/plugin/src/Caelestia/Config/winfoconfig.hpp index 91f2f234b..30d4f5e6e 100644 --- a/plugin/src/Caelestia/Config/winfoconfig.hpp +++ b/plugin/src/Caelestia/Config/winfoconfig.hpp @@ -8,6 +8,7 @@ namespace caelestia::config { // All properties are in AdvancedConfig.winfo class WInfoConfig : public ConfigObject { Q_OBJECT + QML_ANONYMOUS public: explicit WInfoConfig(QObject* parent = nullptr) From 4ff97c8306c5289a547cf4446410fb37f4d32192 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 03:27:19 +1000 Subject: [PATCH 271/409] fix: crash/block on qml engine reload --- plugin/src/Caelestia/Config/config.cpp | 19 +++++++++++++++---- plugin/src/Caelestia/Config/config.hpp | 2 ++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index dfee35f42..c242a1d2c 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -1,11 +1,14 @@ #include "config.hpp" +#include #include namespace caelestia::config { namespace { +GlobalConfig* s_instance = nullptr; + QString configDir() { return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); } @@ -32,6 +35,9 @@ GlobalConfig::GlobalConfig(QObject* parent) , m_services(new ServiceConfig(this)) , m_paths(new UserPaths(this)) , m_advanced(new AdvancedConfig(this)) { + // Set global instance + s_instance = this; + // Bind token base values from advanced config to appearance computed properties auto* adv = m_advanced->appearance(); m_appearance->rounding()->bindTokens(adv->rounding()); @@ -45,13 +51,18 @@ GlobalConfig::GlobalConfig(QObject* parent) m_advanced->setupFileBackend(configDir() + QStringLiteral("advanced.json")); } +GlobalConfig::~GlobalConfig() { + // Clear global instance + s_instance = nullptr; +} + GlobalConfig* GlobalConfig::instance() { - static GlobalConfig instance; - return &instance; + return s_instance; } -GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { - return instance(); +GlobalConfig* GlobalConfig::create(QQmlEngine* engine, QJSEngine*) { + auto* config = new GlobalConfig(engine); + return config; } void GlobalConfig::save() { diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index fda177219..bf23df7b9 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -58,6 +58,8 @@ class GlobalConfig : public ConfigObject { Q_INVOKABLE void save(); Q_INVOKABLE void reload(); + ~GlobalConfig() override; + private: explicit GlobalConfig(QObject* parent = nullptr); From f4b851b5b3916175bc2ce51ef0657789a845bf4a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:09:59 +1000 Subject: [PATCH 272/409] feat: advanced -> tokens --- plugin/src/Caelestia/Config/CMakeLists.txt | 2 +- .../src/Caelestia/Config/appearanceconfig.cpp | 2 +- plugin/src/Caelestia/Config/config.cpp | 18 +-- plugin/src/Caelestia/Config/config.hpp | 10 +- .../{advancedconfig.hpp => tokensconfig.hpp} | 153 ++++++++---------- 5 files changed, 79 insertions(+), 106 deletions(-) rename plugin/src/Caelestia/Config/{advancedconfig.hpp => tokensconfig.hpp} (60%) diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 93f94b065..b071537f5 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -4,7 +4,7 @@ qml_module(caelestia-config config.cpp configobject.cpp appearanceconfig.cpp - advancedconfig.hpp + tokensconfig.hpp backgroundconfig.hpp barconfig.hpp borderconfig.hpp diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp index 1b45f8f65..61cb6abe6 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.cpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -1,5 +1,5 @@ #include "appearanceconfig.hpp" -#include "advancedconfig.hpp" +#include "tokensconfig.hpp" #include diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index c242a1d2c..0df6447c4 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -34,21 +34,21 @@ GlobalConfig::GlobalConfig(QObject* parent) , m_sidebar(new SidebarConfig(this)) , m_services(new ServiceConfig(this)) , m_paths(new UserPaths(this)) - , m_advanced(new AdvancedConfig(this)) { + , m_tokens(new TokenConfig(this)) { // Set global instance s_instance = this; - // Bind token base values from advanced config to appearance computed properties - auto* adv = m_advanced->appearance(); - m_appearance->rounding()->bindTokens(adv->rounding()); - m_appearance->spacing()->bindTokens(adv->spacing()); - m_appearance->padding()->bindTokens(adv->padding()); - m_appearance->font()->size()->bindTokens(adv->fontSize()); - m_appearance->anim()->durations()->bindTokens(adv->animDurations()); + // Bind token base values from token config to appearance computed properties + auto* appearance = m_tokens->appearance(); + m_appearance->rounding()->bindTokens(appearance->rounding()); + m_appearance->spacing()->bindTokens(appearance->spacing()); + m_appearance->padding()->bindTokens(appearance->padding()); + m_appearance->font()->size()->bindTokens(appearance->fontSize()); + m_appearance->anim()->durations()->bindTokens(appearance->animDurations()); // Each has its own file backend setupFileBackend(configDir() + QStringLiteral("shell.json")); - m_advanced->setupFileBackend(configDir() + QStringLiteral("advanced.json")); + m_tokens->setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); } GlobalConfig::~GlobalConfig() { diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index bf23df7b9..0d0c16929 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -3,7 +3,6 @@ #include #include -#include "advancedconfig.hpp" #include "appearanceconfig.hpp" #include "backgroundconfig.hpp" #include "barconfig.hpp" @@ -18,6 +17,7 @@ #include "serviceconfig.hpp" #include "sessionconfig.hpp" #include "sidebarconfig.hpp" +#include "tokensconfig.hpp" #include "userpaths.hpp" #include "utilitiesconfig.hpp" #include "winfoconfig.hpp" @@ -46,14 +46,14 @@ class GlobalConfig : public ConfigObject { CONFIG_SUBOBJECT(SidebarConfig, sidebar) CONFIG_SUBOBJECT(ServiceConfig, services) CONFIG_SUBOBJECT(UserPaths, paths) - // advanced is NOT a CONFIG_SUBOBJECT — it has its own file backend - Q_PROPERTY(AdvancedConfig* advanced READ advanced CONSTANT) + // tokens is NOT a CONFIG_SUBOBJECT — it has its own file backend + Q_PROPERTY(TokenConfig* tokens READ tokens CONSTANT) public: static GlobalConfig* instance(); static GlobalConfig* create(QQmlEngine*, QJSEngine*); - [[nodiscard]] AdvancedConfig* advanced() const { return m_advanced; } + [[nodiscard]] TokenConfig* tokens() const { return m_tokens; } Q_INVOKABLE void save(); Q_INVOKABLE void reload(); @@ -63,7 +63,7 @@ class GlobalConfig : public ConfigObject { private: explicit GlobalConfig(QObject* parent = nullptr); - AdvancedConfig* m_advanced = nullptr; + TokenConfig* m_tokens = nullptr; }; class Config : public QQuickItem { diff --git a/plugin/src/Caelestia/Config/advancedconfig.hpp b/plugin/src/Caelestia/Config/tokensconfig.hpp similarity index 60% rename from plugin/src/Caelestia/Config/advancedconfig.hpp rename to plugin/src/Caelestia/Config/tokensconfig.hpp index 3b8956755..f3f48153d 100644 --- a/plugin/src/Caelestia/Config/advancedconfig.hpp +++ b/plugin/src/Caelestia/Config/tokensconfig.hpp @@ -111,7 +111,7 @@ class AnimDurationTokens : public ConfigObject { : ConfigObject(parent) {} }; -class AdvancedAppearance : public ConfigObject { +class AppearanceTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -123,7 +123,7 @@ class AdvancedAppearance : public ConfigObject { CONFIG_SUBOBJECT(AnimDurationTokens, animDurations) public: - explicit AdvancedAppearance(QObject* parent = nullptr) + explicit AppearanceTokens(QObject* parent = nullptr) : ConfigObject(parent) , m_curves(new AnimCurves(this)) , m_rounding(new RoundingTokens(this)) @@ -133,7 +133,7 @@ class AdvancedAppearance : public ConfigObject { , m_animDurations(new AnimDurationTokens(this)) {} }; -class BarSizes : public ConfigObject { +class BarTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -145,11 +145,11 @@ class BarSizes : public ConfigObject { CONFIG_PROPERTY(int, kbLayoutWidth, 320) public: - explicit BarSizes(QObject* parent = nullptr) + explicit BarTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class AdvancedDashboard : public ConfigObject { +class DashboardTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -157,53 +157,26 @@ class AdvancedDashboard : public ConfigObject { CONFIG_PROPERTY(bool, showMedia, true) CONFIG_PROPERTY(bool, showPerformance, true) CONFIG_PROPERTY(bool, showWeather, true) - - Q_PROPERTY(int tabIndicatorHeight READ tabIndicatorHeight CONSTANT) - Q_PROPERTY(int tabIndicatorSpacing READ tabIndicatorSpacing CONSTANT) - Q_PROPERTY(int infoWidth READ infoWidth CONSTANT) - Q_PROPERTY(int infoIconSize READ infoIconSize CONSTANT) - Q_PROPERTY(int dateTimeWidth READ dateTimeWidth CONSTANT) - Q_PROPERTY(int mediaWidth READ mediaWidth CONSTANT) - Q_PROPERTY(int mediaProgressSweep READ mediaProgressSweep CONSTANT) - Q_PROPERTY(int mediaProgressThickness READ mediaProgressThickness CONSTANT) - Q_PROPERTY(int resourceProgessThickness READ resourceProgessThickness CONSTANT) - Q_PROPERTY(int weatherWidth READ weatherWidth CONSTANT) - Q_PROPERTY(int mediaCoverArtSize READ mediaCoverArtSize CONSTANT) - Q_PROPERTY(int mediaVisualiserSize READ mediaVisualiserSize CONSTANT) - Q_PROPERTY(int resourceSize READ resourceSize CONSTANT) + CONFIG_PROPERTY(int, tabIndicatorHeight, 3) + CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) + CONFIG_PROPERTY(int, infoWidth, 200) + CONFIG_PROPERTY(int, infoIconSize, 25) + CONFIG_PROPERTY(int, dateTimeWidth, 110) + CONFIG_PROPERTY(int, mediaWidth, 200) + CONFIG_PROPERTY(int, mediaProgressSweep, 180) + CONFIG_PROPERTY(int, mediaProgressThickness, 8) + CONFIG_PROPERTY(int, resourceProgressThickness, 10) + CONFIG_PROPERTY(int, weatherWidth, 250) + CONFIG_PROPERTY(int, mediaCoverArtSize, 150) + CONFIG_PROPERTY(int, mediaVisualiserSize, 80) + CONFIG_PROPERTY(int, resourceSize, 200) public: - explicit AdvancedDashboard(QObject* parent = nullptr) + explicit DashboardTokens(QObject* parent = nullptr) : ConfigObject(parent) {} - - [[nodiscard]] static int tabIndicatorHeight() { return 3; } - - [[nodiscard]] static int tabIndicatorSpacing() { return 5; } - - [[nodiscard]] static int infoWidth() { return 200; } - - [[nodiscard]] static int infoIconSize() { return 25; } - - [[nodiscard]] static int dateTimeWidth() { return 110; } - - [[nodiscard]] static int mediaWidth() { return 200; } - - [[nodiscard]] static int mediaProgressSweep() { return 180; } - - [[nodiscard]] static int mediaProgressThickness() { return 8; } - - [[nodiscard]] static int resourceProgessThickness() { return 10; } - - [[nodiscard]] static int weatherWidth() { return 250; } - - [[nodiscard]] static int mediaCoverArtSize() { return 150; } - - [[nodiscard]] static int mediaVisualiserSize() { return 80; } - - [[nodiscard]] static int resourceSize() { return 200; } }; -class LauncherSizes : public ConfigObject { +class LauncherTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -213,11 +186,11 @@ class LauncherSizes : public ConfigObject { CONFIG_PROPERTY(int, wallpaperHeight, 200) public: - explicit LauncherSizes(QObject* parent = nullptr) + explicit LauncherTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class NotifsSizes : public ConfigObject { +class NotifsTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -226,11 +199,11 @@ class NotifsSizes : public ConfigObject { CONFIG_PROPERTY(int, badge, 20) public: - explicit NotifsSizes(QObject* parent = nullptr) + explicit NotifsTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class OsdSizes : public ConfigObject { +class OsdTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -238,33 +211,33 @@ class OsdSizes : public ConfigObject { CONFIG_PROPERTY(int, sliderHeight, 150) public: - explicit OsdSizes(QObject* parent = nullptr) + explicit OsdTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class SessionSizes : public ConfigObject { +class SessionTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS CONFIG_PROPERTY(int, button, 80) public: - explicit SessionSizes(QObject* parent = nullptr) + explicit SessionTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class SidebarSizes : public ConfigObject { +class SidebarTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS CONFIG_PROPERTY(int, width, 430) public: - explicit SidebarSizes(QObject* parent = nullptr) + explicit SidebarTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class UtilitiesSizes : public ConfigObject { +class UtilitiesTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -272,11 +245,11 @@ class UtilitiesSizes : public ConfigObject { CONFIG_PROPERTY(int, toastWidth, 430) public: - explicit UtilitiesSizes(QObject* parent = nullptr) + explicit UtilitiesTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class LockSizes : public ConfigObject { +class LockTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -285,11 +258,11 @@ class LockSizes : public ConfigObject { CONFIG_PROPERTY(int, centerWidth, 600) public: - explicit LockSizes(QObject* parent = nullptr) + explicit LockTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class WInfoSizes : public ConfigObject { +class WInfoTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -297,11 +270,11 @@ class WInfoSizes : public ConfigObject { CONFIG_PROPERTY(qreal, detailsWidth, 500) public: - explicit WInfoSizes(QObject* parent = nullptr) + explicit WInfoTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class ControlCenterSizes : public ConfigObject { +class ControlCenterTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -309,42 +282,42 @@ class ControlCenterSizes : public ConfigObject { CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) public: - explicit ControlCenterSizes(QObject* parent = nullptr) + explicit ControlCenterTokens(QObject* parent = nullptr) : ConfigObject(parent) {} }; -class AdvancedConfig : public ConfigObject { +class TokenConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_SUBOBJECT(AdvancedAppearance, appearance) - CONFIG_SUBOBJECT(BarSizes, bar) - CONFIG_SUBOBJECT(AdvancedDashboard, dashboard) - CONFIG_SUBOBJECT(LauncherSizes, launcher) - CONFIG_SUBOBJECT(NotifsSizes, notifs) - CONFIG_SUBOBJECT(OsdSizes, osd) - CONFIG_SUBOBJECT(SessionSizes, session) - CONFIG_SUBOBJECT(SidebarSizes, sidebar) - CONFIG_SUBOBJECT(UtilitiesSizes, utilities) - CONFIG_SUBOBJECT(LockSizes, lock) - CONFIG_SUBOBJECT(WInfoSizes, winfo) - CONFIG_SUBOBJECT(ControlCenterSizes, controlCenter) + CONFIG_SUBOBJECT(AppearanceTokens, appearance) + CONFIG_SUBOBJECT(BarTokens, bar) + CONFIG_SUBOBJECT(DashboardTokens, dashboard) + CONFIG_SUBOBJECT(LauncherTokens, launcher) + CONFIG_SUBOBJECT(NotifsTokens, notifs) + CONFIG_SUBOBJECT(OsdTokens, osd) + CONFIG_SUBOBJECT(SessionTokens, session) + CONFIG_SUBOBJECT(SidebarTokens, sidebar) + CONFIG_SUBOBJECT(UtilitiesTokens, utilities) + CONFIG_SUBOBJECT(LockTokens, lock) + CONFIG_SUBOBJECT(WInfoTokens, winfo) + CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter) public: - explicit AdvancedConfig(QObject* parent = nullptr) + explicit TokenConfig(QObject* parent = nullptr) : ConfigObject(parent) - , m_appearance(new AdvancedAppearance(this)) - , m_bar(new BarSizes(this)) - , m_dashboard(new AdvancedDashboard(this)) - , m_launcher(new LauncherSizes(this)) - , m_notifs(new NotifsSizes(this)) - , m_osd(new OsdSizes(this)) - , m_session(new SessionSizes(this)) - , m_sidebar(new SidebarSizes(this)) - , m_utilities(new UtilitiesSizes(this)) - , m_lock(new LockSizes(this)) - , m_winfo(new WInfoSizes(this)) - , m_controlCenter(new ControlCenterSizes(this)) {} + , m_appearance(new AppearanceTokens(this)) + , m_bar(new BarTokens(this)) + , m_dashboard(new DashboardTokens(this)) + , m_launcher(new LauncherTokens(this)) + , m_notifs(new NotifsTokens(this)) + , m_osd(new OsdTokens(this)) + , m_session(new SessionTokens(this)) + , m_sidebar(new SidebarTokens(this)) + , m_utilities(new UtilitiesTokens(this)) + , m_lock(new LockTokens(this)) + , m_winfo(new WInfoTokens(this)) + , m_controlCenter(new ControlCenterTokens(this)) {} }; } // namespace caelestia::config From 9f8fd7d660d067c2bedc71b7525e34e68bb63eb0 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:36:18 +1000 Subject: [PATCH 273/409] feat: make tokens separate singleton --- plugin/src/Caelestia/Config/CMakeLists.txt | 2 +- .../src/Caelestia/Config/appearanceconfig.cpp | 2 +- plugin/src/Caelestia/Config/config.cpp | 13 +-- plugin/src/Caelestia/Config/config.hpp | 7 -- plugin/src/Caelestia/Config/tokens.cpp | 85 +++++++++++++++++++ .../Config/{tokensconfig.hpp => tokens.hpp} | 39 +++++---- 6 files changed, 112 insertions(+), 36 deletions(-) create mode 100644 plugin/src/Caelestia/Config/tokens.cpp rename plugin/src/Caelestia/Config/{tokensconfig.hpp => tokens.hpp} (92%) diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index b071537f5..61f14735f 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -4,7 +4,7 @@ qml_module(caelestia-config config.cpp configobject.cpp appearanceconfig.cpp - tokensconfig.hpp + tokens.cpp backgroundconfig.hpp barconfig.hpp borderconfig.hpp diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp index 61cb6abe6..6f2433aab 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.cpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -1,5 +1,5 @@ #include "appearanceconfig.hpp" -#include "tokensconfig.hpp" +#include "tokens.hpp" #include diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 0df6447c4..f6fb721d8 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -33,22 +33,11 @@ GlobalConfig::GlobalConfig(QObject* parent) , m_utilities(new UtilitiesConfig(this)) , m_sidebar(new SidebarConfig(this)) , m_services(new ServiceConfig(this)) - , m_paths(new UserPaths(this)) - , m_tokens(new TokenConfig(this)) { + , m_paths(new UserPaths(this)) { // Set global instance s_instance = this; - // Bind token base values from token config to appearance computed properties - auto* appearance = m_tokens->appearance(); - m_appearance->rounding()->bindTokens(appearance->rounding()); - m_appearance->spacing()->bindTokens(appearance->spacing()); - m_appearance->padding()->bindTokens(appearance->padding()); - m_appearance->font()->size()->bindTokens(appearance->fontSize()); - m_appearance->anim()->durations()->bindTokens(appearance->animDurations()); - - // Each has its own file backend setupFileBackend(configDir() + QStringLiteral("shell.json")); - m_tokens->setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); } GlobalConfig::~GlobalConfig() { diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 0d0c16929..bd18b7fb4 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -17,7 +17,6 @@ #include "serviceconfig.hpp" #include "sessionconfig.hpp" #include "sidebarconfig.hpp" -#include "tokensconfig.hpp" #include "userpaths.hpp" #include "utilitiesconfig.hpp" #include "winfoconfig.hpp" @@ -46,15 +45,11 @@ class GlobalConfig : public ConfigObject { CONFIG_SUBOBJECT(SidebarConfig, sidebar) CONFIG_SUBOBJECT(ServiceConfig, services) CONFIG_SUBOBJECT(UserPaths, paths) - // tokens is NOT a CONFIG_SUBOBJECT — it has its own file backend - Q_PROPERTY(TokenConfig* tokens READ tokens CONSTANT) public: static GlobalConfig* instance(); static GlobalConfig* create(QQmlEngine*, QJSEngine*); - [[nodiscard]] TokenConfig* tokens() const { return m_tokens; } - Q_INVOKABLE void save(); Q_INVOKABLE void reload(); @@ -62,8 +57,6 @@ class GlobalConfig : public ConfigObject { private: explicit GlobalConfig(QObject* parent = nullptr); - - TokenConfig* m_tokens = nullptr; }; class Config : public QQuickItem { diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp new file mode 100644 index 000000000..8c8146b7a --- /dev/null +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -0,0 +1,85 @@ +#include "tokens.hpp" +#include "config.hpp" + +#include +#include + +namespace caelestia::config { + +namespace { + +TokenConfig* s_instance = nullptr; + +QString configDir() { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); +} + +} // namespace + +TokenConfig::TokenConfig(QObject* parent) + : ConfigObject(parent) + , m_appearance(new AppearanceTokens(this)) + , m_bar(new BarTokens(this)) + , m_dashboard(new DashboardTokens(this)) + , m_launcher(new LauncherTokens(this)) + , m_notifs(new NotifsTokens(this)) + , m_osd(new OsdTokens(this)) + , m_session(new SessionTokens(this)) + , m_sidebar(new SidebarTokens(this)) + , m_utilities(new UtilitiesTokens(this)) + , m_lock(new LockTokens(this)) + , m_winfo(new WInfoTokens(this)) + , m_controlCenter(new ControlCenterTokens(this)) { + s_instance = this; + + // Bind token base values to GlobalConfig appearance computed properties + if (auto* global = GlobalConfig::instance()) { + auto* appearanceConfig = global->appearance(); + appearanceConfig->rounding()->bindTokens(m_appearance->rounding()); + appearanceConfig->spacing()->bindTokens(m_appearance->spacing()); + appearanceConfig->padding()->bindTokens(m_appearance->padding()); + appearanceConfig->font()->size()->bindTokens(m_appearance->fontSize()); + appearanceConfig->anim()->durations()->bindTokens(m_appearance->animDurations()); + } + + setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); +} + +TokenConfig::~TokenConfig() { + s_instance = nullptr; +} + +TokenConfig* TokenConfig::instance() { + return s_instance; +} + +TokenConfig* TokenConfig::create(QQmlEngine* engine, QJSEngine*) { + return new TokenConfig(engine); +} + +void TokenConfig::save() { + saveToFile(); +} + +void TokenConfig::reload() { + reloadFromFile(); +} + +// Tokens (attached type) + +Tokens::Tokens(QQuickItem* parent) + : QQuickItem(parent) {} + +Tokens* Tokens::qmlAttachedProperties(QObject* object) { + auto* item = qobject_cast(object); + + while (item) { + if (auto* tokens = qobject_cast(item)) + return tokens; + item = item->parentItem(); + } + + return nullptr; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokensconfig.hpp b/plugin/src/Caelestia/Config/tokens.hpp similarity index 92% rename from plugin/src/Caelestia/Config/tokensconfig.hpp rename to plugin/src/Caelestia/Config/tokens.hpp index f3f48153d..d94f173fb 100644 --- a/plugin/src/Caelestia/Config/tokensconfig.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -3,6 +3,7 @@ #include "configobject.hpp" #include +#include namespace caelestia::config { @@ -288,7 +289,8 @@ class ControlCenterTokens : public ConfigObject { class TokenConfig : public ConfigObject { Q_OBJECT - QML_ANONYMOUS + QML_ELEMENT + QML_SINGLETON CONFIG_SUBOBJECT(AppearanceTokens, appearance) CONFIG_SUBOBJECT(BarTokens, bar) @@ -304,20 +306,27 @@ class TokenConfig : public ConfigObject { CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter) public: - explicit TokenConfig(QObject* parent = nullptr) - : ConfigObject(parent) - , m_appearance(new AppearanceTokens(this)) - , m_bar(new BarTokens(this)) - , m_dashboard(new DashboardTokens(this)) - , m_launcher(new LauncherTokens(this)) - , m_notifs(new NotifsTokens(this)) - , m_osd(new OsdTokens(this)) - , m_session(new SessionTokens(this)) - , m_sidebar(new SidebarTokens(this)) - , m_utilities(new UtilitiesTokens(this)) - , m_lock(new LockTokens(this)) - , m_winfo(new WInfoTokens(this)) - , m_controlCenter(new ControlCenterTokens(this)) {} + static TokenConfig* instance(); + static TokenConfig* create(QQmlEngine*, QJSEngine*); + + Q_INVOKABLE void save(); + Q_INVOKABLE void reload(); + + ~TokenConfig() override; + +private: + explicit TokenConfig(QObject* parent = nullptr); +}; + +class Tokens : public QQuickItem { + Q_OBJECT + QML_ELEMENT + QML_ATTACHED(Tokens) + +public: + explicit Tokens(QQuickItem* parent = nullptr); + + static Tokens* qmlAttachedProperties(QObject* object); }; } // namespace caelestia::config From a932a68a4026c48563d7406fd56c11f4a0177b1a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 15:40:19 +1000 Subject: [PATCH 274/409] feat: add config scope --- plugin/src/Caelestia/Config/CMakeLists.txt | 1 + plugin/src/Caelestia/Config/config.cpp | 16 +++++----------- plugin/src/Caelestia/Config/config.hpp | 15 +++++++++++---- plugin/src/Caelestia/Config/configscope.cpp | 20 ++++++++++++++++++++ plugin/src/Caelestia/Config/configscope.hpp | 18 ++++++++++++++++++ plugin/src/Caelestia/Config/tokens.cpp | 16 +++++----------- plugin/src/Caelestia/Config/tokens.hpp | 14 +++++++++++--- 7 files changed, 71 insertions(+), 29 deletions(-) create mode 100644 plugin/src/Caelestia/Config/configscope.cpp create mode 100644 plugin/src/Caelestia/Config/configscope.hpp diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 61f14735f..dcaf1e691 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -3,6 +3,7 @@ qml_module(caelestia-config SOURCES config.cpp configobject.cpp + configscope.cpp appearanceconfig.cpp tokens.cpp backgroundconfig.hpp diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index f6fb721d8..0a71fdc9b 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -1,4 +1,5 @@ #include "config.hpp" +#include "configscope.hpp" #include #include @@ -64,19 +65,12 @@ void GlobalConfig::reload() { // Config (attached type) -Config::Config(QQuickItem* parent) - : QQuickItem(parent) {} +Config::Config(ConfigScope* scope, QObject* parent) + : QObject(parent) + , m_scope(scope) {} Config* Config::qmlAttachedProperties(QObject* object) { - auto* item = qobject_cast(object); - - while (item) { - if (auto* config = qobject_cast(item)) - return config; - item = item->parentItem(); - } - - return nullptr; + return new Config(ConfigScope::find(object), object); } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index bd18b7fb4..2f5fd570e 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -1,7 +1,6 @@ #pragma once -#include -#include +#include #include "appearanceconfig.hpp" #include "backgroundconfig.hpp" @@ -59,15 +58,23 @@ class GlobalConfig : public ConfigObject { explicit GlobalConfig(QObject* parent = nullptr); }; -class Config : public QQuickItem { +class ConfigScope; + +class Config : public QObject { Q_OBJECT QML_ELEMENT + QML_UNCREATABLE("") QML_ATTACHED(Config) public: - explicit Config(QQuickItem* parent = nullptr); + explicit Config(ConfigScope* scope, QObject* parent = nullptr); + + [[nodiscard]] ConfigScope* scope() const { return m_scope; } static Config* qmlAttachedProperties(QObject* object); + +private: + ConfigScope* m_scope; }; } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configscope.cpp b/plugin/src/Caelestia/Config/configscope.cpp new file mode 100644 index 000000000..e7e8452b8 --- /dev/null +++ b/plugin/src/Caelestia/Config/configscope.cpp @@ -0,0 +1,20 @@ +#include "configscope.hpp" + +namespace caelestia::config { + +ConfigScope::ConfigScope(QQuickItem* parent) + : QQuickItem(parent) {} + +ConfigScope* ConfigScope::find(QObject* object) { + auto* item = qobject_cast(object); + + while (item) { + if (auto* scope = qobject_cast(item)) + return scope; + item = item->parentItem(); + } + + return nullptr; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configscope.hpp b/plugin/src/Caelestia/Config/configscope.hpp new file mode 100644 index 000000000..94380c804 --- /dev/null +++ b/plugin/src/Caelestia/Config/configscope.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include + +namespace caelestia::config { + +class ConfigScope : public QQuickItem { + Q_OBJECT + QML_ELEMENT + +public: + explicit ConfigScope(QQuickItem* parent = nullptr); + + static ConfigScope* find(QObject* object); +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 8c8146b7a..c81567d63 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -1,5 +1,6 @@ #include "tokens.hpp" #include "config.hpp" +#include "configscope.hpp" #include #include @@ -67,19 +68,12 @@ void TokenConfig::reload() { // Tokens (attached type) -Tokens::Tokens(QQuickItem* parent) - : QQuickItem(parent) {} +Tokens::Tokens(ConfigScope* scope, QObject* parent) + : QObject(parent) + , m_scope(scope) {} Tokens* Tokens::qmlAttachedProperties(QObject* object) { - auto* item = qobject_cast(object); - - while (item) { - if (auto* tokens = qobject_cast(item)) - return tokens; - item = item->parentItem(); - } - - return nullptr; + return new Tokens(ConfigScope::find(object), object); } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index d94f173fb..a3ea6f696 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -3,10 +3,12 @@ #include "configobject.hpp" #include -#include +#include namespace caelestia::config { +class ConfigScope; + class AnimCurves : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -318,15 +320,21 @@ class TokenConfig : public ConfigObject { explicit TokenConfig(QObject* parent = nullptr); }; -class Tokens : public QQuickItem { +class Tokens : public QObject { Q_OBJECT QML_ELEMENT + QML_UNCREATABLE("") QML_ATTACHED(Tokens) public: - explicit Tokens(QQuickItem* parent = nullptr); + explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); + + [[nodiscard]] ConfigScope* scope() const { return m_scope; } static Tokens* qmlAttachedProperties(QObject* object); + +private: + ConfigScope* m_scope; }; } // namespace caelestia::config From 71f27bb45fe9c6d7de7411a74063f05596bc2d01 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:09:21 +1000 Subject: [PATCH 275/409] feat: add per monitor configs Per monitor configs go in a separate file in the monitor dir, e.g. `monitors//shell.json` --- plugin/src/Caelestia/Config/CMakeLists.txt | 1 + plugin/src/Caelestia/Config/config.cpp | 63 ++++++- plugin/src/Caelestia/Config/config.hpp | 44 +++++ plugin/src/Caelestia/Config/configobject.cpp | 163 +++++++++++++++++- plugin/src/Caelestia/Config/configobject.hpp | 37 +++- plugin/src/Caelestia/Config/configscope.cpp | 32 ++++ plugin/src/Caelestia/Config/configscope.hpp | 28 +++ .../Caelestia/Config/monitorconfigmanager.cpp | 69 ++++++++ .../Caelestia/Config/monitorconfigmanager.hpp | 38 ++++ plugin/src/Caelestia/Config/tokens.cpp | 52 +++++- plugin/src/Caelestia/Config/tokens.hpp | 34 ++++ 11 files changed, 556 insertions(+), 5 deletions(-) create mode 100644 plugin/src/Caelestia/Config/monitorconfigmanager.cpp create mode 100644 plugin/src/Caelestia/Config/monitorconfigmanager.hpp diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index dcaf1e691..9d8a9d994 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -6,6 +6,7 @@ qml_module(caelestia-config configscope.cpp appearanceconfig.cpp tokens.cpp + monitorconfigmanager.cpp backgroundconfig.hpp barconfig.hpp borderconfig.hpp diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 0a71fdc9b..345655ec6 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -41,6 +41,30 @@ GlobalConfig::GlobalConfig(QObject* parent) setupFileBackend(configDir() + QStringLiteral("shell.json")); } +GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent) + : ConfigObject(parent) + , m_appearance(new AppearanceConfig(this)) + , m_general(new GeneralConfig(this)) + , m_background(new BackgroundConfig(this)) + , m_bar(new BarConfig(this)) + , m_border(new BorderConfig(this)) + , m_dashboard(new DashboardConfig(this)) + , m_controlCenter(new ControlCenterConfig(this)) + , m_launcher(new LauncherConfig(this)) + , m_notifs(new NotifsConfig(this)) + , m_osd(new OsdConfig(this)) + , m_session(new SessionConfig(this)) + , m_winfo(new WInfoConfig(this)) + , m_lock(new LockConfig(this)) + , m_utilities(new UtilitiesConfig(this)) + , m_sidebar(new SidebarConfig(this)) + , m_services(new ServiceConfig(this)) + , m_paths(new UserPaths(this)) { + setSparse(true); + setupFileBackend(filePath); + syncFromGlobal(fallback); +} + GlobalConfig::~GlobalConfig() { // Clear global instance s_instance = nullptr; @@ -67,7 +91,44 @@ void GlobalConfig::reload() { Config::Config(ConfigScope* scope, QObject* parent) : QObject(parent) - , m_scope(scope) {} + , m_scope(scope) { + connectScope(); +} + +void Config::connectScope() { + if (!m_scope) + return; + connect(m_scope, &ConfigScope::configChanged, this, &Config::sourceChanged); + connect(m_scope, &ConfigScope::tokensChanged, this, &Config::sourceChanged); +} + +// Helper: return per-monitor sub-object if scope exists, otherwise global +#define CONFIG_ATTACHED_GETTER(Type, name) \ + const Type* Config::name() const { \ + if (m_scope && m_scope->config()) \ + return m_scope->config()->name(); \ + return GlobalConfig::instance()->name(); \ + } + +CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) +CONFIG_ATTACHED_GETTER(GeneralConfig, general) +CONFIG_ATTACHED_GETTER(BackgroundConfig, background) +CONFIG_ATTACHED_GETTER(BarConfig, bar) +CONFIG_ATTACHED_GETTER(BorderConfig, border) +CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard) +CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter) +CONFIG_ATTACHED_GETTER(LauncherConfig, launcher) +CONFIG_ATTACHED_GETTER(NotifsConfig, notifs) +CONFIG_ATTACHED_GETTER(OsdConfig, osd) +CONFIG_ATTACHED_GETTER(SessionConfig, session) +CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) +CONFIG_ATTACHED_GETTER(LockConfig, lock) +CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) +CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) +CONFIG_ATTACHED_GETTER(ServiceConfig, services) +CONFIG_ATTACHED_GETTER(UserPaths, paths) + +#undef CONFIG_ATTACHED_GETTER Config* Config::qmlAttachedProperties(QObject* object) { return new Config(ConfigScope::find(object), object); diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 2f5fd570e..f74c30b2d 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -55,25 +55,69 @@ class GlobalConfig : public ConfigObject { ~GlobalConfig() override; private: + friend class MonitorConfigManager; explicit GlobalConfig(QObject* parent = nullptr); + explicit GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent); }; class ConfigScope; class Config : public QObject { Q_OBJECT + Q_MOC_INCLUDE("configscope.hpp") QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Config) + Q_PROPERTY(ConfigScope* scope READ scope NOTIFY sourceChanged) + Q_PROPERTY(const AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) + Q_PROPERTY(const GeneralConfig* general READ general NOTIFY sourceChanged) + Q_PROPERTY(const BackgroundConfig* background READ background NOTIFY sourceChanged) + Q_PROPERTY(const BarConfig* bar READ bar NOTIFY sourceChanged) + Q_PROPERTY(const BorderConfig* border READ border NOTIFY sourceChanged) + Q_PROPERTY(const DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) + Q_PROPERTY(const ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) + Q_PROPERTY(const LauncherConfig* launcher READ launcher NOTIFY sourceChanged) + Q_PROPERTY(const NotifsConfig* notifs READ notifs NOTIFY sourceChanged) + Q_PROPERTY(const OsdConfig* osd READ osd NOTIFY sourceChanged) + Q_PROPERTY(const SessionConfig* session READ session NOTIFY sourceChanged) + Q_PROPERTY(const WInfoConfig* winfo READ winfo NOTIFY sourceChanged) + Q_PROPERTY(const LockConfig* lock READ lock NOTIFY sourceChanged) + Q_PROPERTY(const UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) + Q_PROPERTY(const SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) + Q_PROPERTY(const ServiceConfig* services READ services NOTIFY sourceChanged) + Q_PROPERTY(const UserPaths* paths READ paths NOTIFY sourceChanged) + public: explicit Config(ConfigScope* scope, QObject* parent = nullptr); [[nodiscard]] ConfigScope* scope() const { return m_scope; } + [[nodiscard]] const AppearanceConfig* appearance() const; + [[nodiscard]] const GeneralConfig* general() const; + [[nodiscard]] const BackgroundConfig* background() const; + [[nodiscard]] const BarConfig* bar() const; + [[nodiscard]] const BorderConfig* border() const; + [[nodiscard]] const DashboardConfig* dashboard() const; + [[nodiscard]] const ControlCenterConfig* controlCenter() const; + [[nodiscard]] const LauncherConfig* launcher() const; + [[nodiscard]] const NotifsConfig* notifs() const; + [[nodiscard]] const OsdConfig* osd() const; + [[nodiscard]] const SessionConfig* session() const; + [[nodiscard]] const WInfoConfig* winfo() const; + [[nodiscard]] const LockConfig* lock() const; + [[nodiscard]] const UtilitiesConfig* utilities() const; + [[nodiscard]] const SidebarConfig* sidebar() const; + [[nodiscard]] const ServiceConfig* services() const; + [[nodiscard]] const UserPaths* paths() const; + static Config* qmlAttachedProperties(QObject* object); + Q_SIGNAL void sourceChanged(); + private: + void connectScope(); + ConfigScope* m_scope; }; diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index cb6624a1f..3682a00f2 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -50,11 +50,13 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) { for (const auto& v : jsonArr) list.append(v.toString()); prop.write(this, QVariant::fromValue(list)); + m_loadedKeys.insert(key); continue; } // For all other types, let Qt's variant conversion handle it prop.write(this, jsonVal.toVariant()); + m_loadedKeys.insert(key); } } @@ -108,6 +110,159 @@ QJsonObject ConfigObject::toJsonObject() const { return obj; } +QJsonObject ConfigObject::toSparseJsonObject() const { + QJsonObject obj; + const auto* meta = metaObject(); + + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + const auto prop = meta->property(i); + + if (!prop.isReadable()) + continue; + + const auto key = QString::fromUtf8(prop.name()); + const auto value = prop.read(this); + + // Recurse into sub-objects — include only if they have loaded keys + if (value.canView()) { + auto* const subObj = value.value(); + if (subObj) { + auto subJson = subObj->toSparseJsonObject(); + if (!subJson.isEmpty()) + obj.insert(key, subJson); + } + continue; + } + + // Only include properties that were explicitly loaded + if (!m_loadedKeys.contains(key)) + continue; + + if (!prop.isWritable()) + continue; + + if (prop.metaType().id() == QMetaType::QStringList) { + QJsonArray arr; + const auto strList = value.toStringList(); + for (const auto& s : strList) + arr.append(s); + obj.insert(key, arr); + continue; + } + + if (prop.metaType().id() == QMetaType::QVariantList) { + obj.insert(key, QJsonArray::fromVariantList(value.toList())); + continue; + } + + obj.insert(key, QJsonValue::fromVariant(value)); + } + + return obj; +} + +void ConfigObject::clearLoadedKeys() { + m_loadedKeys.clear(); + + const auto* meta = metaObject(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + auto value = prop.read(this); + auto* subObj = value.value(); + if (subObj) + subObj->clearLoadedKeys(); + } +} + +void ConfigObject::syncFromGlobal(ConfigObject* global) { + m_global = global; + + // Connect batched change signal (single connection per ConfigObject pair) + connect(global, &ConfigObject::propertiesChanged, this, &ConfigObject::onGlobalPropertiesChanged); + + // Initial sync: copy all non-loaded property values from global + const auto* meta = metaObject(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + const auto key = QString::fromUtf8(prop.name()); + + auto current = prop.read(this); + auto* subObj = current.value(); + + if (subObj) { + auto globalVal = prop.read(global); + auto* globalSub = globalVal.value(); + if (globalSub) + subObj->syncFromGlobal(globalSub); + continue; + } + + if (!prop.isWritable()) + continue; + + if (!m_loadedKeys.contains(key)) + prop.write(this, prop.read(global)); + } +} + +void ConfigObject::resyncFromGlobal() { + if (!m_global) + return; + + const auto* meta = metaObject(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + const auto key = QString::fromUtf8(prop.name()); + + auto current = prop.read(this); + auto* subObj = current.value(); + + if (subObj) { + subObj->resyncFromGlobal(); + continue; + } + + if (!prop.isWritable()) + continue; + + if (!m_loadedKeys.contains(key)) + prop.write(this, prop.read(m_global)); + } +} + +void ConfigObject::onGlobalPropertiesChanged(const QMap& changed) { + for (auto it = changed.begin(); it != changed.end(); ++it) { + if (m_loadedKeys.contains(it.key())) + continue; + + int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); + if (idx >= 0) + metaObject()->property(idx).write(this, it.value()); + } +} + +void ConfigObject::notifyPropertyChanged(const QString& name, const QVariant& value) { + m_pendingChanges.insert(name, value); + + if (!m_batchTimer) { + m_batchTimer = new QTimer(this); + m_batchTimer->setSingleShot(true); + m_batchTimer->setInterval(0); + connect(m_batchTimer, &QTimer::timeout, this, &ConfigObject::emitBatchedChanges); + } + + m_batchTimer->start(); +} + +void ConfigObject::emitBatchedChanges() { + if (m_pendingChanges.isEmpty()) + return; + + auto changes = std::move(m_pendingChanges); + m_pendingChanges.clear(); + Q_EMIT propertiesChanged(changes); +} + void ConfigObject::setupFileBackend(const QString& path) { m_filePath = path; @@ -126,7 +281,8 @@ void ConfigObject::setupFileBackend(const QString& path) { return; } - file.write(QJsonDocument(toJsonObject()).toJson(QJsonDocument::Indented)); + auto json = m_sparse ? toSparseJsonObject() : toJsonObject(); + file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); }); m_cooldownTimer->setSingleShot(true); @@ -167,7 +323,12 @@ void ConfigObject::reloadFromFile() { return; } + clearLoadedKeys(); loadFromJson(doc.object()); + + // Re-sync non-loaded properties from global after reload + if (m_global) + resyncFromGlobal(); } void ConfigObject::onFileChanged() { diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index bc8271a8d..91c6c3f41 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -2,9 +2,12 @@ #include #include +#include #include #include +#include #include +#include // Declares a serialized config property with getter, setter (change-detected), signal, and member. #define CONFIG_PROPERTY(Type, name, ...) \ @@ -15,8 +18,10 @@ public: return m_##name; \ } \ void set_##name(const Type& val) { \ - if (caelestia::config::ConfigObject::updateMember(m_##name, val)) \ + if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ Q_EMIT name##Changed(); \ + notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ + } \ } \ Q_SIGNAL void name##Changed(); \ \ @@ -45,6 +50,7 @@ class ConfigObject : public QObject { void loadFromJson(const QJsonObject& obj); [[nodiscard]] QJsonObject toJsonObject() const; + [[nodiscard]] QJsonObject toSparseJsonObject() const; // File-backed config support. Call setupFileBackend() to enable // automatic file watching, debounced saving, and reload. @@ -52,6 +58,18 @@ class ConfigObject : public QObject { void saveToFile(); void reloadFromFile(); + // Per-monitor overlay support (Qt Resolve Mask pattern). + // Eagerly syncs non-overridden properties from a global ConfigObject. + void syncFromGlobal(ConfigObject* global); + void resyncFromGlobal(); + void clearLoadedKeys(); + + [[nodiscard]] bool isPropertyLoaded(const QString& name) const { return m_loadedKeys.contains(name); } + + [[nodiscard]] bool isSparse() const { return m_sparse; } + + void setSparse(bool sparse) { m_sparse = sparse; } + [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } template static bool updateMember(T& member, const T& value) { @@ -66,15 +84,30 @@ class ConfigObject : public QObject { return true; } + Q_SIGNAL void propertiesChanged(const QMap& changed); + +protected: + void notifyPropertyChanged(const QString& name, const QVariant& value); + private: void onFileChanged(); + void onGlobalPropertiesChanged(const QMap& changed); + void emitBatchedChanges(); QString m_filePath; bool m_recentlySaved = false; - // These are heap-allocated only when setupFileBackend is called + bool m_sparse = false; + + // File backend (heap-allocated only when setupFileBackend is called) QFileSystemWatcher* m_watcher = nullptr; QTimer* m_saveTimer = nullptr; QTimer* m_cooldownTimer = nullptr; + + // Per-monitor overlay state + ConfigObject* m_global = nullptr; + QSet m_loadedKeys; + QMap m_pendingChanges; + QTimer* m_batchTimer = nullptr; }; } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configscope.cpp b/plugin/src/Caelestia/Config/configscope.cpp index e7e8452b8..7b2074e32 100644 --- a/plugin/src/Caelestia/Config/configscope.cpp +++ b/plugin/src/Caelestia/Config/configscope.cpp @@ -1,10 +1,42 @@ #include "configscope.hpp" +#include "monitorconfigmanager.hpp" namespace caelestia::config { ConfigScope::ConfigScope(QQuickItem* parent) : QQuickItem(parent) {} +void ConfigScope::setScreen(const QString& screen) { + if (m_screen == screen) + return; + + m_screen = screen; + Q_EMIT screenChanged(); + resolveConfig(); +} + +void ConfigScope::resolveConfig() { + GlobalConfig* newConfig = nullptr; + TokenConfig* newTokens = nullptr; + + if (!m_screen.isEmpty()) { + if (auto* mgr = MonitorConfigManager::instance()) { + newConfig = mgr->configForScreen(m_screen); + newTokens = mgr->tokensForScreen(m_screen); + } + } + + if (m_config != newConfig) { + m_config = newConfig; + Q_EMIT configChanged(); + } + + if (m_tokens != newTokens) { + m_tokens = newTokens; + Q_EMIT tokensChanged(); + } +} + ConfigScope* ConfigScope::find(QObject* object) { auto* item = qobject_cast(object); diff --git a/plugin/src/Caelestia/Config/configscope.hpp b/plugin/src/Caelestia/Config/configscope.hpp index 94380c804..411e9034f 100644 --- a/plugin/src/Caelestia/Config/configscope.hpp +++ b/plugin/src/Caelestia/Config/configscope.hpp @@ -5,14 +5,42 @@ namespace caelestia::config { +class GlobalConfig; +class TokenConfig; + class ConfigScope : public QQuickItem { Q_OBJECT + Q_MOC_INCLUDE("config.hpp") + Q_MOC_INCLUDE("tokens.hpp") QML_ELEMENT + Q_PROPERTY(QString screen READ screen WRITE setScreen NOTIFY screenChanged) + Q_PROPERTY(GlobalConfig* config READ config NOTIFY configChanged) + Q_PROPERTY(TokenConfig* tokens READ tokens NOTIFY tokensChanged) + public: explicit ConfigScope(QQuickItem* parent = nullptr); + [[nodiscard]] QString screen() const { return m_screen; } + + void setScreen(const QString& screen); + + [[nodiscard]] GlobalConfig* config() const { return m_config; } + + [[nodiscard]] TokenConfig* tokens() const { return m_tokens; } + static ConfigScope* find(QObject* object); + + Q_SIGNAL void screenChanged(); + Q_SIGNAL void configChanged(); + Q_SIGNAL void tokensChanged(); + +private: + void resolveConfig(); + + QString m_screen; + GlobalConfig* m_config = nullptr; + TokenConfig* m_tokens = nullptr; }; } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp new file mode 100644 index 000000000..d8b0561c7 --- /dev/null +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp @@ -0,0 +1,69 @@ +#include "monitorconfigmanager.hpp" +#include "config.hpp" +#include "tokens.hpp" + +#include + +namespace caelestia::config { + +MonitorConfigManager* MonitorConfigManager::s_instance = nullptr; + +namespace { + +QString monitorConfigDir(const QString& screen) { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + + QStringLiteral("/caelestia/monitors/") + screen + QStringLiteral("/"); +} + +} // namespace + +MonitorConfigManager::MonitorConfigManager(QObject* parent) + : QObject(parent) { + s_instance = this; +} + +MonitorConfigManager::~MonitorConfigManager() { + s_instance = nullptr; +} + +MonitorConfigManager* MonitorConfigManager::instance() { + return s_instance; +} + +MonitorConfigManager* MonitorConfigManager::create(QQmlEngine* engine, QJSEngine*) { + return new MonitorConfigManager(engine); +} + +GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) { + auto it = m_overlays.find(screen); + if (it != m_overlays.end() && it->config) + return it->config; + + auto* global = GlobalConfig::instance(); + if (!global) + return nullptr; + + auto dir = monitorConfigDir(screen); + auto* overlay = new GlobalConfig(global, dir + QStringLiteral("shell.json"), this); + + m_overlays[screen].config = overlay; + return overlay; +} + +TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) { + auto it = m_overlays.find(screen); + if (it != m_overlays.end() && it->tokens) + return it->tokens; + + auto* global = TokenConfig::instance(); + if (!global) + return nullptr; + + auto dir = monitorConfigDir(screen); + auto* overlay = new TokenConfig(global, dir + QStringLiteral("shell-tokens.json"), this); + + m_overlays[screen].tokens = overlay; + return overlay; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.hpp b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp new file mode 100644 index 000000000..2741b7327 --- /dev/null +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::config { + +class GlobalConfig; +class TokenConfig; + +class MonitorConfigManager : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + static MonitorConfigManager* instance(); + static MonitorConfigManager* create(QQmlEngine*, QJSEngine*); + + [[nodiscard]] Q_INVOKABLE GlobalConfig* configForScreen(const QString& screen); + [[nodiscard]] Q_INVOKABLE TokenConfig* tokensForScreen(const QString& screen); + + ~MonitorConfigManager() override; + +private: + explicit MonitorConfigManager(QObject* parent = nullptr); + + struct ScreenOverlay { + GlobalConfig* config = nullptr; + TokenConfig* tokens = nullptr; + }; + + QHash m_overlays; + static MonitorConfigManager* s_instance; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index c81567d63..8aab816e4 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -46,6 +46,25 @@ TokenConfig::TokenConfig(QObject* parent) setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); } +TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent) + : ConfigObject(parent) + , m_appearance(new AppearanceTokens(this)) + , m_bar(new BarTokens(this)) + , m_dashboard(new DashboardTokens(this)) + , m_launcher(new LauncherTokens(this)) + , m_notifs(new NotifsTokens(this)) + , m_osd(new OsdTokens(this)) + , m_session(new SessionTokens(this)) + , m_sidebar(new SidebarTokens(this)) + , m_utilities(new UtilitiesTokens(this)) + , m_lock(new LockTokens(this)) + , m_winfo(new WInfoTokens(this)) + , m_controlCenter(new ControlCenterTokens(this)) { + setSparse(true); + setupFileBackend(filePath); + syncFromGlobal(fallback); +} + TokenConfig::~TokenConfig() { s_instance = nullptr; } @@ -70,7 +89,38 @@ void TokenConfig::reload() { Tokens::Tokens(ConfigScope* scope, QObject* parent) : QObject(parent) - , m_scope(scope) {} + , m_scope(scope) { + connectScope(); +} + +void Tokens::connectScope() { + if (!m_scope) + return; + connect(m_scope, &ConfigScope::tokensChanged, this, &Tokens::sourceChanged); + connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged); +} + +#define TOKENS_ATTACHED_GETTER(Type, name) \ + const Type* Tokens::name() const { \ + if (m_scope && m_scope->tokens()) \ + return m_scope->tokens()->name(); \ + return TokenConfig::instance()->name(); \ + } + +TOKENS_ATTACHED_GETTER(AppearanceTokens, appearance) +TOKENS_ATTACHED_GETTER(BarTokens, bar) +TOKENS_ATTACHED_GETTER(DashboardTokens, dashboard) +TOKENS_ATTACHED_GETTER(LauncherTokens, launcher) +TOKENS_ATTACHED_GETTER(NotifsTokens, notifs) +TOKENS_ATTACHED_GETTER(OsdTokens, osd) +TOKENS_ATTACHED_GETTER(SessionTokens, session) +TOKENS_ATTACHED_GETTER(SidebarTokens, sidebar) +TOKENS_ATTACHED_GETTER(UtilitiesTokens, utilities) +TOKENS_ATTACHED_GETTER(LockTokens, lock) +TOKENS_ATTACHED_GETTER(WInfoTokens, winfo) +TOKENS_ATTACHED_GETTER(ControlCenterTokens, controlCenter) + +#undef TOKENS_ATTACHED_GETTER Tokens* Tokens::qmlAttachedProperties(QObject* object) { return new Tokens(ConfigScope::find(object), object); diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index a3ea6f696..9ab1ddc3d 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -317,23 +317,57 @@ class TokenConfig : public ConfigObject { ~TokenConfig() override; private: + friend class MonitorConfigManager; explicit TokenConfig(QObject* parent = nullptr); + explicit TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent); }; class Tokens : public QObject { Q_OBJECT + Q_MOC_INCLUDE("configscope.hpp") QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Tokens) + Q_PROPERTY(ConfigScope* scope READ scope NOTIFY sourceChanged) + Q_PROPERTY(const AppearanceTokens* appearance READ appearance NOTIFY sourceChanged) + Q_PROPERTY(const BarTokens* bar READ bar NOTIFY sourceChanged) + Q_PROPERTY(const DashboardTokens* dashboard READ dashboard NOTIFY sourceChanged) + Q_PROPERTY(const LauncherTokens* launcher READ launcher NOTIFY sourceChanged) + Q_PROPERTY(const NotifsTokens* notifs READ notifs NOTIFY sourceChanged) + Q_PROPERTY(const OsdTokens* osd READ osd NOTIFY sourceChanged) + Q_PROPERTY(const SessionTokens* session READ session NOTIFY sourceChanged) + Q_PROPERTY(const SidebarTokens* sidebar READ sidebar NOTIFY sourceChanged) + Q_PROPERTY(const UtilitiesTokens* utilities READ utilities NOTIFY sourceChanged) + Q_PROPERTY(const LockTokens* lock READ lock NOTIFY sourceChanged) + Q_PROPERTY(const WInfoTokens* winfo READ winfo NOTIFY sourceChanged) + Q_PROPERTY(const ControlCenterTokens* controlCenter READ controlCenter NOTIFY sourceChanged) + public: explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); [[nodiscard]] ConfigScope* scope() const { return m_scope; } + [[nodiscard]] const AppearanceTokens* appearance() const; + [[nodiscard]] const BarTokens* bar() const; + [[nodiscard]] const DashboardTokens* dashboard() const; + [[nodiscard]] const LauncherTokens* launcher() const; + [[nodiscard]] const NotifsTokens* notifs() const; + [[nodiscard]] const OsdTokens* osd() const; + [[nodiscard]] const SessionTokens* session() const; + [[nodiscard]] const SidebarTokens* sidebar() const; + [[nodiscard]] const UtilitiesTokens* utilities() const; + [[nodiscard]] const LockTokens* lock() const; + [[nodiscard]] const WInfoTokens* winfo() const; + [[nodiscard]] const ControlCenterTokens* controlCenter() const; + static Tokens* qmlAttachedProperties(QObject* object); + Q_SIGNAL void sourceChanged(); + private: + void connectScope(); + ConfigScope* m_scope; }; From 9a8c5ec7f9a47ca373a51e3360073a94f0e7bac9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:15:59 +1000 Subject: [PATCH 276/409] feat: add debug logging --- plugin/src/Caelestia/Config/configobject.cpp | 32 +++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 3682a00f2..6ab8b14ff 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -21,6 +21,9 @@ ConfigObject::ConfigObject(QObject* parent) void ConfigObject::loadFromJson(const QJsonObject& obj) { const auto* meta = metaObject(); + qCDebug(lcConfig) << "Loading JSON into" << meta->className() << "with" << obj.keys().size() + << "keys:" << obj.keys(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { auto prop = meta->property(i); const auto key = QString::fromUtf8(prop.name()); @@ -35,6 +38,7 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) { auto* subObj = current.value(); if (subObj) { + qCDebug(lcConfig) << " Recursing into sub-object" << key; subObj->loadFromJson(jsonVal.toObject()); continue; } @@ -51,12 +55,14 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) { list.append(v.toString()); prop.write(this, QVariant::fromValue(list)); m_loadedKeys.insert(key); + qCDebug(lcConfig) << " Loaded" << key << "=" << list; continue; } // For all other types, let Qt's variant conversion handle it prop.write(this, jsonVal.toVariant()); m_loadedKeys.insert(key); + qCDebug(lcConfig) << " Loaded" << key << "=" << jsonVal.toVariant(); } } @@ -177,11 +183,13 @@ void ConfigObject::clearLoadedKeys() { void ConfigObject::syncFromGlobal(ConfigObject* global) { m_global = global; + const auto* meta = metaObject(); + qCDebug(lcConfig) << "Syncing" << meta->className() << "from global, loaded keys:" << m_loadedKeys; + // Connect batched change signal (single connection per ConfigObject pair) connect(global, &ConfigObject::propertiesChanged, this, &ConfigObject::onGlobalPropertiesChanged); // Initial sync: copy all non-loaded property values from global - const auto* meta = metaObject(); for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { auto prop = meta->property(i); const auto key = QString::fromUtf8(prop.name()); @@ -200,8 +208,13 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) { if (!prop.isWritable()) continue; - if (!m_loadedKeys.contains(key)) - prop.write(this, prop.read(global)); + if (!m_loadedKeys.contains(key)) { + auto val = prop.read(global); + prop.write(this, val); + qCDebug(lcConfig) << " Synced" << key << "=" << val << "from global"; + } else { + qCDebug(lcConfig) << " Keeping loaded" << key << "=" << prop.read(this); + } } } @@ -236,8 +249,11 @@ void ConfigObject::onGlobalPropertiesChanged(const QMap& chan continue; int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); - if (idx >= 0) + if (idx >= 0) { metaObject()->property(idx).write(this, it.value()); + qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value() + << "from global change"; + } } } @@ -293,6 +309,8 @@ void ConfigObject::setupFileBackend(const QString& path) { connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ConfigObject::onFileChanged); + qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; + reloadFromFile(); if (QFile::exists(m_filePath)) @@ -323,12 +341,16 @@ void ConfigObject::reloadFromFile() { return; } + qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; + clearLoadedKeys(); loadFromJson(doc.object()); // Re-sync non-loaded properties from global after reload - if (m_global) + if (m_global) { + qCDebug(lcConfig) << "Re-syncing" << metaObject()->className() << "from global after reload"; resyncFromGlobal(); + } } void ConfigObject::onFileChanged() { From f15b234753787ff05587c93eda58a1fa24ba45a5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:35:05 +1000 Subject: [PATCH 277/409] fix: create tokens singleton when global config is created GlobalConfig depends on TokenConfig to forward properties --- plugin/src/Caelestia/Config/config.cpp | 31 +++++++++++++++++++- plugin/src/Caelestia/Config/config.hpp | 2 ++ plugin/src/Caelestia/Config/configobject.hpp | 3 ++ plugin/src/Caelestia/Config/tokens.cpp | 14 +++------ 4 files changed, 39 insertions(+), 11 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 345655ec6..7a8ea1f79 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -1,5 +1,6 @@ #include "config.hpp" #include "configscope.hpp" +#include "tokens.hpp" #include #include @@ -39,6 +40,10 @@ GlobalConfig::GlobalConfig(QObject* parent) s_instance = this; setupFileBackend(configDir() + QStringLiteral("shell.json")); + + // If TokenConfig was created before us, bind now + if (TokenConfig::instance()) + bindAppearanceTokens(); } GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent) @@ -74,8 +79,32 @@ GlobalConfig* GlobalConfig::instance() { return s_instance; } -GlobalConfig* GlobalConfig::create(QQmlEngine* engine, QJSEngine*) { +void GlobalConfig::bindAppearanceTokens() { + auto* tokens = TokenConfig::instance(); + if (!tokens) { + qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: TokenConfig not yet available"; + return; + } + + qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: binding appearance to token values"; + auto* tokenAppearance = tokens->appearance(); + m_appearance->rounding()->bindTokens(tokenAppearance->rounding()); + m_appearance->spacing()->bindTokens(tokenAppearance->spacing()); + m_appearance->padding()->bindTokens(tokenAppearance->padding()); + m_appearance->font()->size()->bindTokens(tokenAppearance->fontSize()); + m_appearance->anim()->durations()->bindTokens(tokenAppearance->animDurations()); +} + +GlobalConfig* GlobalConfig::create(QQmlEngine* engine, QJSEngine* jsEngine) { auto* config = new GlobalConfig(engine); + + // Ensure TokenConfig is created — appearance computed properties depend on token binding. + if (!TokenConfig::instance()) + TokenConfig::create(engine, jsEngine); + + // Bind now that both singletons exist + config->bindAppearanceTokens(); + return config; } diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index f74c30b2d..2df63a03b 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -54,6 +54,8 @@ class GlobalConfig : public ConfigObject { ~GlobalConfig() override; + void bindAppearanceTokens(); + private: friend class MonitorConfigManager; explicit GlobalConfig(QObject* parent = nullptr); diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 91c6c3f41..bb56e9c7d 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -42,6 +43,8 @@ private: namespace caelestia::config { +Q_DECLARE_LOGGING_CATEGORY(lcConfig) + class ConfigObject : public QObject { Q_OBJECT diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 8aab816e4..e03992a4c 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -33,17 +33,11 @@ TokenConfig::TokenConfig(QObject* parent) , m_controlCenter(new ControlCenterTokens(this)) { s_instance = this; - // Bind token base values to GlobalConfig appearance computed properties - if (auto* global = GlobalConfig::instance()) { - auto* appearanceConfig = global->appearance(); - appearanceConfig->rounding()->bindTokens(m_appearance->rounding()); - appearanceConfig->spacing()->bindTokens(m_appearance->spacing()); - appearanceConfig->padding()->bindTokens(m_appearance->padding()); - appearanceConfig->font()->size()->bindTokens(m_appearance->fontSize()); - appearanceConfig->anim()->durations()->bindTokens(m_appearance->animDurations()); - } - setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); + + // If GlobalConfig was created before us, trigger its binding + if (auto* global = GlobalConfig::instance()) + global->bindAppearanceTokens(); } TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent) From 08cc492698eaeaddf170275a741b0e8ebf24083d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:36:29 +1000 Subject: [PATCH 278/409] fix: ensure tokens are not bound multiple times --- plugin/src/Caelestia/Config/config.cpp | 4 ++++ plugin/src/Caelestia/Config/config.hpp | 2 ++ 2 files changed, 6 insertions(+) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 7a8ea1f79..16be07a8a 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -80,6 +80,9 @@ GlobalConfig* GlobalConfig::instance() { } void GlobalConfig::bindAppearanceTokens() { + if (m_tokensBound) + return; + auto* tokens = TokenConfig::instance(); if (!tokens) { qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: TokenConfig not yet available"; @@ -93,6 +96,7 @@ void GlobalConfig::bindAppearanceTokens() { m_appearance->padding()->bindTokens(tokenAppearance->padding()); m_appearance->font()->size()->bindTokens(tokenAppearance->fontSize()); m_appearance->anim()->durations()->bindTokens(tokenAppearance->animDurations()); + m_tokensBound = true; } GlobalConfig* GlobalConfig::create(QQmlEngine* engine, QJSEngine* jsEngine) { diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 2df63a03b..888caffc3 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -60,6 +60,8 @@ class GlobalConfig : public ConfigObject { friend class MonitorConfigManager; explicit GlobalConfig(QObject* parent = nullptr); explicit GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent); + + bool m_tokensBound = false; }; class ConfigScope; From a53aff357a6fd16351cc492c5829c6e28fb5ad12 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 18:58:45 +1000 Subject: [PATCH 279/409] feat: expose default configs to qml --- plugin/src/Caelestia/Config/config.cpp | 18 ++++++++++++++---- plugin/src/Caelestia/Config/config.hpp | 4 +++- plugin/src/Caelestia/Config/tokens.cpp | 17 ++++++++++++++--- plugin/src/Caelestia/Config/tokens.hpp | 5 ++++- 4 files changed, 35 insertions(+), 9 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 16be07a8a..24cbe9e71 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -66,19 +66,29 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObj , m_services(new ServiceConfig(this)) , m_paths(new UserPaths(this)) { setSparse(true); - setupFileBackend(filePath); - syncFromGlobal(fallback); + if (!filePath.isEmpty()) + setupFileBackend(filePath); + if (fallback) + syncFromGlobal(fallback); } GlobalConfig::~GlobalConfig() { - // Clear global instance - s_instance = nullptr; + if (m_defaults) + m_defaults->deleteLater(); + if (s_instance == this) + s_instance = nullptr; } GlobalConfig* GlobalConfig::instance() { return s_instance; } +GlobalConfig* GlobalConfig::defaults() { + if (!m_defaults) + m_defaults = new GlobalConfig(nullptr, QString()); // Non-singleton constructor + return m_defaults; +} + void GlobalConfig::bindAppearanceTokens() { if (m_tokensBound) return; diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 888caffc3..a6fa2f01b 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -47,6 +47,7 @@ class GlobalConfig : public ConfigObject { public: static GlobalConfig* instance(); + [[nodiscard]] Q_INVOKABLE GlobalConfig* defaults(); static GlobalConfig* create(QQmlEngine*, QJSEngine*); Q_INVOKABLE void save(); @@ -59,8 +60,9 @@ class GlobalConfig : public ConfigObject { private: friend class MonitorConfigManager; explicit GlobalConfig(QObject* parent = nullptr); - explicit GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent); + explicit GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent = nullptr); + GlobalConfig* m_defaults = nullptr; bool m_tokensBound = false; }; diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index e03992a4c..f2f8d5cc5 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -55,18 +55,29 @@ TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject , m_winfo(new WInfoTokens(this)) , m_controlCenter(new ControlCenterTokens(this)) { setSparse(true); - setupFileBackend(filePath); - syncFromGlobal(fallback); + if (!filePath.isEmpty()) + setupFileBackend(filePath); + if (fallback) + syncFromGlobal(fallback); } TokenConfig::~TokenConfig() { - s_instance = nullptr; + if (m_defaults) + m_defaults->deleteLater(); + if (s_instance == this) + s_instance = nullptr; } TokenConfig* TokenConfig::instance() { return s_instance; } +TokenConfig* TokenConfig::defaults() { + if (!m_defaults) + m_defaults = new TokenConfig(nullptr, QString()); // Non-singleton constructor + return m_defaults; +} + TokenConfig* TokenConfig::create(QQmlEngine* engine, QJSEngine*) { return new TokenConfig(engine); } diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 9ab1ddc3d..3d9d48d52 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -309,6 +309,7 @@ class TokenConfig : public ConfigObject { public: static TokenConfig* instance(); + [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); static TokenConfig* create(QQmlEngine*, QJSEngine*); Q_INVOKABLE void save(); @@ -319,7 +320,9 @@ class TokenConfig : public ConfigObject { private: friend class MonitorConfigManager; explicit TokenConfig(QObject* parent = nullptr); - explicit TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent); + explicit TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent = nullptr); + + TokenConfig* m_defaults = nullptr; }; class Tokens : public QObject { From edf6184c81b271cd4460cf284862ecdefa1914e7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:16:44 +1000 Subject: [PATCH 280/409] fix: retry reading file before warn Some programs will not write to the file atomically, causing us to read a malformed JSON while the write is in progress. Add a retry system to avoid false warnings. --- plugin/src/Caelestia/Config/configobject.cpp | 23 ++++++++++++++++++-- plugin/src/Caelestia/Config/configobject.hpp | 2 ++ 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 6ab8b14ff..8b9001fdf 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -285,6 +285,11 @@ void ConfigObject::setupFileBackend(const QString& path) { m_watcher = new QFileSystemWatcher(this); m_saveTimer = new QTimer(this); m_cooldownTimer = new QTimer(this); + m_retryTimer = new QTimer(this); + + m_retryTimer->setSingleShot(true); + m_retryTimer->setInterval(50); + connect(m_retryTimer, &QTimer::timeout, this, &ConfigObject::reloadFromFile); m_saveTimer->setSingleShot(true); m_saveTimer->setInterval(500); @@ -337,10 +342,20 @@ void ConfigObject::reloadFromFile() { auto doc = QJsonDocument::fromJson(file.readAll(), &error); if (error.error != QJsonParseError::NoError) { - qCWarning(lcConfig) << "Failed to parse" << m_filePath << ":" << error.errorString(); + if (m_retryTimer && m_parseRetries < 3) { + m_parseRetries++; + qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qPrintable(m_filePath), + qPrintable(error.errorString()), m_parseRetries); + m_retryTimer->start(); + } else { + qCWarning(lcConfig, "Failed to parse %s: %s", qPrintable(m_filePath), qPrintable(error.errorString())); + m_parseRetries = 0; + } return; } + m_parseRetries = 0; + qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; clearLoadedKeys(); @@ -357,8 +372,12 @@ void ConfigObject::onFileChanged() { if (!m_watcher->files().contains(m_filePath)) m_watcher->addPath(m_filePath); - if (!m_recentlySaved) + if (!m_recentlySaved) { + m_parseRetries = 0; + if (m_retryTimer) + m_retryTimer->stop(); reloadFromFile(); + } } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index bb56e9c7d..096fb9e8c 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -105,6 +105,8 @@ class ConfigObject : public QObject { QFileSystemWatcher* m_watcher = nullptr; QTimer* m_saveTimer = nullptr; QTimer* m_cooldownTimer = nullptr; + QTimer* m_retryTimer = nullptr; + int m_parseRetries = 0; // Per-monitor overlay state ConfigObject* m_global = nullptr; From 8f51502f91c7cf0ccbe48feb68b56dfb4e5c4dae Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:44:22 +1000 Subject: [PATCH 281/409] fix: use qobject parent --- plugin/src/Caelestia/Config/config.cpp | 4 +--- plugin/src/Caelestia/Config/tokens.cpp | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 24cbe9e71..21b902009 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -73,8 +73,6 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObj } GlobalConfig::~GlobalConfig() { - if (m_defaults) - m_defaults->deleteLater(); if (s_instance == this) s_instance = nullptr; } @@ -85,7 +83,7 @@ GlobalConfig* GlobalConfig::instance() { GlobalConfig* GlobalConfig::defaults() { if (!m_defaults) - m_defaults = new GlobalConfig(nullptr, QString()); // Non-singleton constructor + m_defaults = new GlobalConfig(nullptr, QString(), this); // Non-singleton constructor return m_defaults; } diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index f2f8d5cc5..7e6ca0e02 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -62,8 +62,6 @@ TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject } TokenConfig::~TokenConfig() { - if (m_defaults) - m_defaults->deleteLater(); if (s_instance == this) s_instance = nullptr; } @@ -74,7 +72,7 @@ TokenConfig* TokenConfig::instance() { TokenConfig* TokenConfig::defaults() { if (!m_defaults) - m_defaults = new TokenConfig(nullptr, QString()); // Non-singleton constructor + m_defaults = new TokenConfig(nullptr, QString(), this); // Non-singleton constructor return m_defaults; } From e283ee2a9ff37e9a2de40511e9ed13d198c0ce97 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:49:05 +1000 Subject: [PATCH 282/409] fix: fully qualify Q_PROPERTY types --- plugin/src/Caelestia/Config/config.hpp | 36 ++++++++++----------- plugin/src/Caelestia/Config/configscope.hpp | 4 +-- plugin/src/Caelestia/Config/tokens.hpp | 26 +++++++-------- 3 files changed, 33 insertions(+), 33 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index a6fa2f01b..61ef871c2 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -75,24 +75,24 @@ class Config : public QObject { QML_UNCREATABLE("") QML_ATTACHED(Config) - Q_PROPERTY(ConfigScope* scope READ scope NOTIFY sourceChanged) - Q_PROPERTY(const AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) - Q_PROPERTY(const GeneralConfig* general READ general NOTIFY sourceChanged) - Q_PROPERTY(const BackgroundConfig* background READ background NOTIFY sourceChanged) - Q_PROPERTY(const BarConfig* bar READ bar NOTIFY sourceChanged) - Q_PROPERTY(const BorderConfig* border READ border NOTIFY sourceChanged) - Q_PROPERTY(const DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) - Q_PROPERTY(const ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) - Q_PROPERTY(const LauncherConfig* launcher READ launcher NOTIFY sourceChanged) - Q_PROPERTY(const NotifsConfig* notifs READ notifs NOTIFY sourceChanged) - Q_PROPERTY(const OsdConfig* osd READ osd NOTIFY sourceChanged) - Q_PROPERTY(const SessionConfig* session READ session NOTIFY sourceChanged) - Q_PROPERTY(const WInfoConfig* winfo READ winfo NOTIFY sourceChanged) - Q_PROPERTY(const LockConfig* lock READ lock NOTIFY sourceChanged) - Q_PROPERTY(const UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) - Q_PROPERTY(const SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) - Q_PROPERTY(const ServiceConfig* services READ services NOTIFY sourceChanged) - Q_PROPERTY(const UserPaths* paths READ paths NOTIFY sourceChanged) + Q_PROPERTY(caelestia::config::ConfigScope* scope READ scope NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BarConfig* bar READ bar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BorderConfig* border READ border NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) public: explicit Config(ConfigScope* scope, QObject* parent = nullptr); diff --git a/plugin/src/Caelestia/Config/configscope.hpp b/plugin/src/Caelestia/Config/configscope.hpp index 411e9034f..53ff54260 100644 --- a/plugin/src/Caelestia/Config/configscope.hpp +++ b/plugin/src/Caelestia/Config/configscope.hpp @@ -15,8 +15,8 @@ class ConfigScope : public QQuickItem { QML_ELEMENT Q_PROPERTY(QString screen READ screen WRITE setScreen NOTIFY screenChanged) - Q_PROPERTY(GlobalConfig* config READ config NOTIFY configChanged) - Q_PROPERTY(TokenConfig* tokens READ tokens NOTIFY tokensChanged) + Q_PROPERTY(caelestia::config::GlobalConfig* config READ config NOTIFY configChanged) + Q_PROPERTY(caelestia::config::TokenConfig* tokens READ tokens NOTIFY tokensChanged) public: explicit ConfigScope(QQuickItem* parent = nullptr); diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 3d9d48d52..aae51f16b 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -332,19 +332,19 @@ class Tokens : public QObject { QML_UNCREATABLE("") QML_ATTACHED(Tokens) - Q_PROPERTY(ConfigScope* scope READ scope NOTIFY sourceChanged) - Q_PROPERTY(const AppearanceTokens* appearance READ appearance NOTIFY sourceChanged) - Q_PROPERTY(const BarTokens* bar READ bar NOTIFY sourceChanged) - Q_PROPERTY(const DashboardTokens* dashboard READ dashboard NOTIFY sourceChanged) - Q_PROPERTY(const LauncherTokens* launcher READ launcher NOTIFY sourceChanged) - Q_PROPERTY(const NotifsTokens* notifs READ notifs NOTIFY sourceChanged) - Q_PROPERTY(const OsdTokens* osd READ osd NOTIFY sourceChanged) - Q_PROPERTY(const SessionTokens* session READ session NOTIFY sourceChanged) - Q_PROPERTY(const SidebarTokens* sidebar READ sidebar NOTIFY sourceChanged) - Q_PROPERTY(const UtilitiesTokens* utilities READ utilities NOTIFY sourceChanged) - Q_PROPERTY(const LockTokens* lock READ lock NOTIFY sourceChanged) - Q_PROPERTY(const WInfoTokens* winfo READ winfo NOTIFY sourceChanged) - Q_PROPERTY(const ControlCenterTokens* controlCenter READ controlCenter NOTIFY sourceChanged) + Q_PROPERTY(caelestia::config::ConfigScope* scope READ scope NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceTokens* appearance READ appearance NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BarTokens* bar READ bar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::DashboardTokens* dashboard READ dashboard NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LauncherTokens* launcher READ launcher NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::NotifsTokens* notifs READ notifs NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::OsdTokens* osd READ osd NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SessionTokens* session READ session NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SidebarTokens* sidebar READ sidebar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UtilitiesTokens* utilities READ utilities NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LockTokens* lock READ lock NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::WInfoTokens* winfo READ winfo NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::ControlCenterTokens* controlCenter READ controlCenter NOTIFY sourceChanged) public: explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); From d5d6f70f521b91e26c7058bd003c243e4b43a1f5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:10:27 +1000 Subject: [PATCH 283/409] feat: add load/save + fail signals --- plugin/src/Caelestia/Config/config.cpp | 12 +---- plugin/src/Caelestia/Config/config.hpp | 5 +-- plugin/src/Caelestia/Config/configobject.cpp | 46 ++++++++++++++++---- plugin/src/Caelestia/Config/configobject.hpp | 19 +++++++- plugin/src/Caelestia/Config/tokens.cpp | 12 +---- plugin/src/Caelestia/Config/tokens.hpp | 5 +-- 6 files changed, 61 insertions(+), 38 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 21b902009..f78884f29 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -18,7 +18,7 @@ QString configDir() { } // namespace GlobalConfig::GlobalConfig(QObject* parent) - : ConfigObject(parent) + : RootConfig(parent) , m_appearance(new AppearanceConfig(this)) , m_general(new GeneralConfig(this)) , m_background(new BackgroundConfig(this)) @@ -47,7 +47,7 @@ GlobalConfig::GlobalConfig(QObject* parent) } GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent) - : ConfigObject(parent) + : RootConfig(parent) , m_appearance(new AppearanceConfig(this)) , m_general(new GeneralConfig(this)) , m_background(new BackgroundConfig(this)) @@ -120,14 +120,6 @@ GlobalConfig* GlobalConfig::create(QQmlEngine* engine, QJSEngine* jsEngine) { return config; } -void GlobalConfig::save() { - saveToFile(); -} - -void GlobalConfig::reload() { - reloadFromFile(); -} - // Config (attached type) Config::Config(ConfigScope* scope, QObject* parent) diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 61ef871c2..2218e753a 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -22,7 +22,7 @@ namespace caelestia::config { -class GlobalConfig : public ConfigObject { +class GlobalConfig : public RootConfig { Q_OBJECT QML_ELEMENT QML_SINGLETON @@ -50,9 +50,6 @@ class GlobalConfig : public ConfigObject { [[nodiscard]] Q_INVOKABLE GlobalConfig* defaults(); static GlobalConfig* create(QQmlEngine*, QJSEngine*); - Q_INVOKABLE void save(); - Q_INVOKABLE void reload(); - ~GlobalConfig() override; void bindAppearanceTokens(); diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 8b9001fdf..93736837b 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -298,12 +298,16 @@ void ConfigObject::setupFileBackend(const QString& path) { QFile file(m_filePath); if (!file.open(QIODevice::WriteOnly)) { - qCWarning(lcConfig) << "Failed to write" << m_filePath; + qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath)); + if (auto* root = qobject_cast(this)) + Q_EMIT root->fileSaveFailed(QStringLiteral("Failed to open file for writing")); return; } auto json = m_sparse ? toSparseJsonObject() : toJsonObject(); file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); + if (auto* root = qobject_cast(this)) + Q_EMIT root->fileSaved(); }); m_cooldownTimer->setSingleShot(true); @@ -330,12 +334,12 @@ void ConfigObject::saveToFile() { m_cooldownTimer->start(); } -void ConfigObject::reloadFromFile() { +bool ConfigObject::reloadFromFile() { QFile file(m_filePath); if (!file.open(QIODevice::ReadOnly)) { - qCDebug(lcConfig) << "Failed to open" << m_filePath; - return; + qCDebug(lcConfig, "Failed to open %s", qUtf8Printable(m_filePath)); + return false; } QJsonParseError error{}; @@ -344,14 +348,15 @@ void ConfigObject::reloadFromFile() { if (error.error != QJsonParseError::NoError) { if (m_retryTimer && m_parseRetries < 3) { m_parseRetries++; - qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qPrintable(m_filePath), - qPrintable(error.errorString()), m_parseRetries); + qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath), + qUtf8Printable(error.errorString()), m_parseRetries); m_retryTimer->start(); } else { - qCWarning(lcConfig, "Failed to parse %s: %s", qPrintable(m_filePath), qPrintable(error.errorString())); + qCWarning( + lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); m_parseRetries = 0; } - return; + return false; } m_parseRetries = 0; @@ -366,6 +371,8 @@ void ConfigObject::reloadFromFile() { qCDebug(lcConfig) << "Re-syncing" << metaObject()->className() << "from global after reload"; resyncFromGlobal(); } + + return true; } void ConfigObject::onFileChanged() { @@ -376,8 +383,29 @@ void ConfigObject::onFileChanged() { m_parseRetries = 0; if (m_retryTimer) m_retryTimer->stop(); - reloadFromFile(); + + bool ok = reloadFromFile(); + if (auto* root = qobject_cast(this)) { + if (ok) + Q_EMIT root->fileLoaded(); + else + Q_EMIT root->fileLoadFailed(QStringLiteral("Failed to load config file")); + } } } +// RootConfig + +RootConfig::RootConfig(QObject* parent) + : ConfigObject(parent) {} + +void RootConfig::save() { + saveToFile(); +} + +void RootConfig::reload() { + if (reloadFromFile()) + Q_EMIT fileLoaded(); +} + } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 096fb9e8c..f9607fa77 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -59,7 +59,7 @@ class ConfigObject : public QObject { // automatic file watching, debounced saving, and reload. void setupFileBackend(const QString& path); void saveToFile(); - void reloadFromFile(); + bool reloadFromFile(); // Per-monitor overlay support (Qt Resolve Mask pattern). // Eagerly syncs non-overridden properties from a global ConfigObject. @@ -115,4 +115,21 @@ class ConfigObject : public QObject { QTimer* m_batchTimer = nullptr; }; +// Intermediate base for singleton config roots (GlobalConfig, TokenConfig). +// Provides save/reload with file lifecycle signals. +class RootConfig : public ConfigObject { + Q_OBJECT + +public: + explicit RootConfig(QObject* parent = nullptr); + + Q_INVOKABLE void save(); + Q_INVOKABLE void reload(); + + Q_SIGNAL void fileLoaded(); + Q_SIGNAL void fileLoadFailed(const QString& error); + Q_SIGNAL void fileSaved(); + Q_SIGNAL void fileSaveFailed(const QString& error); +}; + } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 7e6ca0e02..ac383ba5d 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -18,7 +18,7 @@ QString configDir() { } // namespace TokenConfig::TokenConfig(QObject* parent) - : ConfigObject(parent) + : RootConfig(parent) , m_appearance(new AppearanceTokens(this)) , m_bar(new BarTokens(this)) , m_dashboard(new DashboardTokens(this)) @@ -41,7 +41,7 @@ TokenConfig::TokenConfig(QObject* parent) } TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent) - : ConfigObject(parent) + : RootConfig(parent) , m_appearance(new AppearanceTokens(this)) , m_bar(new BarTokens(this)) , m_dashboard(new DashboardTokens(this)) @@ -80,14 +80,6 @@ TokenConfig* TokenConfig::create(QQmlEngine* engine, QJSEngine*) { return new TokenConfig(engine); } -void TokenConfig::save() { - saveToFile(); -} - -void TokenConfig::reload() { - reloadFromFile(); -} - // Tokens (attached type) Tokens::Tokens(ConfigScope* scope, QObject* parent) diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index aae51f16b..53c4a7071 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -289,7 +289,7 @@ class ControlCenterTokens : public ConfigObject { : ConfigObject(parent) {} }; -class TokenConfig : public ConfigObject { +class TokenConfig : public RootConfig { Q_OBJECT QML_ELEMENT QML_SINGLETON @@ -312,9 +312,6 @@ class TokenConfig : public ConfigObject { [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); static TokenConfig* create(QQmlEngine*, QJSEngine*); - Q_INVOKABLE void save(); - Q_INVOKABLE void reload(); - ~TokenConfig() override; private: From 4f2b90147eaaa96577947455d846937030130988 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 20:16:37 +1000 Subject: [PATCH 284/409] fix: signal and emit macros -> keywords Only the ones inside macros are unable to use the keywords --- plugin/src/Caelestia/Config/appearanceconfig.hpp | 15 ++++++++++----- plugin/src/Caelestia/Config/config.hpp | 3 ++- plugin/src/Caelestia/Config/configobject.cpp | 12 ++++++------ plugin/src/Caelestia/Config/configobject.hpp | 12 +++++++----- plugin/src/Caelestia/Config/configscope.cpp | 6 +++--- plugin/src/Caelestia/Config/configscope.hpp | 7 ++++--- plugin/src/Caelestia/Config/tokens.hpp | 3 ++- 7 files changed, 34 insertions(+), 24 deletions(-) diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index db395e821..c13d07792 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -35,7 +35,8 @@ class AppearanceRounding : public ConfigObject { [[nodiscard]] int large() const; [[nodiscard]] int full() const; - Q_SIGNAL void valuesChanged(); +signals: + void valuesChanged(); private: RoundingTokens* m_tokens = nullptr; @@ -65,7 +66,8 @@ class AppearanceSpacing : public ConfigObject { [[nodiscard]] int larger() const; [[nodiscard]] int large() const; - Q_SIGNAL void valuesChanged(); +signals: + void valuesChanged(); private: SpacingTokens* m_tokens = nullptr; @@ -95,7 +97,8 @@ class AppearancePadding : public ConfigObject { [[nodiscard]] int larger() const; [[nodiscard]] int large() const; - Q_SIGNAL void valuesChanged(); +signals: + void valuesChanged(); private: PaddingTokens* m_tokens = nullptr; @@ -141,7 +144,8 @@ class FontSize : public ConfigObject { [[nodiscard]] int large() const; [[nodiscard]] int extraLarge() const; - Q_SIGNAL void valuesChanged(); +signals: + void valuesChanged(); private: FontSizeTokens* m_tokens = nullptr; @@ -189,7 +193,8 @@ class AnimDurations : public ConfigObject { [[nodiscard]] int expressiveDefaultSpatial() const; [[nodiscard]] int expressiveSlowSpatial() const; - Q_SIGNAL void valuesChanged(); +signals: + void valuesChanged(); private: AnimDurationTokens* m_tokens = nullptr; diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 2218e753a..ddced1317 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -116,7 +116,8 @@ class Config : public QObject { static Config* qmlAttachedProperties(QObject* object); - Q_SIGNAL void sourceChanged(); +signals: + void sourceChanged(); private: void connectScope(); diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 93736837b..8103b8969 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -276,7 +276,7 @@ void ConfigObject::emitBatchedChanges() { auto changes = std::move(m_pendingChanges); m_pendingChanges.clear(); - Q_EMIT propertiesChanged(changes); + emit propertiesChanged(changes); } void ConfigObject::setupFileBackend(const QString& path) { @@ -300,14 +300,14 @@ void ConfigObject::setupFileBackend(const QString& path) { if (!file.open(QIODevice::WriteOnly)) { qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath)); if (auto* root = qobject_cast(this)) - Q_EMIT root->fileSaveFailed(QStringLiteral("Failed to open file for writing")); + emit root->fileSaveFailed(QStringLiteral("Failed to open file for writing")); return; } auto json = m_sparse ? toSparseJsonObject() : toJsonObject(); file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); if (auto* root = qobject_cast(this)) - Q_EMIT root->fileSaved(); + emit root->fileSaved(); }); m_cooldownTimer->setSingleShot(true); @@ -387,9 +387,9 @@ void ConfigObject::onFileChanged() { bool ok = reloadFromFile(); if (auto* root = qobject_cast(this)) { if (ok) - Q_EMIT root->fileLoaded(); + emit root->fileLoaded(); else - Q_EMIT root->fileLoadFailed(QStringLiteral("Failed to load config file")); + emit root->fileLoadFailed(QStringLiteral("Failed to load config file")); } } } @@ -405,7 +405,7 @@ void RootConfig::save() { void RootConfig::reload() { if (reloadFromFile()) - Q_EMIT fileLoaded(); + emit fileLoaded(); } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index f9607fa77..f04204414 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -87,7 +87,8 @@ class ConfigObject : public QObject { return true; } - Q_SIGNAL void propertiesChanged(const QMap& changed); +signals: + void propertiesChanged(const QMap& changed); protected: void notifyPropertyChanged(const QString& name, const QVariant& value); @@ -126,10 +127,11 @@ class RootConfig : public ConfigObject { Q_INVOKABLE void save(); Q_INVOKABLE void reload(); - Q_SIGNAL void fileLoaded(); - Q_SIGNAL void fileLoadFailed(const QString& error); - Q_SIGNAL void fileSaved(); - Q_SIGNAL void fileSaveFailed(const QString& error); +signals: + void fileLoaded(); + void fileLoadFailed(const QString& error); + void fileSaved(); + void fileSaveFailed(const QString& error); }; } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configscope.cpp b/plugin/src/Caelestia/Config/configscope.cpp index 7b2074e32..6f20f05d0 100644 --- a/plugin/src/Caelestia/Config/configscope.cpp +++ b/plugin/src/Caelestia/Config/configscope.cpp @@ -11,7 +11,7 @@ void ConfigScope::setScreen(const QString& screen) { return; m_screen = screen; - Q_EMIT screenChanged(); + emit screenChanged(); resolveConfig(); } @@ -28,12 +28,12 @@ void ConfigScope::resolveConfig() { if (m_config != newConfig) { m_config = newConfig; - Q_EMIT configChanged(); + emit configChanged(); } if (m_tokens != newTokens) { m_tokens = newTokens; - Q_EMIT tokensChanged(); + emit tokensChanged(); } } diff --git a/plugin/src/Caelestia/Config/configscope.hpp b/plugin/src/Caelestia/Config/configscope.hpp index 53ff54260..202cf48a5 100644 --- a/plugin/src/Caelestia/Config/configscope.hpp +++ b/plugin/src/Caelestia/Config/configscope.hpp @@ -31,9 +31,10 @@ class ConfigScope : public QQuickItem { static ConfigScope* find(QObject* object); - Q_SIGNAL void screenChanged(); - Q_SIGNAL void configChanged(); - Q_SIGNAL void tokensChanged(); +signals: + void screenChanged(); + void configChanged(); + void tokensChanged(); private: void resolveConfig(); diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 53c4a7071..b8b4ab79a 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -363,7 +363,8 @@ class Tokens : public QObject { static Tokens* qmlAttachedProperties(QObject* object); - Q_SIGNAL void sourceChanged(); +signals: + void sourceChanged(); private: void connectScope(); From 324acb492aa3ecdbea840ca3d46fd8cf65f5160f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:08:09 +1000 Subject: [PATCH 285/409] fix: ensure singletons exist when evaluating attached props --- plugin/src/Caelestia/Config/config.cpp | 8 +++++++- plugin/src/Caelestia/Config/tokens.cpp | 8 +++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index f78884f29..da9fdcbea 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -140,7 +140,8 @@ void Config::connectScope() { const Type* Config::name() const { \ if (m_scope && m_scope->config()) \ return m_scope->config()->name(); \ - return GlobalConfig::instance()->name(); \ + auto* global = GlobalConfig::instance(); \ + return global ? global->name() : nullptr; \ } CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) @@ -164,6 +165,11 @@ CONFIG_ATTACHED_GETTER(UserPaths, paths) #undef CONFIG_ATTACHED_GETTER Config* Config::qmlAttachedProperties(QObject* object) { + // Ensure GlobalConfig singleton is created before any attached property access + if (!GlobalConfig::instance()) { + if (auto* engine = qmlEngine(object)) + engine->singletonInstance("Caelestia.Config", "GlobalConfig"); + } return new Config(ConfigScope::find(object), object); } diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index ac383ba5d..2b0559fac 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -99,7 +99,8 @@ void Tokens::connectScope() { const Type* Tokens::name() const { \ if (m_scope && m_scope->tokens()) \ return m_scope->tokens()->name(); \ - return TokenConfig::instance()->name(); \ + auto* global = TokenConfig::instance(); \ + return global ? global->name() : nullptr; \ } TOKENS_ATTACHED_GETTER(AppearanceTokens, appearance) @@ -118,6 +119,11 @@ TOKENS_ATTACHED_GETTER(ControlCenterTokens, controlCenter) #undef TOKENS_ATTACHED_GETTER Tokens* Tokens::qmlAttachedProperties(QObject* object) { + // Ensure TokenConfig singleton is created before any attached property access + if (!TokenConfig::instance()) { + if (auto* engine = qmlEngine(object)) + engine->singletonInstance("Caelestia.Config", "TokenConfig"); + } return new Tokens(ConfigScope::find(object), object); } From 0429650a6e6121bf44235be40cb477bde4f861cf Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:16:35 +1000 Subject: [PATCH 286/409] feat: Tokens attached object points to Config.appearance --- plugin/src/Caelestia/Config/tokens.cpp | 64 ++++++++++++++++---------- plugin/src/Caelestia/Config/tokens.hpp | 40 ++++++---------- 2 files changed, 52 insertions(+), 52 deletions(-) diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 2b0559fac..3c987ac12 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -91,38 +91,52 @@ Tokens::Tokens(ConfigScope* scope, QObject* parent) void Tokens::connectScope() { if (!m_scope) return; - connect(m_scope, &ConfigScope::tokensChanged, this, &Tokens::sourceChanged); connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged); } -#define TOKENS_ATTACHED_GETTER(Type, name) \ - const Type* Tokens::name() const { \ - if (m_scope && m_scope->tokens()) \ - return m_scope->tokens()->name(); \ - auto* global = TokenConfig::instance(); \ - return global ? global->name() : nullptr; \ - } +// Resolve appearance from per-monitor GlobalConfig overlay or global GlobalConfig +static const AppearanceConfig* resolveAppearance(ConfigScope* scope) { + if (scope && scope->config()) + return scope->config()->appearance(); + auto* global = GlobalConfig::instance(); + return global ? global->appearance() : nullptr; +} + +const AppearanceRounding* Tokens::rounding() const { + auto* a = resolveAppearance(m_scope); + return a ? a->rounding() : nullptr; +} -TOKENS_ATTACHED_GETTER(AppearanceTokens, appearance) -TOKENS_ATTACHED_GETTER(BarTokens, bar) -TOKENS_ATTACHED_GETTER(DashboardTokens, dashboard) -TOKENS_ATTACHED_GETTER(LauncherTokens, launcher) -TOKENS_ATTACHED_GETTER(NotifsTokens, notifs) -TOKENS_ATTACHED_GETTER(OsdTokens, osd) -TOKENS_ATTACHED_GETTER(SessionTokens, session) -TOKENS_ATTACHED_GETTER(SidebarTokens, sidebar) -TOKENS_ATTACHED_GETTER(UtilitiesTokens, utilities) -TOKENS_ATTACHED_GETTER(LockTokens, lock) -TOKENS_ATTACHED_GETTER(WInfoTokens, winfo) -TOKENS_ATTACHED_GETTER(ControlCenterTokens, controlCenter) - -#undef TOKENS_ATTACHED_GETTER +const AppearanceSpacing* Tokens::spacing() const { + auto* a = resolveAppearance(m_scope); + return a ? a->spacing() : nullptr; +} + +const AppearancePadding* Tokens::padding() const { + auto* a = resolveAppearance(m_scope); + return a ? a->padding() : nullptr; +} + +const AppearanceFont* Tokens::font() const { + auto* a = resolveAppearance(m_scope); + return a ? a->font() : nullptr; +} + +const AppearanceAnim* Tokens::anim() const { + auto* a = resolveAppearance(m_scope); + return a ? a->anim() : nullptr; +} + +const AppearanceTransparency* Tokens::transparency() const { + auto* a = resolveAppearance(m_scope); + return a ? a->transparency() : nullptr; +} Tokens* Tokens::qmlAttachedProperties(QObject* object) { - // Ensure TokenConfig singleton is created before any attached property access - if (!TokenConfig::instance()) { + // Ensure GlobalConfig singleton is created before any attached property access + if (!GlobalConfig::instance()) { if (auto* engine = qmlEngine(object)) - engine->singletonInstance("Caelestia.Config", "TokenConfig"); + engine->singletonInstance("Caelestia.Config", "GlobalConfig"); } return new Tokens(ConfigScope::find(object), object); } diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index b8b4ab79a..7b38804cf 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -1,5 +1,6 @@ #pragma once +#include "appearanceconfig.hpp" #include "configobject.hpp" #include @@ -329,37 +330,22 @@ class Tokens : public QObject { QML_UNCREATABLE("") QML_ATTACHED(Tokens) - Q_PROPERTY(caelestia::config::ConfigScope* scope READ scope NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceTokens* appearance READ appearance NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::BarTokens* bar READ bar NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::DashboardTokens* dashboard READ dashboard NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::LauncherTokens* launcher READ launcher NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::NotifsTokens* notifs READ notifs NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::OsdTokens* osd READ osd NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::SessionTokens* session READ session NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::SidebarTokens* sidebar READ sidebar NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::UtilitiesTokens* utilities READ utilities NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::LockTokens* lock READ lock NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::WInfoTokens* winfo READ winfo NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::ControlCenterTokens* controlCenter READ controlCenter NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceAnim* anim READ anim NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) public: explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); - [[nodiscard]] ConfigScope* scope() const { return m_scope; } - - [[nodiscard]] const AppearanceTokens* appearance() const; - [[nodiscard]] const BarTokens* bar() const; - [[nodiscard]] const DashboardTokens* dashboard() const; - [[nodiscard]] const LauncherTokens* launcher() const; - [[nodiscard]] const NotifsTokens* notifs() const; - [[nodiscard]] const OsdTokens* osd() const; - [[nodiscard]] const SessionTokens* session() const; - [[nodiscard]] const SidebarTokens* sidebar() const; - [[nodiscard]] const UtilitiesTokens* utilities() const; - [[nodiscard]] const LockTokens* lock() const; - [[nodiscard]] const WInfoTokens* winfo() const; - [[nodiscard]] const ControlCenterTokens* controlCenter() const; + [[nodiscard]] const AppearanceRounding* rounding() const; + [[nodiscard]] const AppearanceSpacing* spacing() const; + [[nodiscard]] const AppearancePadding* padding() const; + [[nodiscard]] const AppearanceFont* font() const; + [[nodiscard]] const AppearanceAnim* anim() const; + [[nodiscard]] const AppearanceTransparency* transparency() const; static Tokens* qmlAttachedProperties(QObject* object); From 3a37d77d1fbb544c9c46846b9a718ae048e6444f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:18:56 +1000 Subject: [PATCH 287/409] feat: replace qml config module with c++ module --- components/Anim.qml | 6 +- components/CAnim.qml | 6 +- components/ConnectionHeader.qml | 8 +- components/ConnectionInfoSection.qml | 18 +- components/MaterialIcon.qml | 6 +- components/PropertyRow.qml | 8 +- components/SectionContainer.qml | 10 +- components/SectionHeader.qml | 6 +- components/StateLayer.qml | 6 +- components/StyledText.qml | 12 +- components/containers/StyledWindow.qml | 11 + components/controls/CircularIndicator.qml | 12 +- components/controls/CircularProgress.qml | 14 +- components/controls/CollapsibleSection.qml | 48 +- components/controls/CustomSpinBox.qml | 20 +- components/controls/FilledSlider.qml | 20 +- components/controls/IconButton.qml | 6 +- components/controls/IconTextButton.qml | 10 +- components/controls/Menu.qml | 18 +- components/controls/SpinBoxRow.qml | 10 +- components/controls/SplitButton.qml | 26 +- components/controls/SplitButtonRow.qml | 10 +- components/controls/StyledInputField.qml | 8 +- components/controls/StyledRadioButton.qml | 12 +- components/controls/StyledScrollBar.qml | 6 +- components/controls/StyledSlider.qml | 8 +- components/controls/StyledSwitch.qml | 22 +- components/controls/StyledTextField.qml | 10 +- components/controls/SwitchRow.qml | 10 +- components/controls/TextButton.qml | 8 +- components/controls/ToggleButton.qml | 22 +- components/controls/ToggleRow.qml | 4 +- components/controls/Tooltip.qml | 22 +- components/effects/InnerBorder.qml | 6 +- components/filedialog/CurrentItem.qml | 12 +- components/filedialog/DialogButtons.qml | 30 +- components/filedialog/FolderContents.qml | 40 +- components/filedialog/HeaderBar.qml | 32 +- components/filedialog/Sidebar.qml | 30 +- components/widgets/ExtraIndicator.qml | 16 +- config/Appearance.qml | 14 - config/AppearanceConfig.qml | 94 ---- config/BackgroundConfig.qml | 37 -- config/BarConfig.qml | 129 ----- config/BorderConfig.qml | 10 - config/Config.qml | 500 ------------------ config/ControlCenterConfig.qml | 10 - config/DashboardConfig.qml | 40 -- config/GeneralConfig.qml | 61 --- config/LauncherConfig.qml | 147 ----- config/LockConfig.qml | 15 - config/NotifsConfig.qml | 19 - config/OsdConfig.qml | 14 - config/ServiceConfig.qml | 24 - config/SessionConfig.qml | 29 - config/SidebarConfig.qml | 11 - config/UserPaths.qml | 11 - config/UtilitiesConfig.qml | 67 --- config/WInfoConfig.qml | 10 - modules/BatteryMonitor.qml | 2 +- modules/IdleMonitors.qml | 2 +- modules/areapicker/Picker.qml | 10 +- modules/background/Background.qml | 10 +- modules/background/DesktopClock.qml | 42 +- modules/background/Visualiser.qml | 10 +- modules/background/Wallpaper.qml | 18 +- modules/bar/Bar.qml | 6 +- modules/bar/BarWrapper.qml | 10 +- modules/bar/components/ActiveWindow.qml | 12 +- modules/bar/components/Clock.qml | 16 +- modules/bar/components/OsIcon.qml | 12 +- modules/bar/components/Power.qml | 10 +- modules/bar/components/Settings.qml | 10 +- modules/bar/components/SettingsIcon.qml | 10 +- modules/bar/components/StatusIcons.qml | 22 +- modules/bar/components/Tray.qml | 24 +- modules/bar/components/TrayItem.qml | 6 +- .../components/workspaces/ActiveIndicator.qml | 10 +- .../bar/components/workspaces/OccupiedBg.qml | 8 +- .../workspaces/SpecialWorkspaces.qml | 38 +- .../bar/components/workspaces/Workspace.qml | 10 +- .../bar/components/workspaces/Workspaces.qml | 14 +- modules/bar/popouts/ActiveWindow.qml | 20 +- modules/bar/popouts/Audio.qml | 20 +- modules/bar/popouts/Battery.qml | 38 +- modules/bar/popouts/Bluetooth.qml | 36 +- modules/bar/popouts/ClipWrapper.qml | 6 +- modules/bar/popouts/Content.qml | 10 +- modules/bar/popouts/LockStatus.qml | 4 +- modules/bar/popouts/Network.qml | 66 +-- modules/bar/popouts/TrayMenu.qml | 28 +- modules/bar/popouts/WirelessPassword.qml | 56 +- modules/bar/popouts/Wrapper.qml | 10 +- modules/bar/popouts/kblayout/KbLayout.qml | 32 +- .../bar/popouts/kblayout/KbLayoutModel.qml | 2 +- modules/controlcenter/ControlCenter.qml | 4 +- modules/controlcenter/NavRail.qml | 58 +- modules/controlcenter/Panes.qml | 6 +- modules/controlcenter/WindowTitle.qml | 12 +- .../appearance/AppearancePane.qml | 14 +- .../appearance/sections/AnimationsSection.qml | 4 +- .../appearance/sections/BackgroundSection.qml | 32 +- .../appearance/sections/BorderSection.qml | 6 +- .../sections/ColorSchemeSection.qml | 24 +- .../sections/ColorVariantSection.qml | 16 +- .../appearance/sections/FontsSection.qml | 46 +- .../appearance/sections/ScalesSection.qml | 8 +- .../appearance/sections/ThemeModeSection.qml | 2 +- .../sections/TransparencySection.qml | 6 +- modules/controlcenter/audio/AudioPane.qml | 96 ++-- modules/controlcenter/bluetooth/BtPane.qml | 2 +- modules/controlcenter/bluetooth/Details.qml | 142 ++--- .../controlcenter/bluetooth/DeviceList.qml | 56 +- modules/controlcenter/bluetooth/Settings.qml | 126 ++--- .../components/ConnectedButtonGroup.qml | 28 +- .../components/DeviceDetails.qml | 4 +- .../controlcenter/components/DeviceList.qml | 10 +- .../components/PaneTransition.qml | 18 +- .../components/ReadonlySlider.qml | 16 +- .../components/SettingsHeader.qml | 8 +- .../controlcenter/components/SliderInput.qml | 12 +- .../components/SplitPaneLayout.qml | 22 +- .../components/SplitPaneWithDetails.qml | 2 +- .../components/WallpaperGrid.qml | 24 +- .../controlcenter/dashboard/DashboardPane.qml | 20 +- .../dashboard/GeneralSection.qml | 6 +- .../dashboard/PerformanceSection.qml | 4 +- .../controlcenter/launcher/LauncherPane.qml | 84 +-- modules/controlcenter/launcher/Settings.qml | 24 +- .../controlcenter/network/EthernetDetails.qml | 10 +- .../controlcenter/network/EthernetList.qml | 34 +- .../controlcenter/network/EthernetPane.qml | 2 +- .../network/EthernetSettings.qml | 22 +- .../controlcenter/network/NetworkSettings.qml | 28 +- .../controlcenter/network/NetworkingPane.qml | 26 +- modules/controlcenter/network/VpnDetails.qml | 82 +-- modules/controlcenter/network/VpnList.qml | 128 ++--- modules/controlcenter/network/VpnSettings.qml | 28 +- .../controlcenter/network/WirelessDetails.qml | 14 +- .../controlcenter/network/WirelessList.qml | 48 +- .../controlcenter/network/WirelessPane.qml | 2 +- .../network/WirelessPasswordDialog.qml | 50 +- .../network/WirelessSettings.qml | 10 +- .../notifications/NotificationsPane.qml | 28 +- modules/controlcenter/taskbar/TaskbarPane.qml | 96 ++-- modules/dashboard/Content.qml | 18 +- modules/dashboard/Dash.qml | 18 +- modules/dashboard/LyricMenu.qml | 66 +-- modules/dashboard/LyricsView.qml | 12 +- modules/dashboard/Media.qml | 84 +-- modules/dashboard/Performance.qml | 142 ++--- modules/dashboard/Tabs.qml | 18 +- modules/dashboard/WeatherTab.qml | 72 +-- modules/dashboard/Wrapper.qml | 6 +- modules/dashboard/dash/Calendar.qml | 48 +- modules/dashboard/dash/DateTime.qml | 18 +- modules/dashboard/dash/Media.qml | 52 +- modules/dashboard/dash/Resources.qml | 16 +- modules/dashboard/dash/SmallWeather.qml | 12 +- modules/dashboard/dash/User.qml | 34 +- modules/drawers/ContentWindow.qml | 18 +- modules/drawers/Drawers.qml | 2 +- modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 4 +- modules/drawers/Regions.qml | 2 +- modules/launcher/AppList.qml | 28 +- modules/launcher/Content.qml | 20 +- modules/launcher/ContentList.qml | 24 +- modules/launcher/WallpaperList.qml | 4 +- modules/launcher/Wrapper.qml | 8 +- modules/launcher/items/ActionItem.qml | 20 +- modules/launcher/items/AppItem.qml | 18 +- modules/launcher/items/CalcItem.qml | 26 +- modules/launcher/items/SchemeItem.qml | 24 +- modules/launcher/items/VariantItem.qml | 22 +- modules/launcher/items/WallpaperItem.qml | 20 +- modules/launcher/services/Actions.qml | 2 +- modules/launcher/services/Apps.qml | 2 +- modules/launcher/services/M3Variants.qml | 2 +- modules/launcher/services/Schemes.qml | 2 +- modules/lock/Center.qml | 62 +-- modules/lock/Content.qml | 26 +- modules/lock/Fetch.qml | 38 +- modules/lock/InputField.qml | 16 +- modules/lock/LockSurface.qml | 54 +- modules/lock/Media.qml | 44 +- modules/lock/NotifDock.qml | 32 +- modules/lock/NotifGroup.qml | 46 +- modules/lock/Pam.qml | 2 +- modules/lock/Resources.qml | 24 +- modules/lock/WeatherInfo.qml | 44 +- modules/notifications/Content.qml | 28 +- modules/notifications/Notification.qml | 86 +-- modules/osd/Content.qml | 10 +- modules/osd/Wrapper.qml | 6 +- modules/session/Content.qml | 12 +- modules/session/Wrapper.qml | 6 +- modules/sidebar/Content.qml | 8 +- modules/sidebar/Notif.qml | 30 +- modules/sidebar/NotifActionList.qml | 20 +- modules/sidebar/NotifDock.qml | 42 +- modules/sidebar/NotifDockList.qml | 22 +- modules/sidebar/NotifGroup.qml | 56 +- modules/sidebar/NotifGroupList.qml | 14 +- modules/sidebar/Wrapper.qml | 10 +- modules/utilities/Content.qml | 4 +- modules/utilities/RecordingDeleteModal.qml | 32 +- modules/utilities/Wrapper.qml | 16 +- modules/utilities/cards/IdleInhibit.qml | 44 +- modules/utilities/cards/Record.qml | 60 +-- modules/utilities/cards/RecordingList.qml | 28 +- modules/utilities/cards/Toggles.qml | 26 +- modules/utilities/toasts/ToastItem.qml | 22 +- modules/utilities/toasts/Toasts.qml | 14 +- modules/windowinfo/Buttons.qml | 46 +- modules/windowinfo/Details.qml | 30 +- modules/windowinfo/Preview.qml | 18 +- modules/windowinfo/WindowInfo.qml | 12 +- services/Audio.qml | 2 +- services/Brightness.qml | 2 +- services/Colours.qml | 8 +- services/GameMode.qml | 2 +- services/Hypr.qml | 2 +- services/LyricsService.qml | 2 +- services/NetworkUsage.qml | 2 +- services/NotifData.qml | 2 +- services/Notifs.qml | 2 +- services/Players.qml | 2 +- services/Screens.qml | 2 +- services/SystemUsage.qml | 2 +- services/Time.qml | 2 +- services/VPN.qml | 2 +- services/Wallpapers.qml | 2 +- services/Weather.qml | 2 +- utils/Icons.qml | 2 +- utils/Paths.qml | 2 +- utils/SysInfo.qml | 2 +- 237 files changed, 2395 insertions(+), 3626 deletions(-) delete mode 100644 config/Appearance.qml delete mode 100644 config/AppearanceConfig.qml delete mode 100644 config/BackgroundConfig.qml delete mode 100644 config/BarConfig.qml delete mode 100644 config/BorderConfig.qml delete mode 100644 config/Config.qml delete mode 100644 config/ControlCenterConfig.qml delete mode 100644 config/DashboardConfig.qml delete mode 100644 config/GeneralConfig.qml delete mode 100644 config/LauncherConfig.qml delete mode 100644 config/LockConfig.qml delete mode 100644 config/NotifsConfig.qml delete mode 100644 config/OsdConfig.qml delete mode 100644 config/ServiceConfig.qml delete mode 100644 config/SessionConfig.qml delete mode 100644 config/SidebarConfig.qml delete mode 100644 config/UserPaths.qml delete mode 100644 config/UtilitiesConfig.qml delete mode 100644 config/WInfoConfig.qml diff --git a/components/Anim.qml b/components/Anim.qml index 505243a79..bf8eefb1c 100644 --- a/components/Anim.qml +++ b/components/Anim.qml @@ -1,8 +1,8 @@ import QtQuick -import qs.config +import Caelestia.Config NumberAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } diff --git a/components/CAnim.qml b/components/CAnim.qml index f2f4e40fe..3b7b7b2f8 100644 --- a/components/CAnim.qml +++ b/components/CAnim.qml @@ -1,8 +1,8 @@ import QtQuick -import qs.config +import Caelestia.Config ColorAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml index cdba17bc1..36886803f 100644 --- a/components/ConnectionHeader.qml +++ b/components/ConnectionHeader.qml @@ -1,7 +1,7 @@ import QtQuick import QtQuick.Layouts import qs.components -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -9,14 +9,14 @@ ColumnLayout { required property string icon required property string title - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Layout.alignment: Qt.AlignHCenter MaterialIcon { Layout.alignment: Qt.AlignHCenter animate: true text: root.icon - font.pointSize: Appearance.font.size.extraLarge * 3 + font.pointSize: Tokens.font.size.extraLarge * 3 font.bold: true } @@ -24,7 +24,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter animate: true text: root.title - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.bold: true } } diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml index 9063a6d26..bac3cc7cb 100644 --- a/components/ConnectionInfoSection.qml +++ b/components/ConnectionInfoSection.qml @@ -2,14 +2,14 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root required property var deviceDetails - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: qsTr("IP Address") @@ -18,40 +18,40 @@ ColumnLayout { StyledText { text: root.deviceDetails?.ipAddress || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Subnet Mask") } StyledText { text: root.deviceDetails?.subnet || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Gateway") } StyledText { text: root.deviceDetails?.gateway || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("DNS Servers") } StyledText { text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.Wrap Layout.maximumWidth: parent.width } diff --git a/components/MaterialIcon.qml b/components/MaterialIcon.qml index a1d19d3c0..e85d39be6 100644 --- a/components/MaterialIcon.qml +++ b/components/MaterialIcon.qml @@ -1,12 +1,12 @@ import qs.services -import qs.config +import Caelestia.Config StyledText { property real fill property int grade: Colours.light ? 0 : -25 - font.family: Appearance.font.family.material - font.pointSize: Appearance.font.size.larger + font.family: Tokens.font.family.material + font.pointSize: Tokens.font.size.larger font.variableAxes: ({ FILL: fill.toFixed(1), GRAD: grade, diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml index 67315b9c5..7f1a68bdc 100644 --- a/components/PropertyRow.qml +++ b/components/PropertyRow.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -11,16 +11,16 @@ ColumnLayout { required property string value property bool showTopMargin: false - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { - Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 + Layout.topMargin: root.showTopMargin ? Tokens.spacing.normal : 0 text: root.label } StyledText { text: root.value color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 8b57cdd43..05b4523c7 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -2,19 +2,19 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root default property alias content: contentColumn.data - property real contentSpacing: Appearance.spacing.larger + property real contentSpacing: Tokens.spacing.larger property bool alignTop: false Layout.fillWidth: true - implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 + implicitHeight: contentColumn.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh ColumnLayout { @@ -24,7 +24,7 @@ StyledRect { anchors.right: parent.right anchors.top: root.alignTop ? parent.top : undefined anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large spacing: root.contentSpacing } diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml index a77247692..107c6d368 100644 --- a/components/SectionHeader.qml +++ b/components/SectionHeader.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -13,9 +13,9 @@ ColumnLayout { spacing: 0 StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: root.title - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } diff --git a/components/StateLayer.qml b/components/StateLayer.qml index f3e3ea13d..59c5cb23a 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -1,6 +1,6 @@ import QtQuick import qs.services -import qs.config +import Caelestia.Config MouseArea { id: root @@ -63,7 +63,7 @@ MouseArea { properties: "implicitWidth,implicitHeight" from: 0 to: rippleAnim.radius * 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { target: ripple @@ -83,7 +83,7 @@ MouseArea { StyledRect { id: ripple - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.color opacity: 0 diff --git a/components/StyledText.qml b/components/StyledText.qml index 5bf8add34..051f58043 100644 --- a/components/StyledText.qml +++ b/components/StyledText.qml @@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.services -import qs.config +import Caelestia.Config Text { id: root @@ -11,13 +11,13 @@ Text { property string animateProp: "scale" property real animateFrom: 0 property real animateTo: 1 - property int animateDuration: Appearance.anim.durations.normal + property int animateDuration: Tokens.anim.durations.normal renderType: Text.NativeRendering textFormat: Text.PlainText color: Colours.palette.m3onSurface - font.family: Appearance.font.family.sans - font.pointSize: Appearance.font.size.smaller + font.family: Tokens.font.family.sans + font.pointSize: Tokens.font.size.smaller Behavior on color { CAnim {} @@ -29,12 +29,12 @@ Text { SequentialAnimation { Anim { to: root.animateFrom - easing.bezierCurve: Appearance.anim.curves.standardAccel + easing.bezierCurve: Tokens.anim.curves.standardAccel } PropertyAction {} Anim { to: root.animateTo - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } } } diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml index 0adca1fe6..57f319c09 100644 --- a/components/containers/StyledWindow.qml +++ b/components/containers/StyledWindow.qml @@ -1,11 +1,22 @@ import Quickshell import Quickshell.Wayland +import Caelestia.Config // qmllint disable uncreatable-type PanelWindow { // qmllint enable uncreatable-type + id: root + required property string name + default property alias contentData: scope.data WlrLayershell.namespace: `caelestia-${name}` color: "transparent" + + ConfigScope { + id: scope + + anchors.fill: parent + screen: root.screen.name + } } diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index 844765005..8bfee428e 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -3,7 +3,7 @@ import QtQuick import QtQuick.Templates import Caelestia.Internal import qs.services -import qs.config +import Caelestia.Config BusyIndicator { id: root @@ -19,8 +19,8 @@ BusyIndicator { Completing } - property real implicitSize: Appearance.font.size.normal * 3 - property real strokeWidth: Appearance.padding.small * 0.8 + property real implicitSize: Tokens.font.size.normal * 3 + property real strokeWidth: Tokens.padding.small * 0.8 property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer @@ -64,7 +64,7 @@ BusyIndicator { transitions: Transition { Anim { properties: "opacity,internalStrokeWidth" - duration: manager.completeEndDuration * Appearance.anim.durations.scale + duration: manager.completeEndDuration * Tokens.anim.durations.scale } } @@ -90,7 +90,7 @@ BusyIndicator { property: "progress" from: 0 to: 1 - duration: manager.duration * Appearance.anim.durations.scale + duration: manager.duration * Tokens.anim.durations.scale } NumberAnimation { @@ -99,7 +99,7 @@ BusyIndicator { property: "completeEndProgress" from: 0 to: 1 - duration: manager.completeEndDuration * Appearance.anim.durations.scale + duration: manager.completeEndDuration * Tokens.anim.durations.scale onFinished: { if (root.animState === CircularIndicator.Completing) root.animState = CircularIndicator.Stopped; diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index 8cd585eef..4cd5f9f7a 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -2,16 +2,16 @@ import ".." import QtQuick import QtQuick.Shapes import qs.services -import qs.config +import Caelestia.Config Shape { id: root property real value property int startAngle: -90 - property int strokeWidth: Appearance.padding.smaller + property int strokeWidth: Tokens.padding.smaller property int padding: 0 - property int spacing: Appearance.spacing.small + property int spacing: Tokens.spacing.small property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer @@ -27,7 +27,7 @@ Shape { fillColor: "transparent" strokeColor: root.bgColour strokeWidth: root.strokeWidth - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle + 360 * root.vValue + root.gapAngle @@ -40,7 +40,7 @@ Shape { Behavior on strokeColor { CAnim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -49,7 +49,7 @@ Shape { fillColor: "transparent" strokeColor: root.fgColour strokeWidth: root.strokeWidth - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle @@ -62,7 +62,7 @@ Shape { Behavior on strokeColor { CAnim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 7e164ebbd..19c7da9b8 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -3,7 +3,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -18,14 +18,14 @@ ColumnLayout { signal toggleRequested - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Layout.fillWidth: true Item { id: sectionHeaderItem Layout.fillWidth: true - Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.normal * 2, 48) RowLayout { id: titleRow @@ -33,13 +33,13 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.leftMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.normal + spacing: Tokens.spacing.normal StyledText { text: root.title - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -51,12 +51,12 @@ ColumnLayout { text: "expand_more" rotation: root.expanded ? 180 : 0 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal Behavior on rotation { Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standard + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standard } } } @@ -70,7 +70,7 @@ ColumnLayout { anchors.fill: parent color: Colours.palette.m3onSurface - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal showHoverBackground: false } } @@ -79,12 +79,12 @@ ColumnLayout { id: contentWrapper Layout.fillWidth: true - Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.small * 2) : 0 clip: true Behavior on Layout.preferredHeight { Anim { - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } } @@ -92,14 +92,14 @@ ColumnLayout { id: backgroundRect anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) opacity: root.showBackground && root.expanded ? 1.0 : 0.0 visible: root.showBackground Behavior on opacity { Anim { - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } } } @@ -109,16 +109,16 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right - y: Appearance.spacing.small - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - anchors.bottomMargin: Appearance.spacing.small - spacing: Appearance.spacing.small + y: Tokens.spacing.small + anchors.leftMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.normal + anchors.bottomMargin: Tokens.spacing.small + spacing: Tokens.spacing.small opacity: root.expanded ? 1.0 : 0.0 Behavior on opacity { Anim { - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } } @@ -126,12 +126,12 @@ ColumnLayout { id: descriptionText Layout.fillWidth: true - Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 - Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 + Layout.topMargin: root.description !== "" ? Tokens.spacing.smaller : 0 + Layout.bottomMargin: root.description !== "" ? Tokens.spacing.small : 0 visible: root.description !== "" text: root.description color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.Wrap } } diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index f63b1402b..6adb137c8 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -4,7 +4,7 @@ import ".." import QtQuick import QtQuick.Layouts import qs.services -import qs.config +import Caelestia.Config RowLayout { id: root @@ -20,7 +20,7 @@ RowLayout { signal valueModified(value: real) - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small onValueChanged: { if (!root.isEditing) { @@ -73,23 +73,23 @@ RowLayout { root.isEditing = false; } - padding: Appearance.padding.small - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal + padding: Tokens.padding.small + leftPadding: Tokens.padding.normal + rightPadding: Tokens.padding.normal background: StyledRect { implicitWidth: 100 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainerHigh } } StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { id: upState @@ -120,11 +120,11 @@ RowLayout { } StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: downIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { id: downState diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index c865482c8..5a65722f1 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -3,7 +3,7 @@ import "../effects" import QtQuick import QtQuick.Templates import qs.services -import qs.config +import Caelestia.Config Slider { id: root @@ -16,7 +16,7 @@ Slider { background: StyledRect { color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StyledRect { anchors.left: parent.left @@ -51,7 +51,7 @@ Slider { anchors.fill: parent color: Colours.palette.m3inverseSurface - radius: Appearance.rounding.full + radius: Tokens.rounding.full MouseArea { id: handleInteraction @@ -70,8 +70,8 @@ Slider { function update(): void { animate = !moving; binding.when = moving; - font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; - font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; + font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger; + font.family = moving ? Tokens.font.family.sans : Tokens.font.family.material; } text: root.icon @@ -96,8 +96,8 @@ Slider { target: icon property: "scale" to: 0 - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.normal / 2 + easing.bezierCurve: Tokens.anim.curves.standardAccel } ScriptAction { script: icon.update() @@ -106,8 +106,8 @@ Slider { target: icon property: "scale" to: 1 - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.normal / 2 + easing.bezierCurve: Tokens.anim.curves.standardDecel } } } @@ -140,7 +140,7 @@ Slider { Behavior on value { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index 569998941..609730943 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -1,7 +1,7 @@ import ".." import QtQuick import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -15,7 +15,7 @@ StyledRect { property alias icon: label.text property bool checked property bool toggle - property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller + property real padding: type === IconButton.Text ? Tokens.padding.small / 2 : Tokens.padding.smaller property alias font: label.font property int type: IconButton.Filled property bool disabled @@ -44,7 +44,7 @@ StyledRect { onCheckedChanged: internalChecked = checked - radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour implicitWidth: implicitHeight diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index d213b9be8..1fbf74ebc 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -2,7 +2,7 @@ import ".." import QtQuick import QtQuick.Layouts import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -17,8 +17,8 @@ StyledRect { property alias text: label.text property bool checked property bool toggle - property real horizontalPadding: Appearance.padding.normal - property real verticalPadding: Appearance.padding.smaller + property real horizontalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.smaller property alias font: label.font property int type: IconTextButton.Filled @@ -36,7 +36,7 @@ StyledRect { onCheckedChanged: internalChecked = checked - radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour implicitWidth: row.implicitWidth + horizontalPadding * 2 @@ -58,7 +58,7 @@ StyledRect { id: row anchors.centerIn: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { id: iconLabel diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 320f318cc..6c3757674 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -5,7 +5,7 @@ import "../effects" import QtQuick import QtQuick.Layouts import qs.services -import qs.config +import Caelestia.Config Elevation { id: root @@ -16,7 +16,7 @@ Elevation { signal itemSelected(item: MenuItem) - radius: Appearance.rounding.small / 2 + radius: Tokens.rounding.small / 2 level: 2 implicitWidth: Math.max(200, column.implicitWidth) @@ -46,8 +46,8 @@ Elevation { readonly property bool active: modelData === root.active Layout.fillWidth: true - implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) @@ -67,8 +67,8 @@ Elevation { id: menuOptionRow anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small MaterialIcon { Layout.alignment: Qt.AlignVCenter @@ -102,14 +102,14 @@ Elevation { Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial } } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index b2c6ef1a8..c5e5c0829 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -3,7 +3,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -16,8 +16,8 @@ StyledRect { property var onValueModified: function (value) {} Layout.fillWidth: true - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -30,8 +30,8 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index 0e9aef8f1..8759635c7 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -2,7 +2,7 @@ import ".." import QtQuick import QtQuick.Layouts import qs.services -import qs.config +import Caelestia.Config Row { id: root @@ -12,8 +12,8 @@ Row { Tonal } - property real horizontalPadding: Appearance.padding.normal - property real verticalPadding: Appearance.padding.smaller + property real horizontalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.smaller property int type: SplitButton.Filled property bool disabled property bool menuOnTop @@ -33,12 +33,12 @@ Row { property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) - spacing: Math.floor(Appearance.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.small / 2) StyledRect { - radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) - topRightRadius: Appearance.rounding.small / 2 - bottomRightRadius: Appearance.rounding.small / 2 + radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + topRightRadius: Tokens.rounding.small / 2 + bottomRightRadius: Tokens.rounding.small / 2 color: root.disabled ? root.disabledColour : root.colour implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 @@ -62,7 +62,7 @@ Row { anchors.centerIn: parent anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { id: iconLabel @@ -86,7 +86,7 @@ Row { Behavior on Layout.preferredWidth { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } } @@ -96,9 +96,9 @@ Row { StyledRect { id: expandBtn - property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2 + property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.small / 2 - radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) topLeftRadius: rad bottomLeftRadius: rad color: root.disabled ? root.disabledColour : root.colour @@ -157,8 +157,8 @@ Row { anchors.top: parent.bottom anchors.right: parent.right - anchors.topMargin: Appearance.spacing.small - anchors.bottomMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small + anchors.bottomMargin: Tokens.spacing.small } } } diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index a58e46d45..9082ce1de 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -5,7 +5,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -21,8 +21,8 @@ StyledRect { signal selected(item: MenuItem) Layout.fillWidth: true - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: false @@ -33,8 +33,8 @@ StyledRect { id: row anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index fb312128c..7cbf26c57 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -4,7 +4,7 @@ import ".." import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -22,14 +22,14 @@ Item { signal editingFinished implicitWidth: 70 - implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 + implicitHeight: inputField.implicitHeight + Tokens.padding.small * 2 StyledRect { id: container anchors.fill: parent color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) opacity: root.enabled ? 1 : 0.5 @@ -55,7 +55,7 @@ Item { id: inputField anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: root.horizontalAlignment validator: root.validator readOnly: root.readOnly diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index 458e6fe07..a2e993277 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -2,12 +2,12 @@ import QtQuick import QtQuick.Templates import qs.components import qs.services -import qs.config +import Caelestia.Config RadioButton { id: root - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) @@ -17,7 +17,7 @@ RadioButton { implicitWidth: 20 implicitHeight: 20 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: "transparent" border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant border.width: 2 @@ -28,7 +28,7 @@ RadioButton { root.click(); } - anchors.margins: -Appearance.padding.smaller + anchors.margins: -Tokens.padding.smaller color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary z: -1 } @@ -38,7 +38,7 @@ RadioButton { implicitWidth: 8 implicitHeight: 8 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0) } @@ -52,6 +52,6 @@ RadioButton { font.pointSize: root.font.pointSize anchors.verticalCenter: parent.verticalCenter anchors.left: outerCircle.right - anchors.leftMargin: Appearance.spacing.smaller + anchors.leftMargin: Tokens.spacing.smaller } } diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index 4c30dda3a..4cd86a4a3 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -2,7 +2,7 @@ import ".." import QtQuick import QtQuick.Templates import qs.services -import qs.config +import Caelestia.Config ScrollBar { id: root @@ -45,7 +45,7 @@ ScrollBar { } } } - implicitWidth: Appearance.padding.small + implicitWidth: Tokens.padding.small contentItem: StyledRect { anchors.left: parent.left @@ -61,7 +61,7 @@ ScrollBar { return 0.6; return 0; } - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3secondary MouseArea { diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index 21b2f2cbf..09349635b 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Templates import qs.components import qs.services -import qs.config +import Caelestia.Config Slider { id: root @@ -18,7 +18,7 @@ Slider { implicitWidth: root.handle.x - root.implicitHeight / 6 color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full topRightRadius: root.implicitHeight / 15 bottomRightRadius: root.implicitHeight / 15 } @@ -33,7 +33,7 @@ Slider { implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 color: Colours.palette.m3surfaceContainerHighest - radius: Appearance.rounding.full + radius: Tokens.rounding.full topLeftRadius: root.implicitHeight / 15 bottomLeftRadius: root.implicitHeight / 15 } @@ -46,7 +46,7 @@ Slider { implicitHeight: root.implicitHeight color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full MouseArea { anchors.fill: parent diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index b56410a9f..92cde7dad 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -3,7 +3,7 @@ import QtQuick import QtQuick.Shapes import QtQuick.Templates import qs.services -import qs.config +import Caelestia.Config Switch { id: root @@ -14,21 +14,21 @@ Switch { implicitHeight: implicitIndicatorHeight indicator: StyledRect { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) implicitWidth: implicitHeight * 1.7 - implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 + implicitHeight: Tokens.font.size.normal + Tokens.padding.smaller * 2 StyledRect { readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) - x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 + x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.small / 2 : Tokens.padding.small / 2 implicitWidth: nonAnimWidth - implicitHeight: parent.implicitHeight - Appearance.padding.small + implicitHeight: parent.implicitHeight - Tokens.padding.small anchors.verticalCenter: parent.verticalCenter StyledRect { @@ -83,15 +83,15 @@ Switch { anchors.centerIn: parent width: height - height: parent.implicitHeight - Appearance.padding.small * 2 + height: parent.implicitHeight - Tokens.padding.small * 2 preferredRendererType: Shape.CurveRenderer asynchronous: true ShapePath { - strokeWidth: Appearance.font.size.larger * 0.15 + strokeWidth: Tokens.font.size.larger * 0.15 strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest fillColor: "transparent" - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap startX: icon.start1.x startY: icon.start1.y @@ -145,8 +145,8 @@ Switch { } component PropAnim: PropertyAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } } diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index bf8e5b54b..9a810f797 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -4,15 +4,15 @@ import ".." import QtQuick import QtQuick.Controls import qs.services -import qs.config +import Caelestia.Config TextField { id: root color: Colours.palette.m3onSurface placeholderTextColor: Colours.palette.m3outline - font.family: Appearance.font.family.sans - font.pointSize: Appearance.font.size.smaller + font.family: Tokens.font.family.sans + font.pointSize: Tokens.font.size.smaller renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering cursorVisible: !readOnly @@ -25,7 +25,7 @@ TextField { implicitWidth: 2 color: Colours.palette.m3primary - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Connections { function onCursorPositionChanged(): void { @@ -61,7 +61,7 @@ TextField { Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 076080b64..01a42dd94 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -3,7 +3,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -13,8 +13,8 @@ StyledRect { property var onToggled: function (checked) {} Layout.fillWidth: true - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -27,8 +27,8 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index 1066910f0..a74c49e11 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -1,7 +1,7 @@ import ".." import QtQuick import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -15,8 +15,8 @@ StyledRect { property alias text: label.text property bool checked property bool toggle - property real horizontalPadding: Appearance.padding.normal - property real verticalPadding: Appearance.padding.smaller + property real horizontalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.smaller property alias font: label.font property int type: TextButton.Filled @@ -47,7 +47,7 @@ StyledRect { onCheckedChanged: internalChecked = checked - radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour implicitWidth: label.implicitWidth + horizontalPadding * 2 diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 1531b8b4d..4e4174b0b 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -15,9 +15,9 @@ StyledRect { property string icon property string label property string accent: "Secondary" - property real iconSize: Appearance.font.size.large - property real horizontalPadding: Appearance.padding.large - property real verticalPadding: Appearance.padding.normal + property real iconSize: Tokens.font.size.large + property real horizontalPadding: Tokens.padding.large + property real verticalPadding: Tokens.padding.normal property string tooltip: "" property bool hovered: false @@ -26,10 +26,10 @@ StyledRect { Component.onCompleted: { hovered = toggleStateLayer.containsMouse; } - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.normal * 2 : toggled ? Tokens.padding.small * 2 : 0) implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 - radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) + radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.small : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] Connections { @@ -57,7 +57,7 @@ StyledRect { id: toggleBtnInner anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { id: toggleBtnIcon @@ -87,15 +87,15 @@ StyledRect { Behavior on radius { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml index 2ec35dc77..01e9cc0e2 100644 --- a/components/controls/ToggleRow.qml +++ b/components/controls/ToggleRow.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.components.controls -import qs.config +import Caelestia.Config RowLayout { id: root @@ -12,7 +12,7 @@ RowLayout { property alias toggle: toggle Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index d550cc921..fdb27bea1 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -3,7 +3,7 @@ import QtQuick import QtQuick.Controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config Popup { id: root @@ -44,10 +44,10 @@ Popup { let newX = targetCenterX - tooltipWidth / 2; // Position tooltip above target - let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; + let newY = targetPos.y - tooltipHeight - Tokens.spacing.small; // Keep within bounds - const padding = Appearance.padding.normal; + const padding = Tokens.padding.normal; if (newX < padding) { newX = padding; } else if (newX + tooltipWidth > (parent.width - padding)) { @@ -100,8 +100,8 @@ Popup { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } @@ -110,19 +110,19 @@ Popup { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } contentItem: StyledRect { id: tooltipRect - implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: tooltipText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: tooltipText.implicitHeight + Tokens.padding.smaller * 2 color: Colours.palette.m3surfaceContainerHighest - radius: Appearance.rounding.small + radius: Tokens.rounding.small antialiasing: true // Add elevation for depth @@ -140,7 +140,7 @@ Popup { text: root.text color: Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index 462f77af0..2770c9bc8 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -4,7 +4,7 @@ import ".." import QtQuick import QtQuick.Effects import qs.services -import qs.config +import Caelestia.Config StyledRect { property alias innerRadius: maskInner.radius @@ -37,8 +37,8 @@ StyledRect { id: maskInner anchors.fill: parent - anchors.margins: Appearance.padding.normal - radius: Appearance.rounding.small + anchors.margins: Tokens.padding.normal + radius: Tokens.rounding.small } } } diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index 0603a626b..d410b13c4 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -2,15 +2,15 @@ import ".." import QtQuick import QtQuick.Shapes import qs.services -import qs.config +import Caelestia.Config Item { id: root required property var currentItem - implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin - implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 + implicitWidth: content.implicitWidth + Tokens.padding.larger + content.anchors.rightMargin + implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.normal + content.anchors.bottomMargin : 0 Shape { preferredRendererType: Shape.CurveRenderer @@ -18,7 +18,7 @@ Item { ShapePath { id: path - readonly property real rounding: Appearance.rounding.small + readonly property real rounding: Tokens.rounding.small readonly property bool flatten: root.implicitHeight < rounding * 2 readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding @@ -76,8 +76,8 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small - anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small + anchors.rightMargin: Tokens.padding.larger - Tokens.padding.small + anchors.bottomMargin: Tokens.padding.normal - Tokens.padding.small Connections { function onCurrentItemChanged(): void { diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index 725f8fc90..94cbd72d4 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -1,7 +1,7 @@ import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -9,7 +9,7 @@ StyledRect { required property var dialog required property FolderContents folder - implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer @@ -17,9 +17,9 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Filter:") @@ -28,14 +28,14 @@ StyledRect { StyledRect { Layout.fillWidth: true Layout.fillHeight: true - Layout.rightMargin: Appearance.spacing.normal + Layout.rightMargin: Tokens.spacing.normal color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.small + radius: Tokens.rounding.small StyledText { anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` } @@ -43,10 +43,10 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -60,7 +60,7 @@ StyledRect { id: selectText anchors.centerIn: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal text: qsTr("Select") color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -69,10 +69,10 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -84,7 +84,7 @@ StyledRect { id: cancelText anchors.centerIn: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal text: qsTr("Cancel") } diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 56a4a2849..980bd3e1b 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -10,7 +10,7 @@ import qs.components.controls import qs.components.filedialog import qs.components.images import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -42,8 +42,8 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: Appearance.padding.small - radius: Appearance.rounding.small + anchors.margins: Tokens.padding.small + radius: Tokens.rounding.small } } @@ -59,14 +59,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 font.weight: 500 } StyledText { text: qsTr("This folder is empty") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } } @@ -80,10 +80,10 @@ Item { id: view anchors.fill: parent - anchors.margins: Appearance.padding.small + Appearance.padding.normal + anchors.margins: Tokens.padding.small + Tokens.padding.normal - cellWidth: Sizes.itemWidth + Appearance.spacing.small - cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 + cellWidth: Sizes.itemWidth + Tokens.spacing.small + cellHeight: Sizes.itemWidth + Tokens.spacing.small * 2 + Tokens.padding.normal * 2 + 1 clip: true focus: true @@ -120,8 +120,8 @@ Item { properties: "opacity,scale" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -140,12 +140,12 @@ Item { Anim { properties: "opacity,scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { properties: "x,y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } @@ -153,7 +153,7 @@ Item { CurrentItem { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small currentItem: view.currentItem } @@ -164,12 +164,12 @@ Item { required property int index required property FileSystemEntry modelData - readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.normal * 2 implicitWidth: Sizes.itemWidth implicitHeight: nonAnimHeight - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 clip: true @@ -192,9 +192,9 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top - anchors.topMargin: Appearance.padding.normal + anchors.topMargin: Tokens.padding.normal - implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 + implicitSize: Sizes.itemWidth - Tokens.padding.normal * 2 Component.onCompleted: { const file = item.modelData; @@ -215,8 +215,8 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small - anchors.margins: Appearance.padding.normal + anchors.topMargin: Tokens.spacing.small + anchors.margins: Tokens.padding.normal horizontalAlignment: Text.AlignHCenter elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index b44ac58b3..c114bbd85 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -4,15 +4,15 @@ import ".." import QtQuick import QtQuick.Layouts import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root required property var dialog - implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: inner.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer @@ -20,19 +20,19 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small Item { implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { function onClicked(): void { root.dialog.cwd.pop(); } - radius: Appearance.rounding.small + radius: Tokens.rounding.small disabled: root.dialog.cwd.length === 1 } @@ -49,7 +49,7 @@ StyledRect { StyledRect { Layout.fillWidth: true - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainerHigh implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 @@ -58,10 +58,10 @@ StyledRect { id: pathComponents anchors.fill: parent - anchors.margins: Appearance.padding.small / 2 + anchors.margins: Tokens.padding.small / 2 anchors.leftMargin: 0 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: root.dialog.cwd @@ -76,7 +76,7 @@ StyledRect { Loader { asynchronous: true - Layout.rightMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.spacing.small active: folder.index > 0 sourceComponent: StyledText { text: "/" @@ -86,8 +86,8 @@ StyledRect { } Item { - implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.small : 0) + folderName.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: folderName.implicitHeight + Tokens.padding.small * 2 Loader { asynchronous: true @@ -98,7 +98,7 @@ StyledRect { root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); } - radius: Appearance.rounding.small + radius: Tokens.rounding.small } } @@ -109,7 +109,7 @@ StyledRect { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Tokens.padding.normal active: folder.index === 0 && folder.modelData === "Home" sourceComponent: MaterialIcon { @@ -124,7 +124,7 @@ StyledRect { anchors.left: homeIcon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 + anchors.leftMargin: homeIcon.active ? Tokens.padding.small : 0 text: folder.modelData color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index d6acb8115..885185c93 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import qs.components import qs.components.filedialog import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -13,7 +13,7 @@ StyledRect { required property var dialog implicitWidth: Sizes.sidebarWidth - implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer @@ -23,16 +23,16 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small / 2 + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small / 2 StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.padding.small / 2 - Layout.bottomMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.padding.small / 2 + Layout.bottomMargin: Tokens.spacing.normal text: qsTr("Files") color: Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.bold: true } @@ -46,9 +46,9 @@ StyledRect { readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] Layout.fillWidth: true - implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: placeInner.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) StateLayer { @@ -66,11 +66,11 @@ StyledRect { id: placeInner anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + anchors.margins: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: { @@ -92,7 +92,7 @@ StyledRect { return "folder"; } color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: place.selected ? 1 : 0 Behavior on fill { @@ -104,7 +104,7 @@ StyledRect { Layout.fillWidth: true text: place.modelData color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } } diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index 44738d85d..938f1ec16 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -2,19 +2,19 @@ import ".." import "../effects" import QtQuick import qs.services -import qs.config +import Caelestia.Config StyledRect { required property int extra anchors.right: parent.right - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal color: Colours.palette.m3tertiary - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: count.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: count.implicitHeight + Appearance.padding.small * 2 + implicitWidth: count.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: count.implicitHeight + Tokens.padding.small * 2 opacity: extra > 0 ? 1 : 0 scale: extra > 0 ? 1 : 0.5 @@ -38,14 +38,14 @@ StyledRect { Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } diff --git a/config/Appearance.qml b/config/Appearance.qml deleted file mode 100644 index 489620a07..000000000 --- a/config/Appearance.qml +++ /dev/null @@ -1,14 +0,0 @@ -pragma Singleton - -import Quickshell - -Singleton { - // Literally just here to shorten accessing stuff :woe: - // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx` - readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding // qmllint disable missing-property - readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing // qmllint disable missing-property - readonly property AppearanceConfig.Padding padding: Config.appearance.padding // qmllint disable missing-property - readonly property AppearanceConfig.FontStuff font: Config.appearance.font // qmllint disable missing-property - readonly property AppearanceConfig.Anim anim: Config.appearance.anim // qmllint disable missing-property - readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency // qmllint disable missing-property -} diff --git a/config/AppearanceConfig.qml b/config/AppearanceConfig.qml deleted file mode 100644 index cee140f66..000000000 --- a/config/AppearanceConfig.qml +++ /dev/null @@ -1,94 +0,0 @@ -import Quickshell.Io - -JsonObject { - property Rounding rounding: Rounding {} - property Spacing spacing: Spacing {} - property Padding padding: Padding {} - property FontStuff font: FontStuff {} - property Anim anim: Anim {} - property Transparency transparency: Transparency {} - - component Rounding: JsonObject { - property real scale: 1 - property int small: 12 * scale - property int normal: 17 * scale - property int large: 25 * scale - property int full: 1000 * scale - } - - component Spacing: JsonObject { - property real scale: 1 - property int small: 7 * scale - property int smaller: 10 * scale - property int normal: 12 * scale - property int larger: 15 * scale - property int large: 20 * scale - } - - component Padding: JsonObject { - property real scale: 1 - property int small: 5 * scale - property int smaller: 7 * scale - property int normal: 10 * scale - property int larger: 12 * scale - property int large: 15 * scale - } - - component FontFamily: JsonObject { - property string sans: "Rubik" - property string mono: "CaskaydiaCove NF" - property string material: "Material Symbols Rounded" - property string clock: "Rubik" - } - - component FontSize: JsonObject { - property real scale: 1 - property int small: 11 * scale - property int smaller: 12 * scale - property int normal: 13 * scale - property int larger: 15 * scale - property int large: 18 * scale - property int extraLarge: 28 * scale - } - - component FontStuff: JsonObject { - property FontFamily family: FontFamily {} - property FontSize size: FontSize {} - } - - component AnimCurves: JsonObject { - property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] - property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] - property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] - property list standard: [0.2, 0, 0, 1, 1, 1] - property list standardAccel: [0.3, 0, 1, 1, 1, 1] - property list standardDecel: [0, 0, 0, 1, 1, 1] - property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] - property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] - property list expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] - } - - component AnimDurations: JsonObject { - property real scale: 1 - property int small: 200 * scale - property int normal: 400 * scale - property int large: 600 * scale - property int extraLarge: 1000 * scale - property int expressiveFastSpatial: 350 * scale - property int expressiveDefaultSpatial: 500 * scale - property int expressiveSlowSpatial: 650 * scale - } - - component Anim: JsonObject { - property real mediaGifSpeedAdjustment: 300 - property real sessionGifSpeed: 0.7 - property AnimCurves curves: AnimCurves {} - property AnimDurations durations: AnimDurations {} - } - - component Transparency: JsonObject { - property bool enabled: false - property real base: 0.85 - property real layers: 0.4 - } -} diff --git a/config/BackgroundConfig.qml b/config/BackgroundConfig.qml deleted file mode 100644 index 8383f5248..000000000 --- a/config/BackgroundConfig.qml +++ /dev/null @@ -1,37 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool enabled: true - property bool wallpaperEnabled: true - property DesktopClock desktopClock: DesktopClock {} - property Visualiser visualiser: Visualiser {} - - component DesktopClock: JsonObject { - property bool enabled: false - property real scale: 1.0 - property string position: "bottom-right" - property bool invertColors: false - property DesktopClockBackground background: DesktopClockBackground {} - property DesktopClockShadow shadow: DesktopClockShadow {} - } - - component DesktopClockBackground: JsonObject { - property bool enabled: false - property real opacity: 0.7 - property bool blur: true - } - - component DesktopClockShadow: JsonObject { - property bool enabled: true - property real opacity: 0.7 - property real blur: 0.4 - } - - component Visualiser: JsonObject { - property bool enabled: false - property bool autoHide: true - property bool blur: false - property real rounding: 1 - property real spacing: 1 - } -} diff --git a/config/BarConfig.qml b/config/BarConfig.qml deleted file mode 100644 index 463302ab5..000000000 --- a/config/BarConfig.qml +++ /dev/null @@ -1,129 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool persistent: true - property bool showOnHover: true - property int dragThreshold: 20 - property ScrollActions scrollActions: ScrollActions {} - property Popouts popouts: Popouts {} - property Workspaces workspaces: Workspaces {} - property ActiveWindow activeWindow: ActiveWindow {} - property Tray tray: Tray {} - property Status status: Status {} - property Clock clock: Clock {} - property Sizes sizes: Sizes {} - property list excludedScreens: [] - - property list entries: [ - { - id: "logo", - enabled: true - }, - { - id: "workspaces", - enabled: true - }, - { - id: "spacer", - enabled: true - }, - { - id: "activeWindow", - enabled: true - }, - { - id: "spacer", - enabled: true - }, - { - id: "tray", - enabled: true - }, - { - id: "clock", - enabled: true - }, - { - id: "statusIcons", - enabled: true - }, - { - id: "power", - enabled: true - } - ] - - component ScrollActions: JsonObject { - property bool workspaces: true - property bool volume: true - property bool brightness: true - } - - component Popouts: JsonObject { - property bool activeWindow: true - property bool tray: true - property bool statusIcons: true - } - - component Workspaces: JsonObject { - property int shown: 5 - property bool activeIndicator: true - property bool occupiedBg: false - property bool showWindows: true - property bool showWindowsOnSpecialWorkspaces: showWindows - property int maxWindowIcons: 0 // 0 = unlimited - property bool activeTrail: false - property bool perMonitorWorkspaces: true - property string label: " " // if empty, will show workspace name's first letter - property string occupiedLabel: "󰮯" - property string activeLabel: "󰮯" - property string capitalisation: "preserve" // upper, lower, or preserve - relevant only if label is empty - property list specialWorkspaceIcons: [] - property list windowIcons: [ - { - regex: "steam(_app_(default|[0-9]+))?", - icon: "sports_esports" - } - ] - } - - component ActiveWindow: JsonObject { - property bool compact: false - property bool inverted: false - property bool showOnHover: true - } - - component Tray: JsonObject { - property bool background: false - property bool recolour: false - property bool compact: false - property list iconSubs: [] - property list hiddenIcons: [] - } - - component Status: JsonObject { - property bool showAudio: false - property bool showMicrophone: false - property bool showKbLayout: false - property bool showNetwork: true - property bool showWifi: true - property bool showBluetooth: true - property bool showBattery: true - property bool showLockStatus: true - } - - component Clock: JsonObject { - property bool background: false - property bool showDate: false - property bool showIcon: true - } - - component Sizes: JsonObject { - property int innerWidth: 40 - property int windowPreviewSize: 400 - property int trayMenuWidth: 300 - property int batteryWidth: 250 - property int networkWidth: 320 - property int kbLayoutWidth: 320 - } -} diff --git a/config/BorderConfig.qml b/config/BorderConfig.qml deleted file mode 100644 index 662320d9e..000000000 --- a/config/BorderConfig.qml +++ /dev/null @@ -1,10 +0,0 @@ -import Quickshell.Io -import qs.config - -JsonObject { - property int thickness: Config.appearance.padding.normal - property int rounding: Config.appearance.rounding.large - - readonly property int minThickness: 2 - readonly property int clampedThickness: Math.max(minThickness, thickness) -} diff --git a/config/Config.qml b/config/Config.qml deleted file mode 100644 index 997eaf1ee..000000000 --- a/config/Config.qml +++ /dev/null @@ -1,500 +0,0 @@ -pragma Singleton - -import QtQuick -import Quickshell -import Quickshell.Io -import Caelestia -import qs.utils - -Singleton { - id: root - - property alias appearance: adapter.appearance - property alias general: adapter.general - property alias background: adapter.background - property alias bar: adapter.bar - property alias border: adapter.border - property alias dashboard: adapter.dashboard - property alias controlCenter: adapter.controlCenter - property alias launcher: adapter.launcher - property alias notifs: adapter.notifs - property alias osd: adapter.osd - property alias session: adapter.session - property alias winfo: adapter.winfo - property alias lock: adapter.lock - property alias utilities: adapter.utilities - property alias sidebar: adapter.sidebar - property alias services: adapter.services - property alias paths: adapter.paths - - property bool recentlySaved: false - - // Public save function - call this to persist config changes - function save(): void { - saveTimer.restart(); - recentlySaved = true; - recentSaveCooldown.restart(); - } - - // Helper function to serialize the config object - function serializeConfig(): var { - return { - appearance: serializeAppearance(), - general: serializeGeneral(), - background: serializeBackground(), - bar: serializeBar(), - border: serializeBorder(), - dashboard: serializeDashboard(), - controlCenter: serializeControlCenter(), - launcher: serializeLauncher(), - notifs: serializeNotifs(), - osd: serializeOsd(), - session: serializeSession(), - winfo: serializeWinfo(), - lock: serializeLock(), - utilities: serializeUtilities(), - sidebar: serializeSidebar(), - services: serializeServices(), - paths: serializePaths() - }; - } - - function serializeAppearance(): var { - return { - rounding: { - scale: appearance.rounding.scale - }, - spacing: { - scale: appearance.spacing.scale - }, - padding: { - scale: appearance.padding.scale - }, - font: { - family: { - sans: appearance.font.family.sans, - mono: appearance.font.family.mono, - material: appearance.font.family.material, - clock: appearance.font.family.clock - }, - size: { - scale: appearance.font.size.scale - } - }, - anim: { - mediaGifSpeedAdjustment: 300, - sessionGifSpeed: 0.7, - durations: { - scale: appearance.anim.durations.scale - } - }, - transparency: { - enabled: appearance.transparency.enabled, - base: appearance.transparency.base, - layers: appearance.transparency.layers - } - }; - } - - function serializeGeneral(): var { - return { - logo: general.logo, - excludedScreens: general.excludedScreens, - apps: { - terminal: general.apps.terminal, - audio: general.apps.audio, - playback: general.apps.playback, - explorer: general.apps.explorer - }, - idle: { - lockBeforeSleep: general.idle.lockBeforeSleep, - inhibitWhenAudio: general.idle.inhibitWhenAudio, - timeouts: general.idle.timeouts - }, - battery: { - warnLevels: general.battery.warnLevels, - criticalLevel: general.battery.criticalLevel - } - }; - } - - function serializeBackground(): var { - return { - enabled: background.enabled, - wallpaperEnabled: background.wallpaperEnabled, - desktopClock: { - enabled: background.desktopClock.enabled, - scale: background.desktopClock.scale, - position: background.desktopClock.position, - invertColors: background.desktopClock.invertColors, - background: { - enabled: background.desktopClock.background.enabled, - opacity: background.desktopClock.background.opacity, - blur: background.desktopClock.background.blur - }, - shadow: { - enabled: background.desktopClock.shadow.enabled, - opacity: background.desktopClock.shadow.opacity, - blur: background.desktopClock.shadow.blur - } - }, - visualiser: { - enabled: background.visualiser.enabled, - autoHide: background.visualiser.autoHide, - blur: background.visualiser.blur, - rounding: background.visualiser.rounding, - spacing: background.visualiser.spacing - } - }; - } - - function serializeBar(): var { - return { - persistent: bar.persistent, - showOnHover: bar.showOnHover, - dragThreshold: bar.dragThreshold, - scrollActions: { - workspaces: bar.scrollActions.workspaces, - volume: bar.scrollActions.volume, - brightness: bar.scrollActions.brightness - }, - popouts: { - activeWindow: bar.popouts.activeWindow, - tray: bar.popouts.tray, - statusIcons: bar.popouts.statusIcons - }, - workspaces: { - shown: bar.workspaces.shown, - activeIndicator: bar.workspaces.activeIndicator, - occupiedBg: bar.workspaces.occupiedBg, - showWindows: bar.workspaces.showWindows, - showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, - maxWindowIcons: bar.workspaces.maxWindowIcons, - activeTrail: bar.workspaces.activeTrail, - perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, - label: bar.workspaces.label, - occupiedLabel: bar.workspaces.occupiedLabel, - activeLabel: bar.workspaces.activeLabel, - capitalisation: bar.workspaces.capitalisation, - specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons, - windowIcons: bar.workspaces.windowIcons - }, - activeWindow: { - compact: bar.activeWindow.compact, - inverted: bar.activeWindow.inverted, - showOnHover: bar.activeWindow.showOnHover - }, - tray: { - background: bar.tray.background, - recolour: bar.tray.recolour, - compact: bar.tray.compact, - iconSubs: bar.tray.iconSubs, - hiddenIcons: bar.tray.hiddenIcons - }, - status: { - showAudio: bar.status.showAudio, - showMicrophone: bar.status.showMicrophone, - showKbLayout: bar.status.showKbLayout, - showNetwork: bar.status.showNetwork, - showWifi: bar.status.showWifi, - showBluetooth: bar.status.showBluetooth, - showBattery: bar.status.showBattery, - showLockStatus: bar.status.showLockStatus - }, - clock: { - background: bar.clock.background, - showDate: bar.clock.showDate, - showIcon: bar.clock.showIcon - }, - entries: bar.entries, - excludedScreens: bar.excludedScreens - }; - } - - function serializeBorder(): var { - return { - thickness: border.thickness, - rounding: border.rounding - }; - } - - function serializeDashboard(): var { - return { - enabled: dashboard.enabled, - showOnHover: dashboard.showOnHover, - mediaUpdateInterval: dashboard.mediaUpdateInterval, - resourceUpdateInterval: dashboard.resourceUpdateInterval, - dragThreshold: dashboard.dragThreshold, - performance: { - showBattery: dashboard.performance.showBattery, - showGpu: dashboard.performance.showGpu, - showCpu: dashboard.performance.showCpu, - showMemory: dashboard.performance.showMemory, - showStorage: dashboard.performance.showStorage, - showNetwork: dashboard.performance.showNetwork - } - }; - } - - function serializeControlCenter(): var { - return {}; - } - - function serializeLauncher(): var { - return { - enabled: launcher.enabled, - showOnHover: launcher.showOnHover, - maxShown: launcher.maxShown, - maxWallpapers: launcher.maxWallpapers, - specialPrefix: launcher.specialPrefix, - actionPrefix: launcher.actionPrefix, - enableDangerousActions: launcher.enableDangerousActions, - dragThreshold: launcher.dragThreshold, - vimKeybinds: launcher.vimKeybinds, - favouriteApps: launcher.favouriteApps, - hiddenApps: launcher.hiddenApps, - useFuzzy: { - apps: launcher.useFuzzy.apps, - actions: launcher.useFuzzy.actions, - schemes: launcher.useFuzzy.schemes, - variants: launcher.useFuzzy.variants, - wallpapers: launcher.useFuzzy.wallpapers - }, - actions: launcher.actions - }; - } - - function serializeNotifs(): var { - return { - expire: notifs.expire, - fullscreen: notifs.fullscreen, - defaultExpireTimeout: notifs.defaultExpireTimeout, - clearThreshold: notifs.clearThreshold, - expandThreshold: notifs.expandThreshold, - actionOnClick: notifs.actionOnClick, - groupPreviewNum: notifs.groupPreviewNum, - openExpanded: notifs.openExpanded - }; - } - - function serializeOsd(): var { - return { - enabled: osd.enabled, - hideDelay: osd.hideDelay, - enableBrightness: osd.enableBrightness, - enableMicrophone: osd.enableMicrophone - }; - } - - function serializeSession(): var { - return { - enabled: session.enabled, - dragThreshold: session.dragThreshold, - vimKeybinds: session.vimKeybinds, - icons: { - logout: session.icons.logout, - shutdown: session.icons.shutdown, - hibernate: session.icons.hibernate, - reboot: session.icons.reboot - }, - commands: { - logout: session.commands.logout, - shutdown: session.commands.shutdown, - hibernate: session.commands.hibernate, - reboot: session.commands.reboot - } - }; - } - - function serializeWinfo(): var { - return {}; - } - - function serializeLock(): var { - return { - recolourLogo: lock.recolourLogo, - enableFprint: lock.enableFprint, - maxFprintTries: lock.maxFprintTries, - hideNotifs: lock.hideNotifs - }; - } - - function serializeUtilities(): var { - const vpnProviders = []; - for (let i = 0; i < utilities.vpn.provider.length; i++) { - const p = utilities.vpn.provider[i]; - const provider = { - displayName: p.displayName, - enabled: p.enabled, - interface: p.interface, - name: p.name - }; - if (p.connectCmd && p.connectCmd.length > 0) { - provider.connectCmd = p.connectCmd; - } - if (p.disconnectCmd && p.disconnectCmd.length > 0) { - provider.disconnectCmd = p.disconnectCmd; - } - vpnProviders.push(provider); - } - - return { - enabled: utilities.enabled, - maxToasts: utilities.maxToasts, - toasts: { - configLoaded: utilities.toasts.configLoaded, - fullscreen: utilities.toasts.fullscreen, - chargingChanged: utilities.toasts.chargingChanged, - gameModeChanged: utilities.toasts.gameModeChanged, - dndChanged: utilities.toasts.dndChanged, - audioOutputChanged: utilities.toasts.audioOutputChanged, - audioInputChanged: utilities.toasts.audioInputChanged, - capsLockChanged: utilities.toasts.capsLockChanged, - numLockChanged: utilities.toasts.numLockChanged, - kbLayoutChanged: utilities.toasts.kbLayoutChanged, - vpnChanged: utilities.toasts.vpnChanged, - nowPlaying: utilities.toasts.nowPlaying - }, - vpn: { - enabled: utilities.vpn.enabled, - provider: vpnProviders - }, - quickToggles: utilities.quickToggles - }; - } - - function serializeSidebar(): var { - return { - enabled: sidebar.enabled, - dragThreshold: sidebar.dragThreshold - }; - } - - function serializeServices(): var { - return { - weatherLocation: services.weatherLocation, - useFahrenheit: services.useFahrenheit, - useFahrenheitPerformance: services.useFahrenheitPerformance, - useTwelveHourClock: services.useTwelveHourClock, - gpuType: services.gpuType, - visualiserBars: services.visualiserBars, - audioIncrement: services.audioIncrement, - brightnessIncrement: services.brightnessIncrement, - maxVolume: services.maxVolume, - smartScheme: services.smartScheme, - defaultPlayer: services.defaultPlayer, - playerAliases: services.playerAliases, - showLyrics: services.showLyrics, - lyricsBackend: services.lyricsBackend - }; - } - - function serializePaths(): var { - return { - wallpaperDir: paths.wallpaperDir, - lyricsDir: paths.lyricsDir, - sessionGif: paths.sessionGif, - mediaGif: paths.mediaGif, - noNotifsPic: paths.noNotifsPic, - lockNoNotifsPic: paths.lockNoNotifsPic - }; - } - - ElapsedTimer { - id: timer - } - - Timer { - id: saveTimer - - interval: 500 - onTriggered: { - timer.restart(); - try { - // Parse current config to preserve structure and comments if possible - let config = {}; - try { - config = JSON.parse(fileView.text()); - } catch (e) { - // If parsing fails, start with empty object - config = {}; - } - - // Update config with current values - config = root.serializeConfig(); - - // Save to file with pretty printing - fileView.setText(JSON.stringify(config, null, 2)); - } catch (e) { - Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); - } - } - } - - Timer { - id: recentSaveCooldown - - interval: 2000 - onTriggered: { - root.recentlySaved = false; - } - } - - FileView { - id: fileView - - path: `${Paths.config}/shell.json` - watchChanges: true - onFileChanged: { - // Prevent reload loop - don't reload if we just saved - if (!root.recentlySaved) { - timer.restart(); - reload(); - } else { - // Self-initiated save - reload without toast - reload(); - } - } - onLoaded: { - try { - JSON.parse(text()); - const elapsed = timer.elapsedMs(); - // Only show toast for external changes (not our own saves) and when elapsed time is meaningful - if (adapter.utilities.toasts.configLoaded && !root.recentlySaved && elapsed > 0) { // qmllint disable unresolved-type - Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); // qmllint disable unresolved-type - } else if (adapter.utilities.toasts.configLoaded && root.recentlySaved && elapsed > 0) { // qmllint disable unresolved-type - Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); - } - } catch (e) { - Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); - } - } - onLoadFailed: err => { - if (err !== FileViewError.FileNotFound) - Toaster.toast(qsTr("Failed to read config file"), FileViewError.toString(err), "settings_alert", Toast.Warning); - } - onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) - - JsonAdapter { // qmllint disable unresolved-type - id: adapter - - property AppearanceConfig appearance: AppearanceConfig {} - property GeneralConfig general: GeneralConfig {} - property BackgroundConfig background: BackgroundConfig {} - property BarConfig bar: BarConfig {} - property BorderConfig border: BorderConfig {} - property DashboardConfig dashboard: DashboardConfig {} - property ControlCenterConfig controlCenter: ControlCenterConfig {} - property LauncherConfig launcher: LauncherConfig {} - property NotifsConfig notifs: NotifsConfig {} - property OsdConfig osd: OsdConfig {} - property SessionConfig session: SessionConfig {} - property WInfoConfig winfo: WInfoConfig {} - property LockConfig lock: LockConfig {} - property UtilitiesConfig utilities: UtilitiesConfig {} - property SidebarConfig sidebar: SidebarConfig {} - property ServiceConfig services: ServiceConfig {} - property UserPaths paths: UserPaths {} - } - } -} diff --git a/config/ControlCenterConfig.qml b/config/ControlCenterConfig.qml deleted file mode 100644 index a5889491d..000000000 --- a/config/ControlCenterConfig.qml +++ /dev/null @@ -1,10 +0,0 @@ -import Quickshell.Io - -JsonObject { - property Sizes sizes: Sizes {} - - component Sizes: JsonObject { - property real heightMult: 0.7 - property real ratio: 16 / 9 - } -} diff --git a/config/DashboardConfig.qml b/config/DashboardConfig.qml deleted file mode 100644 index 0a16cc1f9..000000000 --- a/config/DashboardConfig.qml +++ /dev/null @@ -1,40 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool enabled: true - property bool showOnHover: true - property int mediaUpdateInterval: 500 - property int resourceUpdateInterval: 1000 - property int dragThreshold: 50 - property bool showDashboard: true - property bool showMedia: true - property bool showPerformance: true - property bool showWeather: true - property Sizes sizes: Sizes {} - property Performance performance: Performance {} - - component Performance: JsonObject { - property bool showBattery: true - property bool showGpu: true - property bool showCpu: true - property bool showMemory: true - property bool showStorage: true - property bool showNetwork: true - } - - component Sizes: JsonObject { - readonly property int tabIndicatorHeight: 3 - readonly property int tabIndicatorSpacing: 5 - readonly property int infoWidth: 200 - readonly property int infoIconSize: 25 - readonly property int dateTimeWidth: 110 - readonly property int mediaWidth: 200 - readonly property int mediaProgressSweep: 180 - readonly property int mediaProgressThickness: 8 - readonly property int resourceProgessThickness: 10 - readonly property int weatherWidth: 250 - readonly property int mediaCoverArtSize: 150 - readonly property int mediaVisualiserSize: 80 - readonly property int resourceSize: 200 - } -} diff --git a/config/GeneralConfig.qml b/config/GeneralConfig.qml deleted file mode 100644 index 0d1d1e191..000000000 --- a/config/GeneralConfig.qml +++ /dev/null @@ -1,61 +0,0 @@ -import Quickshell.Io - -JsonObject { - property string logo: "" - property list excludedScreens: [] - property Apps apps: Apps {} - property Idle idle: Idle {} - property Battery battery: Battery {} - - component Apps: JsonObject { - property list terminal: ["foot"] - property list audio: ["pavucontrol"] - property list playback: ["mpv"] - property list explorer: ["thunar"] - } - - component Idle: JsonObject { - property bool lockBeforeSleep: true - property bool inhibitWhenAudio: true - property list timeouts: [ - { - timeout: 180, - idleAction: "lock" - }, - { - timeout: 300, - idleAction: "dpms off", - returnAction: "dpms on" - }, - { - timeout: 600, - idleAction: ["systemctl", "suspend-then-hibernate"] - } - ] - } - - component Battery: JsonObject { - property list warnLevels: [ - { - level: 20, - title: qsTr("Low battery"), - message: qsTr("You might want to plug in a charger"), - icon: "battery_android_frame_2" - }, - { - level: 10, - title: qsTr("Did you see the previous message?"), - message: qsTr("You should probably plug in a charger now"), - icon: "battery_android_frame_1" - }, - { - level: 5, - title: qsTr("Critical battery level"), - message: qsTr("PLUG THE CHARGER RIGHT NOW!!"), - icon: "battery_android_alert", - critical: true - }, - ] - property int criticalLevel: 3 - } -} diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml deleted file mode 100644 index 05088af05..000000000 --- a/config/LauncherConfig.qml +++ /dev/null @@ -1,147 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool enabled: true - property bool showOnHover: false - property int maxShown: 7 - property int maxWallpapers: 9 // Warning: even numbers look bad - property string specialPrefix: "@" - property string actionPrefix: ">" - property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout - property int dragThreshold: 50 - property bool vimKeybinds: false - property list favouriteApps: [] - property list hiddenApps: [] - property UseFuzzy useFuzzy: UseFuzzy {} - property Sizes sizes: Sizes {} - - property list actions: [ - { - name: "Calculator", - icon: "calculate", - description: "Do simple math equations (powered by Qalc)", - command: ["autocomplete", "calc"], - enabled: true, - dangerous: false - }, - { - name: "Scheme", - icon: "palette", - description: "Change the current colour scheme", - command: ["autocomplete", "scheme"], - enabled: true, - dangerous: false - }, - { - name: "Wallpaper", - icon: "image", - description: "Change the current wallpaper", - command: ["autocomplete", "wallpaper"], - enabled: true, - dangerous: false - }, - { - name: "Variant", - icon: "colors", - description: "Change the current scheme variant", - command: ["autocomplete", "variant"], - enabled: true, - dangerous: false - }, - { - name: "Transparency", - icon: "opacity", - description: "Change shell transparency", - command: ["autocomplete", "transparency"], - enabled: false, - dangerous: false - }, - { - name: "Random", - icon: "casino", - description: "Switch to a random wallpaper", - command: ["caelestia", "wallpaper", "-r"], - enabled: true, - dangerous: false - }, - { - name: "Light", - icon: "light_mode", - description: "Change the scheme to light mode", - command: ["setMode", "light"], - enabled: true, - dangerous: false - }, - { - name: "Dark", - icon: "dark_mode", - description: "Change the scheme to dark mode", - command: ["setMode", "dark"], - enabled: true, - dangerous: false - }, - { - name: "Shutdown", - icon: "power_settings_new", - description: "Shutdown the system", - command: ["systemctl", "poweroff"], - enabled: true, - dangerous: true - }, - { - name: "Reboot", - icon: "cached", - description: "Reboot the system", - command: ["systemctl", "reboot"], - enabled: true, - dangerous: true - }, - { - name: "Logout", - icon: "exit_to_app", - description: "Log out of the current session", - command: ["loginctl", "terminate-user", ""], - enabled: true, - dangerous: true - }, - { - name: "Lock", - icon: "lock", - description: "Lock the current session", - command: ["loginctl", "lock-session"], - enabled: true, - dangerous: false - }, - { - name: "Sleep", - icon: "bedtime", - description: "Suspend then hibernate", - command: ["systemctl", "suspend-then-hibernate"], - enabled: true, - dangerous: false - }, - { - name: "Settings", - icon: "settings", - description: "Configure the shell", - command: ["caelestia", "shell", "controlCenter", "open"], - enabled: true, - dangerous: false - } - ] - - component UseFuzzy: JsonObject { - property bool apps: false - property bool actions: false - property bool schemes: false - property bool variants: false - property bool wallpapers: false - } - - component Sizes: JsonObject { - property int itemWidth: 600 - property int itemHeight: 57 - property int wallpaperWidth: 280 - property int wallpaperHeight: 200 - } -} diff --git a/config/LockConfig.qml b/config/LockConfig.qml deleted file mode 100644 index d0a9fb357..000000000 --- a/config/LockConfig.qml +++ /dev/null @@ -1,15 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool recolourLogo: false - property bool enableFprint: true - property int maxFprintTries: 3 - property bool hideNotifs: false - property Sizes sizes: Sizes {} - - component Sizes: JsonObject { - property real heightMult: 0.7 - property real ratio: 16 / 9 - property int centerWidth: 600 - } -} diff --git a/config/NotifsConfig.qml b/config/NotifsConfig.qml deleted file mode 100644 index bd54b94b7..000000000 --- a/config/NotifsConfig.qml +++ /dev/null @@ -1,19 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool expire: true - property string fullscreen: "on" - property int defaultExpireTimeout: 5000 - property real clearThreshold: 0.3 - property int expandThreshold: 20 - property bool actionOnClick: false - property int groupPreviewNum: 3 - property bool openExpanded: false // Show the notifichation in expanded state when opening - property Sizes sizes: Sizes {} - - component Sizes: JsonObject { - property int width: 400 - property int image: 41 - property int badge: 20 - } -} diff --git a/config/OsdConfig.qml b/config/OsdConfig.qml deleted file mode 100644 index 543fc41e5..000000000 --- a/config/OsdConfig.qml +++ /dev/null @@ -1,14 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool enabled: true - property int hideDelay: 2000 - property bool enableBrightness: true - property bool enableMicrophone: false - property Sizes sizes: Sizes {} - - component Sizes: JsonObject { - property int sliderWidth: 30 - property int sliderHeight: 150 - } -} diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml deleted file mode 100644 index a9d7c3dcf..000000000 --- a/config/ServiceConfig.qml +++ /dev/null @@ -1,24 +0,0 @@ -import QtQuick -import Quickshell.Io - -JsonObject { - property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" - property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) - property bool useFahrenheitPerformance: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) - property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") - property string gpuType: "" - property int visualiserBars: 45 - property real audioIncrement: 0.1 - property real brightnessIncrement: 0.1 - property real maxVolume: 1.0 - property bool smartScheme: true - property string defaultPlayer: "Spotify" - property list playerAliases: [ - { - "from": "com.github.th_ch.youtube_music", - "to": "YT Music" - } - ] - property bool showLyrics: false - property string lyricsBackend: "Auto" -} diff --git a/config/SessionConfig.qml b/config/SessionConfig.qml deleted file mode 100644 index 414f821a2..000000000 --- a/config/SessionConfig.qml +++ /dev/null @@ -1,29 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool enabled: true - property int dragThreshold: 30 - property bool vimKeybinds: false - property Icons icons: Icons {} - property Commands commands: Commands {} - - property Sizes sizes: Sizes {} - - component Icons: JsonObject { - property string logout: "logout" - property string shutdown: "power_settings_new" - property string hibernate: "downloading" - property string reboot: "cached" - } - - component Commands: JsonObject { - property list logout: ["loginctl", "terminate-user", ""] - property list shutdown: ["systemctl", "poweroff"] - property list hibernate: ["systemctl", "hibernate"] - property list reboot: ["systemctl", "reboot"] - } - - component Sizes: JsonObject { - property int button: 80 - } -} diff --git a/config/SidebarConfig.qml b/config/SidebarConfig.qml deleted file mode 100644 index a871562b9..000000000 --- a/config/SidebarConfig.qml +++ /dev/null @@ -1,11 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool enabled: true - property int dragThreshold: 80 - property Sizes sizes: Sizes {} - - component Sizes: JsonObject { - property int width: 430 - } -} diff --git a/config/UserPaths.qml b/config/UserPaths.qml deleted file mode 100644 index 3726017c5..000000000 --- a/config/UserPaths.qml +++ /dev/null @@ -1,11 +0,0 @@ -import Quickshell.Io -import qs.utils - -JsonObject { - property string wallpaperDir: `${Paths.pictures}/Wallpapers` - property string lyricsDir: `${Paths.home}/Music/lyrics/` - property string sessionGif: "root:/assets/kurukuru.gif" - property string mediaGif: "root:/assets/bongocat.gif" - property string noNotifsPic: "root:/assets/dino.png" - property string lockNoNotifsPic: "root:/assets/dino.png" -} diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml deleted file mode 100644 index 61cf16786..000000000 --- a/config/UtilitiesConfig.qml +++ /dev/null @@ -1,67 +0,0 @@ -import Quickshell.Io - -JsonObject { - property bool enabled: true - property int maxToasts: 4 - - property Sizes sizes: Sizes {} - property Toasts toasts: Toasts {} - property Vpn vpn: Vpn {} - - property list quickToggles: [ - { - id: "wifi", - enabled: true - }, - { - id: "bluetooth", - enabled: true - }, - { - id: "mic", - enabled: true - }, - { - id: "settings", - enabled: true - }, - { - id: "gameMode", - enabled: true - }, - { - id: "dnd", - enabled: true - }, - { - id: "vpn", - enabled: false - } - ] - - component Sizes: JsonObject { - property int width: 430 - property int toastWidth: 430 - } - - component Toasts: JsonObject { - property bool configLoaded: true - property string fullscreen: "off" - property bool chargingChanged: true - property bool gameModeChanged: true - property bool dndChanged: true - property bool audioOutputChanged: true - property bool audioInputChanged: true - property bool capsLockChanged: true - property bool numLockChanged: true - property bool kbLayoutChanged: true - property bool kbLimit: true - property bool vpnChanged: true - property bool nowPlaying: false - } - - component Vpn: JsonObject { - property bool enabled: false - property list provider: [] - } -} diff --git a/config/WInfoConfig.qml b/config/WInfoConfig.qml deleted file mode 100644 index 502578075..000000000 --- a/config/WInfoConfig.qml +++ /dev/null @@ -1,10 +0,0 @@ -import Quickshell.Io - -JsonObject { - property Sizes sizes: Sizes {} - - component Sizes: JsonObject { - property real heightMult: 0.7 - property real detailsWidth: 500 - } -} diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index 0596a1aa6..b64122830 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -2,7 +2,7 @@ import QtQuick import Quickshell import Quickshell.Services.UPower import Caelestia -import qs.config +import Caelestia.Config Scope { id: root diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml index 440dc704d..c396bc8b1 100644 --- a/modules/IdleMonitors.qml +++ b/modules/IdleMonitors.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Wayland import Caelestia.Internal import qs.services -import qs.config +import Caelestia.Config Scope { id: root diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index e6272737a..6234d192d 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -8,7 +8,7 @@ import Quickshell.Wayland import Caelestia import qs.components import qs.services -import qs.config +import Caelestia.Config MouseArea { id: root @@ -167,7 +167,7 @@ MouseArea { target: root property: "opacity" to: 0 - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } ExAnim { target: root @@ -278,7 +278,7 @@ MouseArea { Behavior on opacity { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } @@ -307,7 +307,7 @@ MouseArea { } component ExAnim: Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 0f44b1a00..14669eabf 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Wayland import qs.components.containers import qs.services -import qs.config +import Caelestia.Config Variants { model: Config.background.enabled ? Screens.screens : [] @@ -56,8 +56,8 @@ Variants { asynchronous: true active: Config.background.desktopClock.enabled - anchors.margins: Appearance.padding.large * 2 - anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) + anchors.margins: Tokens.padding.large * 2 + anchors.leftMargin: Tokens.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) state: Config.background.desktopClock.position states: [ @@ -146,8 +146,8 @@ Variants { transitions: Transition { AnchorAnimation { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 67f77e44e..43f3fcef8 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -5,7 +5,7 @@ import QtQuick.Effects import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -23,8 +23,8 @@ Item { readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary - implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.clockScale) - implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.clockScale) + implicitWidth: layout.implicitWidth + (Tokens.padding.large * 4 * root.clockScale) + implicitHeight: layout.implicitHeight + (Tokens.padding.large * 2 * root.clockScale) Item { id: clockContainer @@ -63,7 +63,7 @@ Item { visible: root.bgEnabled anchors.fill: parent - radius: Appearance.rounding.large * root.clockScale + radius: Tokens.rounding.large * root.clockScale opacity: Config.background.desktopClock.background.opacity color: Colours.palette.m3surface @@ -74,29 +74,29 @@ Item { id: layout anchors.centerIn: parent - spacing: Appearance.spacing.larger * root.clockScale + spacing: Tokens.spacing.larger * root.clockScale RowLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: Time.hourStr - font.pointSize: Appearance.font.size.extraLarge * 3 * root.clockScale + font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale font.weight: Font.Bold color: root.safePrimary } StyledText { text: ":" - font.pointSize: Appearance.font.size.extraLarge * 3 * root.clockScale + font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale color: root.safeTertiary opacity: 0.8 - Layout.topMargin: -Appearance.padding.large * 1.5 * root.clockScale + Layout.topMargin: -Tokens.padding.large * 1.5 * root.clockScale } StyledText { text: Time.minuteStr - font.pointSize: Appearance.font.size.extraLarge * 3 * root.clockScale + font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale font.weight: Font.Bold color: root.safeSecondary } @@ -104,14 +104,14 @@ Item { Loader { asynchronous: true Layout.alignment: Qt.AlignTop - Layout.topMargin: Appearance.padding.large * 1.4 * root.clockScale + Layout.topMargin: Tokens.padding.large * 1.4 * root.clockScale active: Config.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr - font.pointSize: Appearance.font.size.large * root.clockScale + font.pointSize: Tokens.font.size.large * root.clockScale color: root.safeSecondary } } @@ -120,9 +120,9 @@ Item { StyledRect { Layout.fillHeight: true Layout.preferredWidth: 4 * root.clockScale - Layout.topMargin: Appearance.spacing.larger * root.clockScale - Layout.bottomMargin: Appearance.spacing.larger * root.clockScale - radius: Appearance.rounding.full + Layout.topMargin: Tokens.spacing.larger * root.clockScale + Layout.bottomMargin: Tokens.spacing.larger * root.clockScale + radius: Tokens.rounding.full color: root.safePrimary opacity: 0.8 } @@ -132,7 +132,7 @@ Item { StyledText { text: Time.format("MMMM").toUpperCase() - font.pointSize: Appearance.font.size.large * root.clockScale + font.pointSize: Tokens.font.size.large * root.clockScale font.letterSpacing: 4 font.weight: Font.Bold color: root.safeSecondary @@ -140,7 +140,7 @@ Item { StyledText { text: Time.format("dd") - font.pointSize: Appearance.font.size.extraLarge * root.clockScale + font.pointSize: Tokens.font.size.extraLarge * root.clockScale font.letterSpacing: 2 font.weight: Font.Medium color: root.safePrimary @@ -148,7 +148,7 @@ Item { StyledText { text: Time.format("dddd") - font.pointSize: Appearance.font.size.larger * root.clockScale + font.pointSize: Tokens.font.size.larger * root.clockScale font.letterSpacing: 2 color: root.safeSecondary } @@ -158,14 +158,14 @@ Item { Behavior on clockScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on implicitWidth { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index 2f59a3001..ebb28dfab 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -7,7 +7,7 @@ import Caelestia.Internal import Caelestia.Services import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -60,14 +60,14 @@ Item { anchors.fill: parent anchors.margins: Config.border.thickness - anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing + anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Tokens.spacing.small * Config.background.visualiser.spacing values: Audio.cava.values primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) - rounding: Appearance.rounding.small * Config.background.visualiser.rounding - spacing: Appearance.spacing.small * Config.background.visualiser.spacing - animationDuration: Appearance.anim.durations.normal + rounding: Tokens.rounding.small * Config.background.visualiser.rounding + spacing: Tokens.spacing.small * Config.background.visualiser.spacing + animationDuration: Tokens.anim.durations.normal Behavior on anchors.leftMargin { Anim {} diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index 10d743baa..f226d062a 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -5,7 +5,7 @@ import qs.components import qs.components.filedialog import qs.components.images import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -43,30 +43,30 @@ Item { Row { anchors.centerIn: parent - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large MaterialIcon { text: "sentiment_stressed" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraLarge * 5 + font.pointSize: Tokens.font.size.extraLarge * 5 } Column { anchors.verticalCenter: parent.verticalCenter - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Wallpaper missing?") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 font.bold: true } StyledRect { - implicitWidth: selectWallText.implicitWidth + Appearance.padding.large * 2 - implicitHeight: selectWallText.implicitHeight + Appearance.padding.small * 2 + implicitWidth: selectWallText.implicitWidth + Tokens.padding.large * 2 + implicitHeight: selectWallText.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary FileDialog { @@ -94,7 +94,7 @@ Item { text: qsTr("Set it now!") color: Colours.palette.m3onPrimary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 3b99a44bc..5d127f57e 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -8,7 +8,7 @@ import QtQuick.Layouts import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -17,7 +17,7 @@ ColumnLayout { required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts required property bool fullscreen - readonly property int vPadding: Appearance.padding.large + readonly property int vPadding: Tokens.padding.large function closeTray(): void { if (!Config.bar.tray.compact) @@ -102,7 +102,7 @@ ColumnLayout { } } - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Repeater { id: repeater diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index ec9df2d18..8ea61505e 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.components -import qs.config +import Caelestia.Config import qs.utils import qs.modules.bar.popouts as BarPopouts @@ -18,7 +18,7 @@ Item { readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) - readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) + readonly property int padding: Math.max(Tokens.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) @@ -57,8 +57,8 @@ Item { Anim { target: root property: "implicitWidth" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } }, Transition { @@ -68,7 +68,7 @@ Item { Anim { target: root property: "implicitWidth" - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } ] diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 0fa277ee4..63cdb04a9 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -86,8 +86,8 @@ Item { id: metrics text: root.windowTitle - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.smaller + font.family: Tokens.font.family.mono elide: Qt.ElideRight elideWidth: root.maxHeight - icon.height @@ -101,8 +101,8 @@ Item { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -111,7 +111,7 @@ Item { anchors.horizontalCenter: icon.horizontalCenter anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small font.pointSize: metrics.font.pointSize font.family: metrics.font.family diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 90ed78cf1..ead13d9e6 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -3,25 +3,25 @@ pragma ComponentBehavior: Bound import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root readonly property color colour: Colours.palette.m3tertiary - readonly property int padding: Config.bar.clock.background ? Appearance.padding.normal : Appearance.padding.small + readonly property int padding: Config.bar.clock.background ? Tokens.padding.normal : Tokens.padding.small implicitWidth: Config.bar.sizes.innerWidth implicitHeight: layout.implicitHeight + root.padding * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.full + radius: Tokens.rounding.full Column { id: layout anchors.centerIn: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Loader { asynchronous: true @@ -43,8 +43,8 @@ StyledRect { horizontalAlignment: StyledText.AlignHCenter text: Time.format("ddd\nd") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.sans + font.pointSize: Tokens.font.size.smaller + font.family: Tokens.font.family.sans color: root.colour } @@ -63,8 +63,8 @@ StyledRect { horizontalAlignment: StyledText.AlignHCenter text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.smaller + font.family: Tokens.font.family.mono color: root.colour } } diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index fc16a4fca..47a8c88fa 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -2,14 +2,14 @@ import QtQuick import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { id: root - implicitWidth: Math.round(Appearance.font.size.large * 1.2) - implicitHeight: Math.round(Appearance.font.size.large * 1.2) + implicitWidth: Math.round(Tokens.font.size.large * 1.2) + implicitHeight: Math.round(Tokens.font.size.large * 1.2) MouseArea { anchors.fill: parent @@ -30,8 +30,8 @@ Item { id: caelestiaLogo Logo { - implicitWidth: Math.round(Appearance.font.size.large * 1.6) - implicitHeight: Math.round(Appearance.font.size.large * 1.6) + implicitWidth: Math.round(Tokens.font.size.large * 1.6) + implicitHeight: Math.round(Tokens.font.size.large * 1.6) } } @@ -40,7 +40,7 @@ Item { ColouredIcon { source: SysInfo.osLogo - implicitSize: Math.round(Appearance.font.size.large * 1.2) + implicitSize: Math.round(Tokens.font.size.large * 1.2) colour: Colours.palette.m3tertiary } } diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 5f5738115..f3f4706ae 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -1,14 +1,14 @@ import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root required property DrawerVisibilities visibilities - implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { @@ -20,8 +20,8 @@ Item { anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 - radius: Appearance.rounding.full + implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + radius: Tokens.rounding.full } MaterialIcon { @@ -33,6 +33,6 @@ Item { text: "power_settings_new" color: Colours.palette.m3error font.bold: true - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index f6ed88af6..f060225c6 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -1,13 +1,13 @@ import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.modules.controlcenter Item { id: root - implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { @@ -21,8 +21,8 @@ Item { anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 - radius: Appearance.rounding.full + implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + radius: Tokens.rounding.full } MaterialIcon { @@ -34,6 +34,6 @@ Item { text: "settings" color: Colours.palette.m3onSurface font.bold: true - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index f6ed88af6..f060225c6 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -1,13 +1,13 @@ import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.modules.controlcenter Item { id: root - implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { @@ -21,8 +21,8 @@ Item { anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 - radius: Appearance.rounding.full + implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + radius: Tokens.rounding.full } MaterialIcon { @@ -34,6 +34,6 @@ Item { text: "settings" color: Colours.palette.m3onSurface font.bold: true - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 455e04ef1..c32bc21f9 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -7,7 +7,7 @@ import Quickshell.Bluetooth import Quickshell.Services.UPower import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.utils StyledRect { @@ -17,11 +17,11 @@ StyledRect { readonly property alias items: iconColumn color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full clip: true implicitWidth: Config.bar.sizes.innerWidth - implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) + implicitHeight: iconColumn.implicitHeight + Tokens.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) ColumnLayout { id: iconColumn @@ -29,9 +29,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: Appearance.padding.normal + anchors.bottomMargin: Tokens.padding.normal - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 // Lock keys status WrappedLoader { @@ -136,7 +136,7 @@ StyledRect { animate: true text: Hypr.kbLayout color: root.colour - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono } } @@ -172,7 +172,7 @@ StyledRect { active: Config.bar.status.showBluetooth sourceComponent: ColumnLayout { - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 // Bluetooth icon MaterialIcon { @@ -211,14 +211,14 @@ StyledRect { Anim { from: 1 to: 0 - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.large + easing.bezierCurve: Tokens.anim.curves.standardAccel } Anim { from: 0 to: 1 - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.large + easing.bezierCurve: Tokens.anim.curves.standardDecel } } } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 79b57755e..76785813f 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Services.SystemTray import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -14,8 +14,8 @@ StyledRect { readonly property alias items: items readonly property alias expandIcon: expandIcon - readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small - readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0 + readonly property int padding: Config.bar.tray.background ? Tokens.padding.normal : Tokens.padding.small + readonly property int spacing: Config.bar.tray.background ? Tokens.spacing.small : 0 property bool expanded @@ -32,7 +32,7 @@ StyledRect { implicitHeight: nonAnimHeight color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.full + radius: Tokens.rounding.full Column { id: layout @@ -40,7 +40,7 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: root.padding - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0 @@ -49,7 +49,7 @@ StyledRect { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } } @@ -57,7 +57,7 @@ StyledRect { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { properties: "x,y" @@ -91,16 +91,16 @@ StyledRect { sourceComponent: Item { implicitWidth: expandIconInner.implicitWidth - implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2 + implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small * 2 MaterialIcon { id: expandIconInner anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small + anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.small : -Tokens.padding.small text: "expand_less" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large rotation: root.expanded ? 180 : 0 Behavior on rotation { @@ -116,8 +116,8 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index c5cb9fe82..1320eb2aa 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell.Services.SystemTray import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils MouseArea { @@ -13,8 +13,8 @@ MouseArea { required property SystemTrayItem modelData acceptedButtons: Qt.LeftButton | Qt.RightButton - implicitWidth: Appearance.font.size.small * 2 - implicitHeight: Appearance.font.size.small * 2 + implicitWidth: Tokens.font.size.small * 2 + implicitHeight: Tokens.font.size.small * 2 onClicked: event => { if (event.button === Qt.LeftButton) diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index d10086fd7..0c785901d 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -2,7 +2,7 @@ import QtQuick import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -42,9 +42,9 @@ StyledRect { clip: true y: offset + mask.y - implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + implicitWidth: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 implicitHeight: size - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary Colouriser { @@ -70,7 +70,7 @@ StyledRect { enabled: Config.bar.workspaces.activeTrail EAnim { - duration: Appearance.anim.durations.normal * 2 + duration: Tokens.anim.durations.normal * 2 } } @@ -93,6 +93,6 @@ StyledRect { } component EAnim: Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 3f0871603..891b1250f 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -65,18 +65,18 @@ Item { anchors.horizontalCenter: root.horizontalCenter y: (start?.y ?? 0) - 1 - implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2 + implicitWidth: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 + 2 implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full scale: 0 Component.onCompleted: scale = 1 Behavior on scale { Anim { - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index cef8a9906..741d43401 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -7,7 +7,7 @@ import Quickshell.Hyprland import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -31,7 +31,7 @@ Item { Rectangle { anchors.fill: parent - radius: Appearance.rounding.full + radius: Tokens.rounding.full gradient: Gradient { orientation: Gradient.Vertical @@ -60,7 +60,7 @@ Item { anchors.left: parent.left anchors.right: parent.right - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitHeight: parent.height / 2 opacity: view.contentY > 0 ? 0 : 1 @@ -74,9 +74,9 @@ Item { anchors.left: parent.left anchors.right: parent.right - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitHeight: parent.height / 2 - opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1 + opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.small ? 0 : 1 Behavior on opacity { Anim {} @@ -88,7 +88,7 @@ Item { id: view anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal interactive: false currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) @@ -119,7 +119,7 @@ Item { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } } @@ -127,12 +127,12 @@ Item { Anim { property: "scale" to: 0.5 - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } Anim { property: "opacity" to: 0 - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } @@ -140,7 +140,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { properties: "x,y" @@ -151,7 +151,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { properties: "x,y" @@ -175,7 +175,7 @@ Item { implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 color: Colours.palette.m3tertiary - radius: Appearance.rounding.full + radius: Tokens.rounding.full Colouriser { source: view @@ -192,13 +192,13 @@ Item { Behavior on y { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } Behavior on implicitHeight { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } } @@ -213,7 +213,7 @@ Item { drag.target: view.contentItem drag.axis: Drag.YAxis drag.maximumY: 0 - drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) + drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.small) onPressed: event => startY = event.y @@ -233,7 +233,7 @@ Item { id: ws required property HyprlandWorkspace modelData - readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.small : 0) property int wsId property string icon property bool hasWindows @@ -284,7 +284,7 @@ Item { asynchronous: true Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + Layout.preferredHeight: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 sourceComponent: ws.icon.length === 1 ? letterComp : iconComp @@ -328,7 +328,7 @@ Item { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } } @@ -336,7 +336,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { properties: "x,y" diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index bfbdc2ac1..af0370513 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { @@ -18,7 +18,7 @@ ColumnLayout { readonly property bool isWorkspace: true // Flag for finding workspace children // Unanimated prop for others to use as reference - readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0) + readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.small : 0) readonly property int ws: groupOffset + index + 1 readonly property bool isOccupied: occupied[ws] ?? false @@ -33,7 +33,7 @@ ColumnLayout { id: indicator Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + Layout.preferredHeight: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 animate: true text: { @@ -74,7 +74,7 @@ ColumnLayout { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } } @@ -82,7 +82,7 @@ ColumnLayout { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { properties: "x,y" diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index bbbfdfbcc..a0ccb7759 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -6,7 +6,7 @@ import QtQuick.Layouts import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config StyledClippingRect { id: root @@ -28,10 +28,10 @@ StyledClippingRect { property real blur: onSpecial ? 1 : 0 implicitWidth: Config.bar.sizes.innerWidth - implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.small * 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full Item { anchors.fill: parent @@ -51,7 +51,7 @@ StyledClippingRect { active: Config.bar.workspaces.occupiedBg anchors.fill: parent - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small sourceComponent: OccupiedBg { workspaces: workspaces @@ -64,7 +64,7 @@ StyledClippingRect { id: layout anchors.centerIn: parent - spacing: Math.floor(Appearance.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.small / 2) Repeater { id: workspaces @@ -118,7 +118,7 @@ StyledClippingRect { asynchronous: true anchors.fill: parent - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small active: opacity > 0 @@ -140,7 +140,7 @@ StyledClippingRect { Behavior on blur { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 7e4bd60a0..a37d81f91 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -4,7 +4,7 @@ import Quickshell.Wayland import Quickshell.Widgets import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -12,21 +12,21 @@ Item { required property PopoutState popouts - implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 + implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.large * 2 implicitHeight: child.implicitHeight Column { id: child anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { id: detailsRow anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IconImage { id: icon @@ -46,7 +46,7 @@ Item { StyledText { Layout.fillWidth: true text: Hypr.activeToplevel?.title ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } @@ -59,8 +59,8 @@ Item { } Item { - implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2 - implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: expandIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: expandIcon.implicitHeight + Tokens.padding.small * 2 Layout.alignment: Qt.AlignVCenter @@ -69,7 +69,7 @@ Item { root.popouts.detachRequested("winfo"); } - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } MaterialIcon { @@ -80,14 +80,14 @@ Item { text: "chevron_right" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } ClippingWrapperRectangle { color: "transparent" - radius: Appearance.rounding.small + radius: Tokens.rounding.small ScreencopyView { id: preview diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 52ac3b806..50bf1915d 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -7,15 +7,15 @@ import Quickshell.Services.Pipewire import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config Item { id: root required property PopoutState popouts - implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.normal * 2 ButtonGroup { id: sinks @@ -30,7 +30,7 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Output device") @@ -53,7 +53,7 @@ Item { } StyledText { - Layout.topMargin: Appearance.spacing.smaller + Layout.topMargin: Tokens.spacing.smaller text: qsTr("Input device") font.weight: 500 } @@ -72,15 +72,15 @@ Item { } StyledText { - Layout.topMargin: Appearance.spacing.smaller - Layout.bottomMargin: -Appearance.spacing.small / 2 + Layout.topMargin: Tokens.spacing.smaller + Layout.bottomMargin: -Tokens.spacing.small / 2 text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) font.weight: 500 } CustomMouseArea { Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 onWheel: event => { if (event.angleDelta.y > 0) @@ -105,10 +105,10 @@ Item { IconTextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Appearance.padding.small + verticalPadding: Tokens.padding.small text: qsTr("Open settings") icon: "settings" diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 86d903d24..2a11071ec 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -4,12 +4,12 @@ import QtQuick import Quickshell.Services.UPower import qs.components import qs.services -import qs.config +import Caelestia.Config Column { id: root - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal width: Config.bar.sizes.batteryWidth StyledText { @@ -45,11 +45,11 @@ Column { height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { - implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: child.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: child.implicitHeight + Tokens.padding.smaller * 2 color: Colours.palette.m3error - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Column { id: child @@ -58,7 +58,7 @@ Column { Row { anchors.horizontalCenter: parent.horizontalCenter - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { anchors.verticalCenter: parent.verticalCenter @@ -72,7 +72,7 @@ Column { anchors.verticalCenter: parent.verticalCenter text: qsTr("Performance Degraded") color: Colours.palette.m3onError - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono font.weight: 500 } @@ -109,17 +109,17 @@ Column { anchors.horizontalCenter: parent.horizontalCenter - implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2 - implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2 + implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.normal * 2 + Tokens.spacing.large * 2 + implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small * 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full StyledRect { id: indicator color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full state: profiles.current states: [ @@ -148,9 +148,9 @@ Column { transitions: Transition { AnchorAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } } @@ -160,7 +160,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Appearance.padding.small + anchors.leftMargin: Tokens.padding.small profile: PowerProfile.PowerSaver icon: "energy_savings_leaf" @@ -180,7 +180,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Appearance.padding.small + anchors.rightMargin: Tokens.padding.small profile: PowerProfile.Performance icon: "rocket_launch" @@ -201,15 +201,15 @@ Column { required property string icon required property int profile - implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 StateLayer { function onClicked(): void { PowerProfiles.profile = parent.profile; } - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } @@ -219,7 +219,7 @@ Column { anchors.centerIn: parent text: parent.icon - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface fill: profiles.current === text ? 1 : 0 diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 027b3d41a..d5cc0f619 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -7,7 +7,7 @@ import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { @@ -16,11 +16,11 @@ ColumnLayout { required property PopoutState popouts width: 300 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Tokens.padding.normal + Layout.rightMargin: Tokens.padding.small text: qsTr("Bluetooth") font.weight: 500 } @@ -46,8 +46,8 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.small - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Tokens.spacing.small + Layout.rightMargin: Tokens.padding.small text: { const devices = Bluetooth.devices.values; // qmllint disable unresolved-type let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); @@ -57,7 +57,7 @@ ColumnLayout { return available; } color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Repeater { @@ -72,8 +72,8 @@ ColumnLayout { readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.small opacity: 0 scale: 0.7 @@ -96,8 +96,8 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 Layout.fillWidth: true text: device.modelData.name elide: Text.ElideRight @@ -113,9 +113,9 @@ ColumnLayout { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.small - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type CircularIndicator { @@ -161,7 +161,7 @@ ColumnLayout { device.modelData.forget(); } - radius: Appearance.rounding.full + radius: Tokens.rounding.full } MaterialIcon { @@ -175,10 +175,10 @@ ColumnLayout { IconTextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Appearance.padding.small + verticalPadding: Tokens.padding.small text: qsTr("Open settings") icon: "settings" @@ -191,8 +191,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.normal + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index bf2a00493..810ac2e56 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.components -import qs.config +import Caelestia.Config import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others Item { @@ -35,8 +35,8 @@ Item { Behavior on offsetScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 71a8c5ebf..381d3ef0d 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -5,7 +5,7 @@ import QtQuick import Quickshell import Quickshell.Services.SystemTray import qs.components -import qs.config +import Caelestia.Config Item { id: root @@ -14,14 +14,14 @@ Item { readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null - implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2 - implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2 + implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.large * 2 + implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.large * 2 Item { id: content anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large Popout { name: "activewindow" @@ -194,7 +194,7 @@ Item { SequentialAnimation { Anim { properties: "opacity,scale" - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } PropertyAction { target: popout diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml index 9b61e0372..4d646a0cd 100644 --- a/modules/bar/popouts/LockStatus.qml +++ b/modules/bar/popouts/LockStatus.qml @@ -1,10 +1,10 @@ import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Capslock: %1").arg(Hypr.capsLock ? "Enabled" : "Disabled") diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 53350ee73..6bcf66041 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -6,7 +6,7 @@ import Quickshell import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { @@ -19,15 +19,15 @@ ColumnLayout { property var passwordNetwork: null property bool showPasswordDialog: false - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small width: Config.bar.sizes.networkWidth // Wireless section StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.padding.normal : 0 - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: visible ? Tokens.padding.normal : 0 + Layout.rightMargin: Tokens.padding.small text: qsTr("Wireless") font.weight: 500 } @@ -43,11 +43,11 @@ ColumnLayout { StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.spacing.small : 0 - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: visible ? Tokens.spacing.small : 0 + Layout.rightMargin: Tokens.padding.small text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Repeater { @@ -70,8 +70,8 @@ ColumnLayout { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.small opacity: 0 scale: 0.7 @@ -97,12 +97,12 @@ ColumnLayout { MaterialIcon { visible: networkItem.modelData.isSecure text: "lock" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 Layout.fillWidth: true text: networkItem.modelData.ssid elide: Text.ElideRight @@ -112,9 +112,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small + implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.small - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) CircularIndicator { @@ -165,11 +165,11 @@ ColumnLayout { StyledRect { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.spacing.small : 0 + Layout.topMargin: visible ? Tokens.spacing.small : 0 Layout.fillWidth: true - implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2 + implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer StateLayer { @@ -185,7 +185,7 @@ ColumnLayout { id: rescanBtn anchors.centerIn: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small opacity: Nmcli.scanning ? 0 : 1 MaterialIcon { @@ -210,9 +210,9 @@ ColumnLayout { CircularIndicator { anchors.centerIn: parent - strokeWidth: Appearance.padding.small / 2 + strokeWidth: Tokens.padding.small / 2 bgColour: "transparent" - implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2 + implicitSize: parent.implicitHeight - Tokens.padding.smaller * 2 running: Nmcli.scanning } } @@ -221,8 +221,8 @@ ColumnLayout { StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.padding.normal : 0 - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: visible ? Tokens.padding.normal : 0 + Layout.rightMargin: Tokens.padding.small text: qsTr("Ethernet") font.weight: 500 } @@ -230,11 +230,11 @@ ColumnLayout { StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.spacing.small : 0 - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: visible ? Tokens.spacing.small : 0 + Layout.rightMargin: Tokens.padding.small text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Repeater { @@ -256,8 +256,8 @@ ColumnLayout { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.small opacity: 0 scale: 0.7 @@ -281,8 +281,8 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 Layout.fillWidth: true text: ethernetItem.modelData.interface || qsTr("Unknown") elide: Text.ElideRight @@ -292,9 +292,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.small - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) CircularIndicator { @@ -374,8 +374,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.normal + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 4347e39e1..f39d64b07 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -6,7 +6,7 @@ import Quickshell import Quickshell.Widgets import qs.components import qs.services -import qs.config +import Caelestia.Config StackView { id: root @@ -45,8 +45,8 @@ StackView { property bool isSubMenu property bool shown - padding: Appearance.padding.smaller - spacing: Appearance.spacing.small + padding: Tokens.padding.smaller + spacing: Tokens.spacing.small opacity: shown ? 1 : 0 scale: shown ? 1 : 0.8 @@ -81,7 +81,7 @@ StackView { implicitWidth: Config.bar.sizes.trayMenuWidth implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent" Loader { @@ -110,9 +110,9 @@ StackView { } } - anchors.margins: -Appearance.padding.small / 2 - anchors.leftMargin: -Appearance.padding.smaller - anchors.rightMargin: -Appearance.padding.smaller + anchors.margins: -Tokens.padding.small / 2 + anchors.leftMargin: -Tokens.padding.smaller + anchors.rightMargin: -Tokens.padding.smaller radius: item.radius disabled: !item.modelData.enabled @@ -138,7 +138,7 @@ StackView { id: label anchors.left: icon.right - anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0 + anchors.leftMargin: icon.active ? Tokens.spacing.smaller : 0 text: labelMetrics.elidedText color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -152,7 +152,7 @@ StackView { font.family: label.font.family elide: Text.ElideRight - elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0) + elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Tokens.spacing.normal : 0) } Loader { @@ -180,7 +180,7 @@ StackView { sourceComponent: Item { implicitWidth: back.implicitWidth - implicitHeight: back.implicitHeight + Appearance.spacing.small / 2 + implicitHeight: back.implicitHeight + Tokens.spacing.small / 2 Item { anchors.bottom: parent.bottom @@ -189,11 +189,11 @@ StackView { StyledRect { anchors.fill: parent - anchors.margins: -Appearance.padding.small / 2 - anchors.leftMargin: -Appearance.padding.smaller - anchors.rightMargin: -Appearance.padding.smaller * 2 + anchors.margins: -Tokens.padding.small / 2 + anchors.leftMargin: -Tokens.padding.smaller + anchors.rightMargin: -Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3secondaryContainer StateLayer { diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 8858273d7..153170695 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -6,7 +6,7 @@ import Quickshell import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { @@ -69,9 +69,9 @@ ColumnLayout { } } - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + implicitHeight: content.implicitHeight + Tokens.padding.large * 2 visible: shouldBeVisible || isClosing enabled: shouldBeVisible && !isClosing focus: enabled @@ -128,8 +128,8 @@ ColumnLayout { StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: content.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 @@ -170,20 +170,20 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -201,7 +201,7 @@ ColumnLayout { return qsTr("Network: Unknown"); } color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Timer { @@ -237,7 +237,7 @@ ColumnLayout { id: statusText Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small + Layout.topMargin: Tokens.spacing.small visible: connectButton.connecting || connectButton.hasError text: { if (connectButton.hasError) { @@ -249,10 +249,10 @@ ColumnLayout { return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: 400 wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + Layout.maximumWidth: parent.width - Tokens.padding.large * 2 } FocusScope { @@ -261,9 +261,9 @@ ColumnLayout { property string passwordBuffer: "" objectName: "passwordContainer" - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) focus: true activeFocusOnTab: true @@ -336,7 +336,7 @@ ColumnLayout { StyledRect { anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) border.color: { @@ -369,7 +369,7 @@ ColumnLayout { hoverEnabled: false cursorShape: Qt.IBeamCursor - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } StyledText { @@ -378,8 +378,8 @@ ColumnLayout { anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { @@ -394,10 +394,10 @@ ColumnLayout { anchors.centerIn: parent implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal + implicitHeight: Tokens.font.size.normal orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false model: ScriptModel { @@ -411,7 +411,7 @@ ColumnLayout { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 + radius: Tokens.rounding.small / 2 opacity: 0 scale: 0 @@ -454,8 +454,8 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } @@ -467,15 +467,15 @@ ColumnLayout { } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { id: cancelButton Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -490,7 +490,7 @@ ColumnLayout { property bool hasError: false Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 04212e28f..fbf5075fa 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -6,7 +6,7 @@ import Quickshell.Hyprland import Quickshell.Wayland import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.modules.controlcenter import qs.modules.windowinfo @@ -32,13 +32,13 @@ Item { property string detachedMode property string queuedMode - property int animLength: Appearance.anim.durations.expressiveDefaultSpatial - property list animCurve: Appearance.anim.curves.expressiveDefaultSpatial + property int animLength: Tokens.anim.durations.expressiveDefaultSpatial + property list animCurve: Tokens.anim.curves.expressiveDefaultSpatial function setAnims(detach: bool): void { const type = `expressive${detach ? "Slow" : "Default"}Spatial`; - animLength = Appearance.anim.durations[type]; - animCurve = Appearance.anim.curves[type]; + animLength = Tokens.anim.durations[type]; + animCurve = Tokens.anim.curves[type]; } function detach(mode: string): void { diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 3e5819027..5767de46b 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -5,7 +5,7 @@ import QtQuick.Controls import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -14,7 +14,7 @@ ColumnLayout { kb.refresh(); } - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small width: Config.bar.sizes.kbLayoutWidth Component.onCompleted: kb.start() @@ -24,8 +24,8 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Tokens.padding.normal + Layout.rightMargin: Tokens.padding.small text: qsTr("Keyboard Layouts") font.weight: 500 } @@ -36,14 +36,14 @@ ColumnLayout { model: kb.visibleModel Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - Layout.topMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.spacing.small clip: true interactive: true implicitHeight: Math.min(contentHeight, 320) visible: kb.visibleModel.count > 0 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small add: Transition { NumberAnimation { @@ -88,7 +88,7 @@ ColumnLayout { readonly property bool isDisabled: layoutIndex > 3 width: list.width - height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) + height: Math.max(36, rowText.implicitHeight + Tokens.padding.small * 2) ToolTip.visible: isDisabled && layer.containsMouse ToolTip.text: "XKB limitation: maximum 4 layouts allowed" @@ -104,7 +104,7 @@ ColumnLayout { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height - 4 - radius: Appearance.rounding.full + radius: Tokens.rounding.full enabled: !kbDelegate.isDisabled } @@ -114,8 +114,8 @@ ColumnLayout { anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right - anchors.leftMargin: Appearance.padding.small - anchors.rightMargin: Appearance.padding.small + anchors.leftMargin: Tokens.padding.small + anchors.rightMargin: Tokens.padding.small text: kbDelegate.label elide: Text.ElideRight opacity: kbDelegate.isDisabled ? 0.4 : 1.0 @@ -126,8 +126,8 @@ ColumnLayout { Rectangle { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - Layout.topMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.spacing.small implicitHeight: 1 color: Colours.palette.m3onSurfaceVariant @@ -139,9 +139,9 @@ ColumnLayout { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - Layout.topMargin: Appearance.spacing.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.spacing.small + spacing: Tokens.spacing.small opacity: 1 scale: 1 diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 4caebbd29..b6535847a 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell.Io import Caelestia -import qs.config +import Caelestia.Config // TODO: handle this better later diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 5b067fc0d..9b1f01eaa 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -6,13 +6,13 @@ import Quickshell import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config Item { id: root required property ShellScreen screen - readonly property int rounding: floating ? 0 : Appearance.rounding.normal + readonly property int rounding: floating ? 0 : Tokens.rounding.normal property alias floating: session.floating property alias active: session.active diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 037ea0d64..706d9d98a 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.modules.controlcenter Item { @@ -15,23 +15,23 @@ Item { required property Session session required property bool initialOpeningComplete - implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.larger * 4 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 ColumnLayout { id: layout anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.larger * 2 - spacing: Appearance.spacing.normal + anchors.leftMargin: Tokens.padding.larger * 2 + spacing: Tokens.spacing.normal states: State { name: "expanded" when: root.session.navExpanded PropertyChanges { - layout.spacing: Appearance.spacing.small + layout.spacing: Tokens.spacing.small } } @@ -42,7 +42,7 @@ Item { } Loader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large asynchronous: true active: !root.session.floating visible: active @@ -51,10 +51,10 @@ Item { readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 implicitWidth: nonAnimWidth - implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth + implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Tokens.padding.normal * 2 : nonAnimWidth color: Colours.palette.m3primaryContainer - radius: Appearance.rounding.small + radius: Tokens.rounding.small StateLayer { id: normalWinState @@ -75,11 +75,11 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large + anchors.leftMargin: Tokens.padding.large text: "select_window" color: Colours.palette.m3onPrimaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: 1 } @@ -88,7 +88,7 @@ Item { anchors.left: normalWinIcon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal text: qsTr("Float window") color: Colours.palette.m3onPrimaryContainer @@ -96,22 +96,22 @@ Item { Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } Behavior on implicitWidth { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } @@ -123,7 +123,7 @@ Item { NavItem { required property int index - Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 + Layout.topMargin: index === 0 ? Tokens.spacing.large * 2 : 0 icon: PaneRegistry.getByIndex(index).icon label: PaneRegistry.getByIndex(index).label } @@ -148,7 +148,7 @@ Item { expandedLabel.opacity: 1 smallLabel.opacity: 0 background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth - background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + background.implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 item.implicitHeight: background.implicitHeight } } @@ -156,24 +156,24 @@ Item { transitions: Transition { Anim { property: "opacity" - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } Anim { properties: "implicitWidth,implicitHeight" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } StyledRect { id: background - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 - implicitHeight: icon.implicitHeight + Appearance.padding.small + implicitHeight: icon.implicitHeight + Tokens.padding.small StateLayer { function onClicked(): void { @@ -192,11 +192,11 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large + anchors.leftMargin: Tokens.padding.large text: item.icon color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: item.active ? 1 : 0 Behavior on fill { @@ -209,7 +209,7 @@ Item { anchors.left: icon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal opacity: 0 text: item.label @@ -222,10 +222,10 @@ Item { anchors.horizontalCenter: icon.horizontalCenter anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small / 2 + anchors.topMargin: Tokens.spacing.small / 2 text: item.label - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.capitalization: Font.Capitalize } } diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 6a5364100..c87185283 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -13,7 +13,7 @@ import QtQuick.Layouts import Quickshell.Widgets import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.modules.controlcenter ClippingRectangle { @@ -58,7 +58,7 @@ ClippingRectangle { Timer { id: animationDelayTimer - interval: Appearance.anim.durations.normal + interval: Tokens.anim.durations.normal onTriggered: { layout.animationComplete = true; } @@ -67,7 +67,7 @@ ClippingRectangle { Timer { id: initialOpeningTimer - interval: Appearance.anim.durations.large + interval: Tokens.anim.durations.large running: true onTriggered: { layout.initialOpeningComplete = true; diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index a55445c5a..1993446f8 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -2,7 +2,7 @@ import QtQuick import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -10,7 +10,7 @@ StyledRect { required property ShellScreen screen required property Session session - implicitHeight: text.implicitHeight + Appearance.padding.normal + implicitHeight: text.implicitHeight + Tokens.padding.normal color: Colours.tPalette.m3surfaceContainer StyledText { @@ -21,24 +21,24 @@ StyledRect { text: qsTr("Caelestia Settings - %1").arg(root.session.active) font.capitalization: Font.Capitalize - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } Item { anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal implicitWidth: implicitHeight - implicitHeight: closeIcon.implicitHeight + Appearance.padding.small + implicitHeight: closeIcon.implicitHeight + Tokens.padding.small StateLayer { function onClicked(): void { QsWindow.window.destroy(); } - radius: Appearance.rounding.full + radius: Tokens.rounding.full } MaterialIcon { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index c0ccfcd7a..472c654a8 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -15,7 +15,7 @@ import qs.components.controls import qs.components.effects import qs.components.images import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -111,9 +111,9 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: Appearance.spacing.normal + Layout.bottomMargin: Tokens.spacing.normal text: qsTr("Wallpaper") - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 600 } @@ -122,7 +122,7 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - Layout.bottomMargin: -Appearance.padding.large * 2 + Layout.bottomMargin: -Tokens.padding.large * 2 asynchronous: true active: { @@ -172,14 +172,14 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Appearance") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index e4d8a0333..e5e973e24 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { id: root @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 8b50c1242..95670d247 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { id: root @@ -37,9 +37,9 @@ CollapsibleSection { } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Desktop Clock") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -64,12 +64,12 @@ CollapsibleSection { rootPane.saveConfig(); } - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small z: 1 StyledText { text: qsTr("Positioning") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -156,11 +156,11 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small StyledText { text: qsTr("Shadow") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -174,7 +174,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -199,7 +199,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -225,11 +225,11 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small StyledText { text: qsTr("Background") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -252,7 +252,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -278,9 +278,9 @@ CollapsibleSection { } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Visualiser") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -303,7 +303,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -328,7 +328,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index e0c677cf7..e92a28da2 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { id: root @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -43,7 +43,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index b954cd458..69dff698f 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -9,7 +9,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { title: qsTr("Color scheme") @@ -18,7 +18,7 @@ CollapsibleSection { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 Repeater { model: Schemes.list @@ -32,10 +32,10 @@ CollapsibleSection { readonly property bool isCurrent: schemeKey === Schemes.currentScheme color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: schemeRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -65,9 +65,9 @@ CollapsibleSection { id: schemeRow anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { id: preview @@ -78,7 +78,7 @@ CollapsibleSection { border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitWidth: iconPlaceholder.implicitWidth implicitHeight: iconPlaceholder.implicitWidth @@ -87,7 +87,7 @@ CollapsibleSection { visible: false text: "circle" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } Item { @@ -105,7 +105,7 @@ CollapsibleSection { implicitWidth: preview.implicitWidth color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full } } } @@ -116,12 +116,12 @@ CollapsibleSection { StyledText { text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -137,7 +137,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index b3cc4cfba..8fbd80d9a 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -9,7 +9,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { title: qsTr("Color variant") @@ -18,7 +18,7 @@ CollapsibleSection { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 Repeater { model: M3Variants.list @@ -29,10 +29,10 @@ CollapsibleSection { Layout.fillWidth: true color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: variantRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -62,13 +62,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: modelData.icon - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.variant === Schemes.currentVariant ? 1 : 0 } @@ -82,7 +82,7 @@ CollapsibleSection { visible: modelData.variant === Schemes.currentVariant text: "check" color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 47b738f19..562ea8d5a 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { id: root @@ -38,7 +38,7 @@ CollapsibleSection { property alias contentHeight: sansFontList.contentHeight clip: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { @@ -52,10 +52,10 @@ CollapsibleSection { width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -70,13 +70,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: modelData - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -90,7 +90,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } @@ -119,7 +119,7 @@ CollapsibleSection { property alias contentHeight: monoFontList.contentHeight clip: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { @@ -133,10 +133,10 @@ CollapsibleSection { width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -151,13 +151,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: modelData - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -171,7 +171,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } @@ -202,7 +202,7 @@ CollapsibleSection { property alias contentHeight: materialFontList.contentHeight clip: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) StyledScrollBar.vertical: StyledScrollBar { @@ -216,10 +216,10 @@ CollapsibleSection { width: ListView.view.width color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -234,13 +234,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: modelData - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -254,7 +254,7 @@ CollapsibleSection { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } @@ -264,7 +264,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index 6d5d5b303..8113f28c7 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { id: root @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -43,7 +43,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -67,7 +67,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index c63c73aaf..8857da56e 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -6,7 +6,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { title: qsTr("Theme mode") diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 77582f9c6..2dc91b65c 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config CollapsibleSection { id: root @@ -28,7 +28,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -53,7 +53,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 172132f6b..13885656e 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -10,7 +10,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -38,15 +38,15 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Audio") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -64,15 +64,15 @@ Item { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Devices (%1)").arg(Audio.sinks.length) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } } @@ -93,8 +93,8 @@ Item { Layout.fillWidth: true color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Appearance.rounding.normal - implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 + radius: Tokens.rounding.normal + implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -108,13 +108,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: Audio.sink?.id === modelData.id ? 1 : 0 } @@ -141,15 +141,15 @@ Item { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Devices (%1)").arg(Audio.sources.length) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } } @@ -170,8 +170,8 @@ Item { Layout.fillWidth: true color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Appearance.rounding.normal - implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 + radius: Tokens.rounding.normal + implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -185,13 +185,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "mic" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: Audio.source?.id === modelData.id ? 1 : 0 } @@ -229,7 +229,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "volume_up" @@ -242,19 +242,19 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } @@ -306,15 +306,15 @@ Item { StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: Audio.muted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: muteIcon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { @@ -339,7 +339,7 @@ Item { id: outputVolumeSlider Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 value: Audio.volume enabled: !Audio.muted @@ -360,19 +360,19 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } @@ -424,15 +424,15 @@ Item { StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: Audio.sourceMuted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: muteInputIcon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { @@ -457,7 +457,7 @@ Item { id: inputVolumeSlider Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 value: Audio.sourceVolume enabled: !Audio.sourceMuted @@ -478,11 +478,11 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: Audio.streams @@ -493,15 +493,15 @@ Item { required property int index Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "apps" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal fill: 0 } @@ -510,7 +510,7 @@ Item { elide: Text.ElideRight maximumLineCount: 1 text: Audio.getStreamName(modelData) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } @@ -558,15 +558,15 @@ Item { StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: streamMuteIcon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { @@ -587,7 +587,7 @@ Item { StyledSlider { Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 value: Audio.getStreamVolume(modelData) enabled: !Audio.getStreamMuted(modelData) @@ -617,7 +617,7 @@ Item { visible: Audio.streams.length === 0 text: qsTr("No applications currently playing audio") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small horizontalAlignment: Text.AlignHCenter } } diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 97ea99c57..d57942541 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -9,7 +9,7 @@ import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.controls -import qs.config +import Caelestia.Config SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 9b347acdc..04b4ba01a 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -10,7 +10,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils StyledFlickable { @@ -54,12 +54,12 @@ StyledFlickable { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Connection status") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -70,9 +70,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 + implicitHeight: deviceStatus.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -81,9 +81,9 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger Toggle { label: qsTr("Connected") @@ -113,12 +113,12 @@ StyledFlickable { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Device properties") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -129,9 +129,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 + implicitHeight: deviceProps.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -140,19 +140,19 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Item { id: renameDevice Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.spacing.small implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight @@ -167,15 +167,15 @@ StyledFlickable { PropertyChanges { renameDevice.implicitHeight: deviceNameEdit.implicitHeight renameLabel.opacity: 0 - deviceNameEdit.padding: Appearance.padding.normal + deviceNameEdit.padding: Tokens.padding.normal } } transitions: Transition { AnchorAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } Anim { properties: "implicitHeight,opacity,padding" @@ -189,7 +189,7 @@ StyledFlickable { text: qsTr("Device name") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledTextField { @@ -198,7 +198,7 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal + anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.normal text: root.device?.name ?? "" readOnly: !root.session.bt.editingDeviceName @@ -207,11 +207,11 @@ StyledFlickable { root.device.name = text; } - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal + leftPadding: Tokens.padding.normal + rightPadding: Tokens.padding.normal background: StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingDeviceName ? 1 : 0 @@ -233,9 +233,9 @@ StyledFlickable { StyledRect { implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingDeviceName ? 1 : 0 scale: root.session.bt.editingDeviceName ? 1 : 0.5 @@ -265,17 +265,17 @@ StyledFlickable { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } StyledRect { implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: root.session.bt.editingDeviceName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { @@ -322,12 +322,12 @@ StyledFlickable { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Device information") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -338,9 +338,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 + implicitHeight: deviceInfo.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -349,9 +349,9 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") @@ -360,15 +360,15 @@ StyledFlickable { RowLayout { id: batteryPercent - Layout.topMargin: Appearance.spacing.small / 2 + Layout.topMargin: Tokens.spacing.small / 2 Layout.fillWidth: true - Layout.preferredHeight: Appearance.padding.smaller - spacing: Appearance.spacing.small / 2 + Layout.preferredHeight: Tokens.padding.smaller + spacing: Tokens.spacing.small / 2 StyledRect { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3secondaryContainer StyledRect { @@ -378,54 +378,54 @@ StyledFlickable { anchors.margins: parent.height * 0.25 implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary } } } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Dbus path") } StyledText { text: root.device?.dbusPath ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("MAC address") } StyledText { text: root.device?.address ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Bonded") } StyledText { text: root.device?.bonded ? qsTr("Yes") : qsTr("No") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("System name") } StyledText { text: root.device?.deviceName ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } } @@ -438,7 +438,7 @@ StyledFlickable { ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top - anchors.bottomMargin: Appearance.padding.normal + anchors.bottomMargin: Tokens.padding.normal Repeater { id: fabMenu @@ -470,9 +470,9 @@ StyledFlickable { Layout.alignment: Qt.AlignRight - implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2 + implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.larger * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer opacity: 0 @@ -482,7 +482,7 @@ StyledFlickable { when: root.session.bt.fabMenuOpen PropertyChanges { - fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2 + fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Tokens.padding.large * 2 fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } @@ -494,17 +494,17 @@ StyledFlickable { SequentialAnimation { PauseAnimation { - duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8 + duration: (fabMenu.count - 1 - fabMenuItem.index) * Tokens.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } Anim { property: "opacity" - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } @@ -514,17 +514,17 @@ StyledFlickable { SequentialAnimation { PauseAnimation { - duration: fabMenuItem.index * Appearance.anim.durations.small / 8 + duration: fabMenuItem.index * Tokens.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } Anim { property: "opacity" - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } @@ -549,7 +549,7 @@ StyledFlickable { id: fabMenuItemInner anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal opacity: 0 MaterialIcon { @@ -567,7 +567,7 @@ StyledFlickable { Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } @@ -594,7 +594,7 @@ StyledFlickable { implicitWidth: 64 implicitHeight: 64 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer states: State { @@ -605,15 +605,15 @@ StyledFlickable { fabBg.implicitWidth: 48 fabBg.implicitHeight: 48 fabBg.radius: 48 / 2 - fab.font.pointSize: Appearance.font.size.larger + fab.font.pointSize: Tokens.font.size.larger } } transitions: Transition { Anim { properties: "implicitWidth,implicitHeight" - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } Anim { properties: "radius,font.pointSize" @@ -644,7 +644,7 @@ StyledFlickable { animate: true text: root.session.bt.fabMenuOpen ? "close" : "settings" color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: 1 } } @@ -656,7 +656,7 @@ StyledFlickable { property alias toggle: toggle Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index b53829b93..ca76c8c1e 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -10,7 +10,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils DeviceList { @@ -32,11 +32,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Bluetooth") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -48,9 +48,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.enabled ?? false icon: "power" accent: "Tertiary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Toggle Bluetooth") onClicked: { @@ -64,9 +64,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.discoverable ?? false icon: root.smallDiscoverable ? "group_search" : "" label: root.smallDiscoverable ? "" : qsTr("Discoverable") - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Make discoverable") onClicked: { @@ -80,9 +80,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.pairable ?? false icon: "missing_controller" label: root.smallPairable ? "" : qsTr("Pairable") - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Make pairable") onClicked: { @@ -96,9 +96,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.discovering ?? false icon: "bluetooth_searching" accent: "Secondary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Scan for devices") onClicked: { @@ -112,9 +112,9 @@ DeviceList { toggled: !root.session.bt.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Bluetooth settings") onClicked: { @@ -137,10 +137,10 @@ DeviceList { readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected width: ListView.view ? ListView.view.width : undefined - implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: deviceInner.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { id: stateLayer @@ -155,15 +155,15 @@ DeviceList { id: deviceInner anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { @@ -178,7 +178,7 @@ DeviceList { anchors.centerIn: parent text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: device.connected ? 1 : 0 Behavior on fill { @@ -202,7 +202,7 @@ DeviceList { Layout.fillWidth: true text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } } @@ -211,9 +211,9 @@ DeviceList { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) CircularIndicator { diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index 6936c1ea2..bd09deebc 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -9,14 +9,14 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "bluetooth" @@ -24,9 +24,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Adapter status") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -37,9 +37,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2 + implicitHeight: adapterStatus.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -48,9 +48,9 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger Toggle { label: qsTr("Powered") @@ -85,9 +85,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Adapter properties") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -98,9 +98,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2 + implicitHeight: adapterSettings.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -109,13 +109,13 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -127,28 +127,28 @@ ColumnLayout { property bool expanded - implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: adapterPicker.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: adapterPicker.implicitHeight + Tokens.padding.smaller * 2 StateLayer { function onClicked(): void { adapterPickerButton.expanded = !adapterPickerButton.expanded; } - radius: Appearance.rounding.small + radius: Tokens.rounding.small } RowLayout { id: adapterPicker anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.topMargin: Appearance.padding.smaller - anchors.bottomMargin: Appearance.padding.smaller - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.normal + anchors.topMargin: Tokens.padding.smaller + anchors.bottomMargin: Tokens.padding.smaller + spacing: Tokens.spacing.normal StyledText { - Layout.leftMargin: Appearance.padding.small + Layout.leftMargin: Tokens.padding.small text: Bluetooth.defaultAdapter?.name ?? qsTr("None") } @@ -170,8 +170,8 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } @@ -185,7 +185,7 @@ ColumnLayout { implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight color: Colours.palette.m3secondaryContainer - radius: Appearance.rounding.small + radius: Tokens.rounding.small opacity: adapterPickerButton.expanded ? 1 : 0 scale: adapterPickerButton.expanded ? 1 : 0.7 @@ -207,7 +207,7 @@ ColumnLayout { required property BluetoothAdapter modelData Layout.fillWidth: true - implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: adapterInner.implicitHeight + Tokens.padding.normal * 2 StateLayer { function onClicked(): void { @@ -224,12 +224,12 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.small + Layout.leftMargin: Tokens.padding.small text: adapter.modelData.name color: Colours.palette.m3onSecondaryContainer } @@ -250,15 +250,15 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } @@ -267,7 +267,7 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -287,13 +287,13 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Item { id: renameAdapter Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.spacing.small implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight @@ -308,15 +308,15 @@ ColumnLayout { PropertyChanges { renameAdapter.implicitHeight: adapterNameEdit.implicitHeight renameLabel.opacity: 0 - adapterNameEdit.padding: Appearance.padding.normal + adapterNameEdit.padding: Tokens.padding.normal } } transitions: Transition { AnchorAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } Anim { properties: "implicitHeight,opacity,padding" @@ -330,7 +330,7 @@ ColumnLayout { text: qsTr("Rename adapter (currently does not work)") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledTextField { @@ -339,7 +339,7 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal + anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Tokens.padding.normal text: root.session.bt.currentAdapter?.name ?? "" readOnly: !root.session.bt.editingAdapterName @@ -347,11 +347,11 @@ ColumnLayout { root.session.bt.editingAdapterName = false; } - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal + leftPadding: Tokens.padding.normal + rightPadding: Tokens.padding.normal background: StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingAdapterName ? 1 : 0 @@ -373,9 +373,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingAdapterName ? 1 : 0 scale: root.session.bt.editingAdapterName ? 1 : 0.5 @@ -405,17 +405,17 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } StyledRect { implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: root.session.bt.editingAdapterName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { @@ -448,9 +448,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Adapter information") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -461,9 +461,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2 + implicitHeight: adapterInfo.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -472,9 +472,9 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: qsTr("Adapter state") @@ -483,29 +483,29 @@ ColumnLayout { StyledText { text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Dbus path") } StyledText { text: Bluetooth.defaultAdapter?.dbusPath ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Adapter id") } StyledText { text: Bluetooth.defaultAdapter?.adapterId ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } } @@ -516,7 +516,7 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 15b1896cb..e95fa094a 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -5,7 +5,7 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -16,8 +16,8 @@ StyledRect { property int rows: 1 // Number of rows Layout.fillWidth: true - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: true @@ -29,21 +29,21 @@ StyledRect { id: layout anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { visible: root.title !== "" text: root.title - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } GridLayout { id: buttonGrid Layout.alignment: Qt.AlignHCenter - rowSpacing: Appearance.spacing.small - columnSpacing: Appearance.spacing.small + rowSpacing: Tokens.spacing.small + columnSpacing: Tokens.spacing.small rows: root.rows columns: Math.ceil(root.options.length / root.rows) @@ -81,13 +81,13 @@ StyledRect { // Match utilities Toggles radius styling // Each button has full rounding (not connected) since they have spacing - radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal + radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal // Match utilities Toggles inactive color inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) // Adjust width similar to utilities toggles - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) onClicked: { if (modelData.onToggled && root.rootItem && modelData.propertyName) { @@ -98,15 +98,15 @@ StyledRect { Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } Behavior on radius { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index c150abd7d..5fc4d0ed9 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -7,7 +7,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.components.effects -import qs.config +import Caelestia.Config Item { id: root @@ -30,7 +30,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Loader { id: headerLoader diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 2134d8cfe..8a65fc0c9 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -27,7 +27,7 @@ ColumnLayout { signal itemSelected(var item) - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Loader { id: headerLoader @@ -41,13 +41,13 @@ ColumnLayout { RowLayout { Layout.fillWidth: true Layout.topMargin: root.headerComponent ? 0 : 0 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small visible: root.title !== "" || root.description !== "" StyledText { visible: root.title !== "" text: root.title - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -78,7 +78,7 @@ ColumnLayout { model: root.model delegate: root.delegate - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false clip: false } diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml index ac9c3371a..a025f7d08 100644 --- a/modules/controlcenter/components/PaneTransition.qml +++ b/modules/controlcenter/components/PaneTransition.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.config +import Caelestia.Config SequentialAnimation { id: root @@ -20,9 +20,9 @@ SequentialAnimation { property: "opacity" from: root.opacityFrom to: root.opacityTo - duration: Appearance.anim.durations.normal / 2 + duration: Tokens.anim.durations.normal / 2 easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardAccel + easing.bezierCurve: Tokens.anim.curves.standardAccel } NumberAnimation { @@ -30,9 +30,9 @@ SequentialAnimation { property: "scale" from: root.scaleFrom to: root.scaleTo - duration: Appearance.anim.durations.normal / 2 + duration: Tokens.anim.durations.normal / 2 easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardAccel + easing.bezierCurve: Tokens.anim.curves.standardAccel } } @@ -53,9 +53,9 @@ SequentialAnimation { property: "opacity" from: root.opacityTo to: root.opacityFrom - duration: Appearance.anim.durations.normal / 2 + duration: Tokens.anim.durations.normal / 2 easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } NumberAnimation { @@ -63,9 +63,9 @@ SequentialAnimation { property: "scale" from: root.scaleTo to: root.scaleFrom - duration: Appearance.anim.durations.normal / 2 + duration: Tokens.anim.durations.normal / 2 easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } } } diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index 105270451..e1bff1378 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -17,16 +17,16 @@ ColumnLayout { property string suffix: "" property bool readonly: false - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { visible: root.label !== "" text: root.label - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface } @@ -38,20 +38,20 @@ ColumnLayout { visible: root.readonly text: "lock" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface } } StyledRect { Layout.fillWidth: true - implicitHeight: Appearance.padding.normal - radius: Appearance.rounding.full + implicitHeight: Tokens.padding.normal + radius: Tokens.rounding.full color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) opacity: root.readonly ? 0.5 : 1.0 diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index 6d392f5ea..425a9e8ee 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import qs.components -import qs.config +import Caelestia.Config Item { id: root @@ -18,19 +18,19 @@ Item { id: column anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: root.icon - font.pointSize: Appearance.font.size.extraLarge * 3 + font.pointSize: Tokens.font.size.extraLarge * 3 font.bold: true } StyledText { Layout.alignment: Qt.AlignHCenter text: root.title - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.bold: true } } diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index df3acf41e..1c1f29bef 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -6,7 +6,7 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -52,7 +52,7 @@ ColumnLayout { return parseFloat(text); } - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Component.onCompleted: { // Set initialized flag after a brief delay to allow component to fully load @@ -71,12 +71,12 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { visible: root.label !== "" text: root.label - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -138,7 +138,7 @@ ColumnLayout { visible: root.suffix !== "" text: root.suffix color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } @@ -146,7 +146,7 @@ ColumnLayout { id: slider Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 from: root.from to: root.to diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 5bf5c41ad..cff20ec72 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell.Widgets import qs.components import qs.components.effects -import qs.config +import Caelestia.Config RowLayout { id: root @@ -32,9 +32,9 @@ RowLayout { id: leftClippingRect anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 + anchors.rightMargin: Tokens.padding.normal / 2 radius: leftBorder.innerRadius color: "transparent" @@ -43,9 +43,9 @@ RowLayout { id: leftLoader anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large + Tokens.padding.normal / 2 asynchronous: true sourceComponent: root.leftContent @@ -62,7 +62,7 @@ RowLayout { id: leftBorder leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + rightThickness: Tokens.padding.normal / 2 } } @@ -76,9 +76,9 @@ RowLayout { id: rightClippingRect anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 + anchors.rightMargin: Tokens.padding.normal / 2 radius: rightBorder.innerRadius color: "transparent" @@ -87,7 +87,7 @@ RowLayout { id: rightLoader anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Tokens.padding.large * 2 asynchronous: true sourceComponent: root.rightContent @@ -103,7 +103,7 @@ RowLayout { InnerBorder { id: rightBorder - leftThickness: Appearance.padding.normal / 2 + leftThickness: Tokens.padding.normal / 2 } } } diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index e8dcb269c..dcc472d9e 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -7,7 +7,7 @@ import Quickshell.Widgets import qs.components import qs.components.containers import qs.components.effects -import qs.config +import Caelestia.Config Item { id: root diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index 6a65c8e15..b85ec556a 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -8,18 +8,18 @@ import qs.components.controls import qs.components.effects import qs.components.images import qs.services -import qs.config +import Caelestia.Config GridView { id: root required property Session session - readonly property int minCellWidth: 200 + Appearance.spacing.normal + readonly property int minCellWidth: 200 + Tokens.spacing.normal readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) cellWidth: width / columnsCount - cellHeight: 140 + Appearance.spacing.normal + cellHeight: 140 + Tokens.spacing.normal model: Wallpapers.list @@ -33,8 +33,8 @@ GridView { required property var modelData required property int index readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent - readonly property real itemMargin: Appearance.spacing.normal / 2 - readonly property real itemRadius: Appearance.rounding.normal + readonly property real itemMargin: Tokens.spacing.normal / 2 + readonly property real itemRadius: Tokens.rounding.normal width: root.cellWidth height: root.cellHeight @@ -130,7 +130,7 @@ GridView { anchors.right: parent.right anchors.bottom: parent.bottom - implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 + implicitHeight: filenameText.implicitHeight + Tokens.padding.normal * 1.5 radius: 0 gradient: Gradient { @@ -190,12 +190,12 @@ GridView { MaterialIcon { anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small visible: isCurrent text: "check_circle" color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } @@ -205,12 +205,12 @@ GridView { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 - anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 - anchors.bottomMargin: Appearance.padding.normal + anchors.leftMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 + anchors.rightMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 + anchors.bottomMargin: Tokens.padding.normal text: modelData.name - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller font.weight: 500 color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface elide: Text.ElideMiddle diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index bd6b9d5f1..d2e95801e 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -11,7 +11,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -66,9 +66,9 @@ Item { id: dashboardClippingRect anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal + anchors.rightMargin: Tokens.padding.normal radius: dashboardBorder.innerRadius color: "transparent" @@ -77,9 +77,9 @@ Item { id: dashboardLoader anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large asynchronous: true sourceComponent: dashboardContentComponent @@ -90,7 +90,7 @@ Item { id: dashboardBorder leftThickness: 0 - rightThickness: Appearance.padding.normal + rightThickness: Tokens.padding.normal } Component { @@ -113,14 +113,14 @@ Item { anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Dashboard") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } } diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 61e83d3c7..6294a05d5 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config SectionContainer { id: root @@ -17,7 +17,7 @@ SectionContainer { StyledText { text: qsTr("General Settings") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } SwitchRow { @@ -40,7 +40,7 @@ SectionContainer { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SwitchRow { Layout.fillWidth: true diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index eebf5fd29..1a0b6482c 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -6,7 +6,7 @@ import Quickshell.Services.UPower import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config SectionContainer { id: root @@ -22,7 +22,7 @@ SectionContainer { StyledText { text: qsTr("Performance Resources") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } ConnectedButtonGroup { diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 677cf4182..51989c630 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -14,7 +14,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -150,14 +150,14 @@ Item { anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Launcher") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -169,9 +169,9 @@ Item { toggled: !root.session.launcher.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Launcher settings") onClicked: { @@ -187,9 +187,9 @@ Item { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } @@ -200,11 +200,11 @@ Item { StyledRect { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.bottomMargin: Appearance.spacing.small + Layout.topMargin: Tokens.spacing.normal + Layout.bottomMargin: Tokens.spacing.small color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) @@ -213,7 +213,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Tokens.padding.normal text: "search" color: Colours.palette.m3onSurfaceVariant @@ -224,11 +224,11 @@ Item { anchors.left: searchIcon.right anchors.right: clearIcon.left - anchors.leftMargin: Appearance.spacing.small - anchors.rightMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small + anchors.rightMargin: Tokens.spacing.small - topPadding: Appearance.padding.normal - bottomPadding: Appearance.padding.normal + topPadding: Tokens.padding.normal + bottomPadding: Tokens.padding.normal placeholderText: qsTr("Search applications...") @@ -242,7 +242,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Appearance.padding.normal + anchors.rightMargin: Tokens.padding.normal width: searchField.text ? implicitWidth : implicitWidth / 2 opacity: { @@ -270,13 +270,13 @@ Item { Behavior on width { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } @@ -297,7 +297,7 @@ Item { Layout.fillHeight: true model: root.filteredApps - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 clip: true StyledScrollBar.vertical: StyledScrollBar { @@ -313,7 +313,7 @@ Item { implicitHeight: 40 color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal opacity: 0 @@ -338,9 +338,9 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IconImage { asynchronous: true @@ -355,7 +355,7 @@ Item { StyledText { Layout.fillWidth: true text: modelData.name || modelData.entry?.name || qsTr("Unknown") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Loader { @@ -518,12 +518,12 @@ Item { readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.padding.large * 2 + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.padding.large * 2 visible: displayedApp === null icon: "apps" title: qsTr("Launcher Applications") @@ -531,23 +531,23 @@ Item { Item { Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.padding.large * 2 + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.padding.large * 2 visible: displayedApp !== null implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) - implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight + implicitHeight: appIconImage.implicitHeight + Tokens.spacing.normal + appTitleText.implicitHeight ColumnLayout { anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IconImage { id: appIconImage asynchronous: true Layout.alignment: Qt.AlignHCenter - implicitSize: Appearance.font.size.extraLarge * 3 * 2 + implicitSize: Tokens.font.size.extraLarge * 3 * 2 source: { const app = appDetailsLayout.displayedApp; if (!app) @@ -565,7 +565,7 @@ Item { Layout.alignment: Qt.AlignHCenter text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.bold: true } } @@ -574,9 +574,9 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true - Layout.topMargin: Appearance.spacing.large - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Tokens.spacing.large + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 StyledFlickable { id: detailsFlickable @@ -595,10 +595,10 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SwitchRow { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal visible: appDetailsLayout.displayedApp !== null label: qsTr("Mark as favourite") checked: root.favouriteChecked @@ -631,7 +631,7 @@ Item { } } SwitchRow { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal visible: appDetailsLayout.displayedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 95fc2fb07..1641570c8 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -8,14 +8,14 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "apps" @@ -23,7 +23,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("General") description: qsTr("General launcher settings") } @@ -67,13 +67,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Display") description: qsTr("Display and appearance settings") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Max shown items") @@ -94,13 +94,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Prefixes") description: qsTr("Command prefix settings") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Special prefix") @@ -115,7 +115,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Fuzzy search") description: qsTr("Fuzzy search settings") } @@ -168,13 +168,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Sizes") description: qsTr("Size settings for launcher items") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Item width") @@ -201,13 +201,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Hidden apps") description: qsTr("Applications hidden from launcher") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Total hidden") diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 9b78ccafc..e6128a317 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -9,7 +9,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config DeviceDetails { id: root @@ -43,7 +43,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection status") @@ -69,7 +69,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Device properties") @@ -77,7 +77,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Interface") @@ -100,7 +100,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection information") diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 4fcd5b92d..440482652 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config DeviceList { id: root @@ -23,11 +23,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Settings") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -39,9 +39,9 @@ DeviceList { toggled: !root.session.ethernet.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { if (root.session.ethernet.active) @@ -62,10 +62,10 @@ DeviceList { readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { id: stateLayer @@ -79,15 +79,15 @@ DeviceList { id: rowLayout anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { @@ -101,7 +101,7 @@ DeviceList { anchors.centerIn: parent text: "cable" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.connected ? 1 : 0 color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface @@ -124,13 +124,13 @@ DeviceList { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: modelData.connected ? 500 : 400 elide: Text.ElideRight } @@ -141,9 +141,9 @@ DeviceList { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 8fb833fde..a2699c789 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -6,7 +6,7 @@ import QtQuick import Quickshell.Widgets import qs.components import qs.components.containers -import qs.config +import Caelestia.Config SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 3d99c47c2..7a37316c2 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -8,14 +8,14 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "cable" @@ -23,9 +23,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Ethernet devices") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -36,9 +36,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 + implicitHeight: ethernetInfo.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -47,9 +47,9 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: qsTr("Total devices") @@ -58,18 +58,18 @@ ColumnLayout { StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Connected devices") } StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 15ac56d24..6a1bc6df0 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -10,14 +10,14 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "router" @@ -25,13 +25,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Ethernet") description: qsTr("Ethernet device information") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Total devices") @@ -46,7 +46,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Wireless") description: qsTr("WiFi network settings") } @@ -62,7 +62,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("VPN") description: qsTr("VPN provider settings") visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 @@ -88,8 +88,8 @@ ColumnLayout { TextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.topMargin: Tokens.spacing.normal + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 text: qsTr("⚙ Manage VPN Providers") inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer @@ -101,13 +101,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Current connection") description: qsTr("Active network connection information") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Network") @@ -141,20 +141,20 @@ ColumnLayout { parent: Overlay.overlay anchors.centerIn: parent - width: Math.min(600, parent.width - Appearance.padding.large * 2) - height: Math.min(700, parent.height - Appearance.padding.large * 2) + width: Math.min(600, parent.width - Tokens.padding.large * 2) + height: Math.min(700, parent.height - Tokens.padding.large * 2) modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: StyledRect { color: Colours.palette.m3surface - radius: Appearance.rounding.large + radius: Tokens.rounding.large } StyledFlickable { anchors.fill: parent - anchors.margins: Appearance.padding.large * 1.5 + anchors.margins: Tokens.padding.large * 1.5 flickableDirection: Flickable.VerticalFlick contentHeight: vpnSettingsContent.height clip: true diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 990c4a269..2829a1148 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -12,7 +12,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -43,15 +43,15 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Network") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -63,9 +63,9 @@ Item { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Toggle WiFi") onClicked: { @@ -77,9 +77,9 @@ Item { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Scan for networks") onClicked: { @@ -91,9 +91,9 @@ Item { toggled: !root.session.ethernet.active && !root.session.network.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Network settings") onClicked: { diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index b02f4c497..214e4eb34 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -10,7 +10,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils DeviceDetails { @@ -37,7 +37,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection status") @@ -92,12 +92,12 @@ DeviceDetails { RowLayout { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - spacing: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal + spacing: Tokens.spacing.normal TextButton { Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 visible: root.providerEnabled enabled: !VPN.connecting inactiveColour: Colours.palette.m3primaryContainer @@ -149,7 +149,7 @@ DeviceDetails { TextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" text: qsTr("Open Login Page") inactiveColour: Colours.palette.m3tertiaryContainer @@ -162,10 +162,10 @@ DeviceDetails { StyledText { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" text: qsTr("Click 'Connect' to generate authentication URL") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap @@ -175,7 +175,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Provider details") @@ -183,7 +183,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Provider") @@ -262,8 +262,8 @@ DeviceDetails { parent: Overlay.overlay anchors.centerIn: parent - width: Math.min(400, parent.width - Appearance.padding.large * 2) - padding: Appearance.padding.large * 1.5 + width: Math.min(400, parent.width - Tokens.padding.large * 2) + padding: Tokens.padding.large * 1.5 modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside @@ -276,15 +276,15 @@ DeviceDetails { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } Anim { property: "scale" from: 0.7 to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } @@ -293,15 +293,15 @@ DeviceDetails { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } Anim { property: "scale" from: 1 to: 0.7 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } @@ -311,7 +311,7 @@ DeviceDetails { background: StyledRect { color: Colours.palette.m3surfaceContainerHigh - radius: Appearance.rounding.large + radius: Tokens.rounding.large Elevation { anchors.fill: parent @@ -322,21 +322,21 @@ DeviceDetails { } contentItem: ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Edit VPN Provider") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Display Name") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -344,7 +344,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -359,7 +359,7 @@ DeviceDetails { id: displayNameField anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.displayName onTextChanged: editVpnDialog.displayName = text @@ -369,11 +369,11 @@ DeviceDetails { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -381,7 +381,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -396,7 +396,7 @@ DeviceDetails { id: interfaceNameField anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.interfaceName onTextChanged: editVpnDialog.interfaceName = text @@ -406,12 +406,12 @@ DeviceDetails { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 visible: editVpnDialog.connectCmd.length > 0 StyledText { text: qsTr("Connect Command") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -419,7 +419,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -434,7 +434,7 @@ DeviceDetails { id: connectCmdFieldEdit anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.connectCmd onTextChanged: editVpnDialog.connectCmd = text @@ -444,12 +444,12 @@ DeviceDetails { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 visible: editVpnDialog.disconnectCmd.length > 0 StyledText { text: qsTr("Disconnect Command") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -457,7 +457,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -472,7 +472,7 @@ DeviceDetails { id: disconnectCmdFieldEdit anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.disconnectCmd onTextChanged: editVpnDialog.disconnectCmd = text @@ -481,9 +481,9 @@ DeviceDetails { } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { Layout.fillWidth: true diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 8522f9729..5ca385cb8 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -9,7 +9,7 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -18,7 +18,7 @@ ColumnLayout { property bool showHeader: true property int pendingSwitchIndex: -1 - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Connections { function onConnectedChanged() { @@ -77,7 +77,7 @@ ColumnLayout { Layout.preferredHeight: contentHeight interactive: false - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller model: ScriptModel { values: Config.utilities.vpn.provider.map((provider, index) => { @@ -104,10 +104,10 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { function onClicked(): void { @@ -123,15 +123,15 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { @@ -139,7 +139,7 @@ ColumnLayout { anchors.centerIn: parent text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.enabled && VPN.connected ? 1 : 0 color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } @@ -160,7 +160,7 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true @@ -197,7 +197,7 @@ ColumnLayout { return Colours.palette.m3tertiary; return Colours.palette.m3onSurface; } - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: modelData.enabled && VPN.connected ? 500 : 400 elide: Text.ElideRight } @@ -206,9 +206,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) StateLayer { @@ -267,9 +267,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: deleteIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: "transparent" StateLayer { @@ -366,8 +366,8 @@ ColumnLayout { parent: Overlay.overlay x: Math.round((parent.width - width) / 2) y: Math.round((parent.height - height) / 2) - implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) - padding: Appearance.padding.large * 1.5 + implicitWidth: Math.min(400, parent.width - Tokens.padding.large * 2) + padding: Tokens.padding.large * 1.5 modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside @@ -381,15 +381,15 @@ ColumnLayout { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.normal + easing.bezierCurve: Tokens.anim.curves.emphasized } Anim { property: "scale" from: 0.7 to: 1 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.normal + easing.bezierCurve: Tokens.anim.curves.emphasized } } } @@ -400,15 +400,15 @@ ColumnLayout { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.emphasized } Anim { property: "scale" from: 1 to: 0.7 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.emphasized } } } @@ -423,7 +423,7 @@ ColumnLayout { background: StyledRect { color: Colours.palette.m3surfaceContainerHigh - radius: Appearance.rounding.large + radius: Tokens.rounding.large Elevation { anchors.fill: parent @@ -434,8 +434,8 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.normal + easing.bezierCurve: Tokens.anim.curves.emphasized } } } @@ -445,8 +445,8 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.normal + easing.bezierCurve: Tokens.anim.curves.emphasized } } @@ -454,20 +454,20 @@ ColumnLayout { id: selectionContent anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal visible: vpnDialog.currentState === "selection" opacity: vpnDialog.currentState === "selection" ? 1 : 0 Behavior on opacity { Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.emphasized } } StyledText { text: qsTr("Add VPN Provider") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -476,11 +476,11 @@ ColumnLayout { text: qsTr("Choose a provider to add") wrapMode: Text.WordWrap color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } TextButton { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true text: qsTr("NetBird") inactiveColour: Colours.tPalette.m3surfaceContainerHigh @@ -564,7 +564,7 @@ ColumnLayout { } TextButton { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true text: qsTr("Cancel") inactiveColour: Colours.palette.m3secondaryContainer @@ -577,30 +577,30 @@ ColumnLayout { id: formContent anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal visible: vpnDialog.currentState === "form" opacity: vpnDialog.currentState === "form" ? 1 : 0 Behavior on opacity { Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.emphasized } } StyledText { text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Display Name") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -608,7 +608,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -623,7 +623,7 @@ ColumnLayout { id: displayNameField anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.displayName onTextChanged: vpnDialog.displayName = text @@ -633,11 +633,11 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -645,7 +645,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -660,7 +660,7 @@ ColumnLayout { id: interfaceNameField anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.interfaceName onTextChanged: vpnDialog.interfaceName = text @@ -670,12 +670,12 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") StyledText { text: qsTr("Connect Command (e.g., wg-quick up wg0)") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -683,7 +683,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -698,7 +698,7 @@ ColumnLayout { id: connectCmdField anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.connectCmd onTextChanged: vpnDialog.connectCmd = text @@ -708,12 +708,12 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") StyledText { text: qsTr("Disconnect Command (e.g., wg-quick down wg0)") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -721,7 +721,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -736,7 +736,7 @@ ColumnLayout { id: disconnectCmdField anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.disconnectCmd onTextChanged: vpnDialog.disconnectCmd = text @@ -745,9 +745,9 @@ ColumnLayout { } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { Layout.fillWidth: true @@ -822,8 +822,8 @@ ColumnLayout { target: selectionContent property: "opacity" to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.emphasized } } @@ -838,8 +838,8 @@ ColumnLayout { target: formContent property: "opacity" to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.emphasized } } } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 80732fdeb..d8dd09d18 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -11,14 +11,14 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "vpn_key" @@ -26,7 +26,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("General") description: qsTr("VPN configuration") } @@ -43,20 +43,20 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Providers") description: qsTr("Manage VPN providers") } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ListView { Layout.fillWidth: true Layout.preferredHeight: contentHeight interactive: false - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller model: ScriptModel { values: Config.utilities.vpn.provider.map((provider, index) => { @@ -84,18 +84,18 @@ ColumnLayout { width: ListView.view ? ListView.view.width : undefined implicitHeight: 60 color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal MaterialIcon { text: modelData.isActive ? "vpn_key" : "vpn_key_off" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline } @@ -110,7 +110,7 @@ ColumnLayout { StyledText { text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface")) - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline } } @@ -186,7 +186,7 @@ ColumnLayout { TextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("+ Add Provider") inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer @@ -198,13 +198,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Quick Add") description: qsTr("Add common VPN providers") } SectionContainer { - contentSpacing: Appearance.spacing.smaller + contentSpacing: Tokens.spacing.smaller TextButton { Layout.fillWidth: true diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index 913e5d75a..b6d7e77e1 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -10,7 +10,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils DeviceDetails { @@ -64,7 +64,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection status") @@ -86,8 +86,8 @@ DeviceDetails { TextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.topMargin: Tokens.spacing.normal + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 visible: { if (!root.network || !root.network.ssid) { return false; @@ -112,7 +112,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Network properties") @@ -120,7 +120,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("SSID") @@ -155,7 +155,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection information") diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index bff142d3a..878986d1b 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -11,7 +11,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils DeviceList { @@ -34,7 +34,7 @@ DeviceList { visible: Nmcli.scanning text: qsTr("Scanning...") color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } @@ -48,11 +48,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Settings") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -64,9 +64,9 @@ DeviceList { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { Nmcli.toggleWifi(null); @@ -77,9 +77,9 @@ DeviceList { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { Nmcli.rescanWifi(); @@ -90,9 +90,9 @@ DeviceList { toggled: !root.session.network.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { if (root.session.network.active) @@ -110,10 +110,10 @@ DeviceList { required property var modelData width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { function onClicked(): void { @@ -130,15 +130,15 @@ DeviceList { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { @@ -146,7 +146,7 @@ DeviceList { anchors.centerIn: parent text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.active ? 1 : 0 color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } @@ -167,7 +167,7 @@ DeviceList { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true @@ -182,7 +182,7 @@ DeviceList { return qsTr("Open"); } color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: modelData.active ? 500 : 400 elide: Text.ElideRight } @@ -191,9 +191,9 @@ DeviceList { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) StateLayer { diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index cccf8222c..02af096db 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -6,7 +6,7 @@ import QtQuick import Quickshell.Widgets import qs.components import qs.components.containers -import qs.config +import Caelestia.Config SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 8700f1c43..14fb565a5 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -10,7 +10,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -99,9 +99,9 @@ Item { anchors.centerIn: parent implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + implicitHeight: content.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surface opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 @@ -142,20 +142,20 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -163,14 +163,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { id: statusText Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small + Layout.topMargin: Tokens.spacing.small visible: connectButton.connecting || connectButton.hasError text: { if (connectButton.hasError) { @@ -182,10 +182,10 @@ Item { return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: 400 wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + Layout.maximumWidth: parent.width - Tokens.padding.large * 2 } Item { @@ -193,9 +193,9 @@ Item { property string passwordBuffer: "" - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) focus: true Keys.onPressed: event => { if (!activeFocus) { @@ -261,7 +261,7 @@ Item { StyledRect { anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) border.color: { @@ -302,8 +302,8 @@ Item { anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { @@ -318,10 +318,10 @@ Item { anchors.centerIn: parent implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal + implicitHeight: Tokens.font.size.normal orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false model: ScriptModel { @@ -335,7 +335,7 @@ Item { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 + radius: Tokens.rounding.small / 2 opacity: 0 scale: 0 @@ -378,8 +378,8 @@ Item { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } @@ -391,15 +391,15 @@ Item { } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { id: cancelButton Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -414,7 +414,7 @@ Item { property bool hasError: false Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index 7b929bf98..bce381bae 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -8,14 +8,14 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "wifi" @@ -23,7 +23,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("WiFi status") description: qsTr("General WiFi settings") } @@ -39,13 +39,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Network information") description: qsTr("Current network connection") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Connected network") diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml index 8d58e9e82..5768e4789 100644 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -11,7 +11,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -66,9 +66,9 @@ Item { id: notificationsClippingRect anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal + anchors.rightMargin: Tokens.padding.normal color: "transparent" radius: notificationsBorder.innerRadius @@ -77,9 +77,9 @@ Item { id: notificationsLoader anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large sourceComponent: notificationsContentComponent } @@ -89,7 +89,7 @@ Item { id: notificationsBorder leftThickness: 0 - rightThickness: Appearance.padding.normal + rightThickness: Tokens.padding.normal } Component { @@ -111,13 +111,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal ColumnLayout { Layout.fillWidth: true Layout.maximumWidth: 500 Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionContainer { Layout.fillWidth: true @@ -125,7 +125,7 @@ Item { StyledText { text: qsTr("Notifications") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } SplitButtonRow { @@ -220,7 +220,7 @@ Item { ColumnLayout { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionContainer { Layout.fillWidth: true @@ -228,7 +228,7 @@ Item { StyledText { text: qsTr("Toast settings") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } SplitButtonRow { @@ -316,8 +316,8 @@ Item { GridLayout { Layout.fillWidth: true columns: 2 - columnSpacing: Appearance.spacing.normal - rowSpacing: Appearance.spacing.normal + columnSpacing: Tokens.spacing.normal + rowSpacing: Tokens.spacing.normal SwitchRow { Layout.fillWidth: true diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index d61e932dc..7ec577753 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -11,7 +11,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -126,9 +126,9 @@ Item { id: taskbarClippingRect anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal + anchors.rightMargin: Tokens.padding.normal radius: taskbarBorder.innerRadius color: "transparent" @@ -137,9 +137,9 @@ Item { id: taskbarLoader anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large asynchronous: true sourceComponent: taskbarContentComponent @@ -150,7 +150,7 @@ Item { id: taskbarBorder leftThickness: 0 - rightThickness: Appearance.padding.normal + rightThickness: Tokens.padding.normal } Component { @@ -173,14 +173,14 @@ Item { anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Taskbar") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } } @@ -191,7 +191,7 @@ Item { StyledText { text: qsTr("Status Icons") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } ConnectedButtonGroup { @@ -270,14 +270,14 @@ Item { id: mainRowLayout Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal ColumnLayout { id: leftColumnLayout Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionContainer { Layout.fillWidth: true @@ -285,13 +285,13 @@ Item { StyledText { text: qsTr("Workspaces") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledRect { Layout.fillWidth: true - implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: workspacesShownRow.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -304,8 +304,8 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -326,8 +326,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -340,8 +340,8 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -360,8 +360,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: workspacesOccupiedBgRow.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -374,8 +374,8 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -394,8 +394,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: workspacesShowWindowsRow.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -408,8 +408,8 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -428,8 +428,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -442,8 +442,8 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -464,8 +464,8 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: workspacesPerMonitorRow.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -478,8 +478,8 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -503,7 +503,7 @@ Item { StyledText { text: qsTr("Scroll Actions") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } ConnectedButtonGroup { @@ -544,7 +544,7 @@ Item { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionContainer { Layout.fillWidth: true @@ -552,7 +552,7 @@ Item { StyledText { text: qsTr("Clock") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } SwitchRow { @@ -589,7 +589,7 @@ Item { StyledText { text: qsTr("Bar Behavior") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } SwitchRow { @@ -611,7 +611,7 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -642,7 +642,7 @@ Item { StyledText { text: qsTr("Active window") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } SwitchRow { @@ -670,7 +670,7 @@ Item { Layout.fillWidth: true Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionContainer { Layout.fillWidth: true @@ -678,7 +678,7 @@ Item { StyledText { text: qsTr("Popouts") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } SwitchRow { @@ -715,7 +715,7 @@ Item { StyledText { text: qsTr("Tray Settings") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } ConnectedButtonGroup { @@ -756,7 +756,7 @@ Item { StyledText { text: qsTr("Monitors") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } ConnectedButtonGroup { diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 957389f86..a07aca412 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -6,7 +6,7 @@ import Quickshell import Quickshell.Widgets import qs.components import qs.components.filedialog -import qs.config +import Caelestia.Config Item { id: root @@ -66,8 +66,8 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Appearance.padding.normal - anchors.margins: Appearance.padding.large + anchors.topMargin: Tokens.padding.normal + anchors.margins: Tokens.padding.large nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 dashState: root.dashState @@ -81,9 +81,9 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: "transparent" Flickable { @@ -199,15 +199,15 @@ Item { Behavior on implicitWidth { Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.large + easing.bezierCurve: Tokens.anim.curves.emphasized } } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.large + easing.bezierCurve: Tokens.anim.curves.emphasized } } } diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 59620d88a..3de9fb6bd 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import qs.components import qs.components.filedialog import qs.services -import qs.config +import Caelestia.Config GridLayout { id: root @@ -12,8 +12,8 @@ GridLayout { required property DashboardState dashState required property FileDialog facePicker - rowSpacing: Appearance.spacing.normal - columnSpacing: Appearance.spacing.normal + rowSpacing: Tokens.spacing.normal + columnSpacing: Tokens.spacing.normal Rect { Layout.column: 2 @@ -21,7 +21,7 @@ GridLayout { Layout.preferredWidth: user.implicitWidth Layout.preferredHeight: user.implicitHeight - radius: Appearance.rounding.large + radius: Tokens.rounding.large User { id: user @@ -37,7 +37,7 @@ GridLayout { Layout.preferredWidth: Config.dashboard.sizes.weatherWidth Layout.fillHeight: true - radius: Appearance.rounding.large * 1.5 + radius: Tokens.rounding.large * 1.5 SmallWeather {} } @@ -47,7 +47,7 @@ GridLayout { Layout.preferredWidth: dateTime.implicitWidth Layout.fillHeight: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal DateTime { id: dateTime @@ -61,7 +61,7 @@ GridLayout { Layout.fillWidth: true Layout.preferredHeight: calendar.implicitHeight - radius: Appearance.rounding.large + radius: Tokens.rounding.large Calendar { id: calendar @@ -76,7 +76,7 @@ GridLayout { Layout.preferredWidth: resources.implicitWidth Layout.fillHeight: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Resources { id: resources @@ -90,7 +90,7 @@ GridLayout { Layout.preferredWidth: media.implicitWidth Layout.fillHeight: true - radius: Appearance.rounding.large * 2 + radius: Tokens.rounding.large * 2 Media { id: media diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index 54b43eecc..b311ff7c8 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -19,7 +19,7 @@ StyledRect { implicitHeight: contentHeight - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer Loader { @@ -29,31 +29,31 @@ StyledRect { sourceComponent: ColumnLayout { anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal // Header: icon, backend selector, refresh, toggle RowLayout { Layout.fillWidth: true - spacing: Appearance.padding.small + spacing: Tokens.padding.small MaterialIcon { text: "lyrics" fill: 1 color: Colours.palette.m3primary - font.pointSize: Appearance.spacing.large + font.pointSize: Tokens.spacing.large } Rectangle { Layout.preferredHeight: 24 Layout.preferredWidth: 80 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) StyledText { anchors.centerIn: parent text: LyricsService.preferredBackend - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3primary } @@ -73,14 +73,14 @@ StyledRect { Rectangle { Layout.preferredHeight: 24 Layout.preferredWidth: 60 - radius: Appearance.rounding.small + radius: Tokens.rounding.small visible: LyricsService.preferredBackend === "Auto" color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) StyledText { anchors.centerIn: parent text: LyricsService.backend - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary } } @@ -105,7 +105,7 @@ StyledRect { Layout.fillWidth: true text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 } @@ -115,12 +115,12 @@ StyledRect { Layout.fillWidth: true Layout.preferredHeight: 48 visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) ColumnLayout { anchors.fill: parent - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small spacing: 0 StyledText { @@ -130,7 +130,7 @@ StyledRect { const parts = path.split('/'); return parts[parts.length - 1]; } - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3tertiary elide: Text.ElideMiddle } @@ -145,7 +145,7 @@ StyledRect { } return ""; } - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideMiddle } @@ -164,7 +164,7 @@ StyledRect { model: LyricsService.candidatesModel clip: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small visible: LyricsService.candidatesModel.count > 0 opacity: visible ? 1 : 0 @@ -186,7 +186,7 @@ StyledRect { Behavior on scale { NumberAnimation { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small easing.type: Easing.OutCubic } } @@ -195,7 +195,7 @@ StyledRect { id: background anchors.fill: parent - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) @@ -204,12 +204,12 @@ StyledRect { Behavior on color { ColorAnimation { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } Behavior on border.width { NumberAnimation { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } @@ -228,8 +228,8 @@ StyledRect { Row { anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small // Active indicator bar Rectangle { @@ -241,7 +241,7 @@ StyledRect { Behavior on color { ColorAnimation { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } @@ -253,7 +253,7 @@ StyledRect { Text { text: delegateRoot.title - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.bold: true color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface width: parent.width @@ -261,14 +261,14 @@ StyledRect { Behavior on color { ColorAnimation { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } Text { text: delegateRoot.artist - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant elide: Text.ElideRight } @@ -286,19 +286,19 @@ StyledRect { // Manual search ColumnLayout { Layout.fillWidth: true - spacing: Appearance.padding.small + spacing: Tokens.padding.small StyledText { Layout.fillWidth: true text: "Manual Search" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant elide: Text.ElideRight } RowLayout { Layout.fillWidth: true - spacing: Appearance.padding.small + spacing: Tokens.padding.small StyledInputField { id: searchTitle @@ -336,18 +336,18 @@ StyledRect { // Offset controls RowLayout { Layout.fillWidth: true - spacing: Appearance.padding.small + spacing: Tokens.padding.small MaterialIcon { text: "contrast_square" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: Colours.palette.m3secondary } StyledText { text: "Offset" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -368,7 +368,7 @@ StyledRect { horizontalAlignment: TextInput.AlignHCenter color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal selectByMouse: true text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" onEditingFinished: { diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml index 285e92f8c..b0ef99a3a 100644 --- a/modules/dashboard/LyricsView.qml +++ b/modules/dashboard/LyricsView.qml @@ -4,7 +4,7 @@ import Quickshell import qs.components import qs.components.containers import qs.services -import qs.config +import Caelestia.Config StyledListView { id: root @@ -19,7 +19,7 @@ StyledListView { preferredHighlightEnd: height / 2 + 30 highlightRangeMode: ListView.ApplyRange highlightFollowsCurrentItem: true - highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Appearance.anim.durations.normal + highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Tokens.anim.durations.normal layer.enabled: true layer.effect: ShaderEffect { required property Item source @@ -46,7 +46,7 @@ StyledListView { property bool isCurrent: ListView.isCurrentItem width: ListView.view.width - height: hasContent ? (lyricText.contentHeight + Appearance.spacing.large) : 0 + height: hasContent ? (lyricText.contentHeight + Tokens.spacing.large) : 0 MultiEffect { id: effect @@ -84,19 +84,19 @@ StyledListView { anchors.centerIn: parent horizontalAlignment: Text.AlignHCenter wrapMode: Text.WordWrap - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant font.bold: delegateRoot.isCurrent scale: delegateRoot.isCurrent ? 1.15 : 1.0 Behavior on color { CAnim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } Behavior on scale { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 4c838a5f5..d8ed44b3e 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -9,7 +9,7 @@ import Caelestia.Services import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -18,7 +18,7 @@ Item { required property DrawerVisibilities visibilities readonly property bool needsKeyboard: lyricMenuOpen - readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 + readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight property bool lyricMenuOpen: false @@ -52,7 +52,7 @@ Item { } } - implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 + implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 implicitHeight: nonAnimHeight Behavior on implicitHeight { @@ -61,7 +61,7 @@ Item { Behavior on playerProgress { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } @@ -106,8 +106,8 @@ Item { readonly property real centerX: width / 2 readonly property real centerY: height / 2 - readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small - readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small + readonly property real innerX: cover.implicitWidth / 2 + Tokens.spacing.small + readonly property real innerY: cover.implicitHeight / 2 + Tokens.spacing.small property color colour: Colours.palette.m3primary anchors.fill: cover @@ -136,8 +136,8 @@ Item { readonly property real cos: Math.cos(angle) readonly property real sin: Math.sin(angle) - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4 + capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: 360 / Config.services.visualiserBars - Tokens.spacing.small / 4 strokeColor: Colours.palette.m3primary startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos @@ -159,7 +159,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize + anchors.leftMargin: Tokens.padding.large + Config.dashboard.sizes.mediaVisualiserSize implicitWidth: Config.dashboard.sizes.mediaCoverArtSize implicitHeight: Config.dashboard.sizes.mediaCoverArtSize @@ -201,9 +201,9 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: visualiser.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { id: title @@ -215,7 +215,7 @@ Item { horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } @@ -230,7 +230,7 @@ Item { visible: !!Players.active text: Players.active?.trackAlbum || qsTr("Unknown album") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } @@ -259,15 +259,15 @@ Item { id: controls Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small - Layout.bottomMargin: Appearance.spacing.smaller + Layout.topMargin: Tokens.spacing.small + Layout.bottomMargin: Tokens.spacing.smaller - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small PlayerControl { type: IconButton.Text icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" - font.pointSize: Math.round(Appearance.font.size.large) + font.pointSize: Math.round(Tokens.font.size.large) disabled: !Players.active?.shuffleSupported onClicked: Players.active.shuffle = !Players.active?.shuffle } @@ -275,7 +275,7 @@ Item { PlayerControl { type: IconButton.Text icon: "skip_previous" - font.pointSize: Math.round(Appearance.font.size.large * 1.5) + font.pointSize: Math.round(Tokens.font.size.large * 1.5) disabled: !Players.active?.canGoPrevious onClicked: Players.active?.previous() } @@ -284,9 +284,9 @@ Item { icon: Players.active?.isPlaying ? "pause" : "play_arrow" label.animate: true toggle: true - padding: Appearance.padding.small / 2 + padding: Tokens.padding.small / 2 checked: Players.active?.isPlaying ?? false - font.pointSize: Math.round(Appearance.font.size.large * 1.5) + font.pointSize: Math.round(Tokens.font.size.large * 1.5) disabled: !Players.active?.canTogglePlaying onClicked: Players.active?.togglePlaying() } @@ -294,7 +294,7 @@ Item { PlayerControl { type: IconButton.Text icon: "skip_next" - font.pointSize: Math.round(Appearance.font.size.large * 1.5) + font.pointSize: Math.round(Tokens.font.size.large * 1.5) disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } @@ -302,7 +302,7 @@ Item { PlayerControl { type: IconButton.Text icon: "lyrics" - font.pointSize: Math.round(Appearance.font.size.large) + font.pointSize: Math.round(Tokens.font.size.large) onClicked: root.lyricMenuOpen = !root.lyricMenuOpen } } @@ -312,7 +312,7 @@ Item { enabled: !!Players.active implicitWidth: 280 - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 onMoved: { const active = Players.active; @@ -356,7 +356,7 @@ Item { text: root.lengthStr(Players.active?.position ?? -1) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { @@ -366,7 +366,7 @@ Item { text: root.lengthStr(Players.active?.length ?? -1) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } } @@ -377,14 +377,14 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 anchors.left: details.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal visible: lyricMenu.height === 0 || opacity > 0 opacity: lyricMenu.height === 0 ? 1 : 0 Behavior on opacity { NumberAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.OutCubic } } @@ -402,7 +402,7 @@ Item { height: visualiser.height * 0.75 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + speed: Audio.beatTracker.bpm / Tokens.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit @@ -416,9 +416,9 @@ Item { anchors.top: parent.top anchors.left: details.right anchors.right: parent.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal - contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Appearance.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight + contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Tokens.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight visible: root.lyricMenuOpen || height > 0 height: root.lyricMenuOpen ? implicitHeight : 0 @@ -426,7 +426,7 @@ Item { Behavior on height { NumberAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.OutCubic } } @@ -437,14 +437,14 @@ Item { parent: !root.lyricsShowingDebounced ? details : leftSection Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small PlayerControl { type: IconButton.Text icon: "move_up" inactiveOnColour: Colours.palette.m3secondary - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large + padding: Tokens.padding.small + font.pointSize: Tokens.font.size.large disabled: !Players.active?.canRaise onClicked: { Players.active?.raise(); @@ -482,8 +482,8 @@ Item { type: IconButton.Text icon: "delete" inactiveOnColour: Colours.palette.m3error - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large + padding: Tokens.padding.small + font.pointSize: Tokens.font.size.large disabled: !Players.active?.canQuit onClicked: Players.active?.quit() } @@ -498,15 +498,15 @@ Item { } component PlayerControl: IconButton { - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) - radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2 - radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial - radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) + radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : implicitHeight / 2 + radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial + radiusAnim.easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 75198254f..13ab57a75 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -6,12 +6,12 @@ import Caelestia.Internal import qs.components import qs.components.misc import qs.services -import qs.config +import Caelestia.Config Item { id: root - readonly property int minWidth: 400 + 400 + Appearance.spacing.normal + 120 + Appearance.padding.large * 2 + readonly property int minWidth: 400 + 400 + Tokens.spacing.normal + 120 + Tokens.padding.large * 2 function displayTemp(temp: real): string { return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? "F" : "C"}`; @@ -26,32 +26,32 @@ Item { anchors.centerIn: parent width: 400 height: 350 - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) ColumnLayout { anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "tune" - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 color: Colours.palette.m3onSurfaceVariant } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No widgets enabled") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: Colours.palette.m3onSurface } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enable widgets in dashboard settings") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } } @@ -62,7 +62,7 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal visible: !placeholder.visible Ref { @@ -73,11 +73,11 @@ Item { id: mainColumn Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") HeroCard { @@ -115,7 +115,7 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork GaugeCard { @@ -166,7 +166,7 @@ Item { property real animatedPercentage: 0 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.large + radius: Tokens.rounding.large Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage @@ -181,13 +181,13 @@ Item { ColumnLayout { anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small // Header Section ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { text: { @@ -214,14 +214,14 @@ Item { return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; } - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: batteryTank.accentColor } StyledText { Layout.fillWidth: true text: qsTr("Battery") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurface } } @@ -238,7 +238,7 @@ Item { StyledText { Layout.alignment: Qt.AlignRight text: `${Math.round(batteryTank.percentage * 100)}%` - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: Font.Medium color: batteryTank.accentColor } @@ -263,7 +263,7 @@ Item { return `${min}m`; } - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller color: Colours.palette.m3onSurfaceVariant } } @@ -271,7 +271,7 @@ Item { Behavior on animatedPercentage { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -282,19 +282,19 @@ Item { property color accentColor: Colours.palette.m3primary Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { text: parent.icon fill: 1 color: parent.accentColor - font.pointSize: Appearance.spacing.large + font.pointSize: Tokens.spacing.large } StyledText { Layout.fillWidth: true text: parent.title - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } } @@ -308,7 +308,7 @@ Item { property real animatedValue: 0 color: bgColor - radius: Appearance.rounding.full + radius: Tokens.rounding.full Component.onCompleted: animatedValue = value onValueChanged: animatedValue = value @@ -318,12 +318,12 @@ Item { anchors.bottom: parent.bottom width: parent.width * progressBar.animatedValue color: progressBar.fgColor - radius: Appearance.rounding.full + radius: Tokens.rounding.full } Behavior on animatedValue { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -346,7 +346,7 @@ Item { property real animatedTemp: 0 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.large + radius: Tokens.rounding.large Component.onCompleted: { animatedUsage = usage; animatedTemp = tempProgress; @@ -365,10 +365,10 @@ Item { CardHeader { anchors.left: parent.left anchors.top: parent.top - anchors.leftMargin: Appearance.padding.large - anchors.topMargin: Math.round(Appearance.padding.large * 1.2) + anchors.leftMargin: Tokens.padding.large + anchors.topMargin: Math.round(Tokens.padding.large * 1.2) - width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Appearance.spacing.normal + width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Tokens.spacing.normal icon: heroCard.icon title: heroCard.title accentColor: heroCard.accentColor @@ -378,23 +378,23 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Math.round(Appearance.padding.large * 1.2) - anchors.bottomMargin: Math.round(Appearance.padding.large * 1.3) + anchors.margins: Math.round(Tokens.padding.large * 1.2) + anchors.bottomMargin: Math.round(Tokens.padding.large * 1.3) - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Row { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: heroCard.secondaryValue - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: Font.Medium } StyledText { text: heroCard.secondaryLabel - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant anchors.baseline: parent.children[0].baseline } @@ -414,7 +414,7 @@ Item { anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large anchors.rightMargin: 32 spacing: 0 @@ -423,14 +423,14 @@ Item { anchors.right: parent.right text: heroCard.mainLabel - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurfaceVariant } StyledText { anchors.right: parent.right text: heroCard.mainValue - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: Font.Medium color: heroCard.accentColor } @@ -438,13 +438,13 @@ Item { Behavior on animatedUsage { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } Behavior on animatedTemp { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -462,15 +462,15 @@ Item { property real animatedPercentage: 0 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.large + radius: Tokens.rounding.large clip: true Component.onCompleted: animatedPercentage = percentage onPercentageChanged: animatedPercentage = percentage ColumnLayout { anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.smaller + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.smaller CardHeader { icon: gaugeCard.icon @@ -496,7 +496,7 @@ Item { StyledText { anchors.centerIn: parent text: `${Math.round(gaugeCard.percentage * 100)}%` - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: Font.Medium color: gaugeCard.accentColor } @@ -505,14 +505,14 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter text: gaugeCard.subtitle - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller color: Colours.palette.m3onSurfaceVariant } } Behavior on animatedPercentage { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -529,7 +529,7 @@ Item { property color accentColor: Colours.palette.m3secondary color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.large + radius: Tokens.rounding.large clip: true Component.onCompleted: { diskCount = SystemUsage.disks.length; @@ -567,8 +567,8 @@ Item { ColumnLayout { anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.smaller + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.smaller CardHeader { icon: "hard_disk" @@ -585,7 +585,7 @@ Item { MaterialIcon { text: "unfold_more" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal visible: storageGaugeCard.diskCount > 1 opacity: 0.7 ToolTip.visible: hintHover.hovered @@ -616,7 +616,7 @@ Item { StyledText { anchors.centerIn: parent text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: Font.Medium color: storageGaugeCard.accentColor } @@ -632,14 +632,14 @@ Item { const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; } - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller color: Colours.palette.m3onSurfaceVariant } } Behavior on animatedPercentage { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -650,7 +650,7 @@ Item { property color accentColor: Colours.palette.m3primary color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.large + radius: Tokens.rounding.large clip: true Ref { @@ -659,8 +659,8 @@ Item { ColumnLayout { anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small CardHeader { icon: "swap_vert" @@ -710,7 +710,7 @@ Item { Behavior on smoothMax { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -719,7 +719,7 @@ Item { StyledText { anchors.centerIn: parent text: qsTr("Collecting data...") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant visible: NetworkUsage.downloadBuffer.count < 2 opacity: 0.6 @@ -729,17 +729,17 @@ Item { // Download row RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "download" color: Colours.palette.m3tertiary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: qsTr("Download") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -752,7 +752,7 @@ Item { const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; } - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: Font.Medium color: Colours.palette.m3tertiary } @@ -761,17 +761,17 @@ Item { // Upload row RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "upload" color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: qsTr("Upload") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -784,7 +784,7 @@ Item { const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; } - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: Font.Medium color: Colours.palette.m3secondary } @@ -793,17 +793,17 @@ Item { // Session totals RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "history" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: qsTr("Total") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -817,7 +817,7 @@ Item { const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; } - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 7d8ff051c..94556acac 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -7,7 +7,7 @@ import Quickshell.Widgets import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -77,7 +77,7 @@ Item { implicitHeight: parent.implicitHeight * 2 color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full } Behavior on x { @@ -163,16 +163,16 @@ Item { properties: "implicitWidth,implicitHeight" from: 0 to: rippleAnim.radius * 2 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.normal + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { target: ripple property: "opacity" to: 0 - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } } @@ -185,7 +185,7 @@ Item { implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2 color: "transparent" - radius: Appearance.rounding.small + radius: Tokens.rounding.small StyledRect { id: stateLayer @@ -203,7 +203,7 @@ Item { StyledRect { id: ripple - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface opacity: 0 @@ -223,7 +223,7 @@ Item { text: tab.iconName color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant fill: tab.current ? 1 : 0 - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large Behavior on fill { Anim {} diff --git a/modules/dashboard/WeatherTab.qml b/modules/dashboard/WeatherTab.qml index a87ce29a1..78be9ad7b 100644 --- a/modules/dashboard/WeatherTab.qml +++ b/modules/dashboard/WeatherTab.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -17,26 +17,26 @@ Item { id: layout anchors.fill: parent - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller RowLayout { - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large Layout.fillWidth: true Column { - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: Weather.city || qsTr("Loading...") - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 600 color: Colours.palette.m3onSurface } StyledText { text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } } @@ -46,7 +46,7 @@ Item { } Row { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large WeatherStat { icon: "wb_twilight" @@ -66,40 +66,40 @@ Item { StyledRect { Layout.fillWidth: true - implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2 + implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.large * 2 + radius: Tokens.rounding.large * 2 color: Colours.tPalette.m3surfaceContainer RowLayout { id: bigInfoRow anchors.centerIn: parent - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large MaterialIcon { Layout.alignment: Qt.AlignVCenter text: Weather.icon - font.pointSize: Appearance.font.size.extraLarge * 3 + font.pointSize: Tokens.font.size.extraLarge * 3 color: Colours.palette.m3secondary animate: true } ColumnLayout { Layout.alignment: Qt.AlignVCenter - spacing: -Appearance.spacing.small + spacing: -Tokens.spacing.small StyledText { text: Weather.temp - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 font.weight: 500 color: Colours.palette.m3primary } StyledText { - Layout.leftMargin: Appearance.padding.small + Layout.leftMargin: Tokens.padding.small text: Weather.description - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurfaceVariant } } @@ -108,7 +108,7 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller DetailCard { icon: "water_drop" @@ -131,18 +131,18 @@ Item { } StyledText { - Layout.topMargin: Appearance.spacing.normal - Layout.leftMargin: Appearance.padding.normal + Layout.topMargin: Tokens.spacing.normal + Layout.leftMargin: Tokens.padding.normal visible: forecastRepeater.count > 0 text: qsTr("7-Day Forecast") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 600 color: Colours.palette.m3onSurface } RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller Repeater { id: forecastRepeater @@ -156,30 +156,30 @@ Item { required property var modelData Layout.fillWidth: true - implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: forecastItemColumn anchors.centerIn: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.alignment: Qt.AlignHCenter text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 600 color: Colours.palette.m3primary } StyledText { - Layout.topMargin: -Appearance.spacing.small / 2 + Layout.topMargin: -Tokens.spacing.small / 2 Layout.alignment: Qt.AlignHCenter text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small opacity: 0.7 color: Colours.palette.m3onSurfaceVariant } @@ -187,7 +187,7 @@ Item { MaterialIcon { Layout.alignment: Qt.AlignHCenter text: forecastItem.modelData.icon - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge color: Colours.palette.m3secondary } @@ -213,17 +213,17 @@ Item { Layout.fillWidth: true Layout.preferredHeight: 60 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer Row { anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: detailRoot.icon color: detailRoot.colour - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large anchors.verticalCenter: parent.verticalCenter } @@ -233,7 +233,7 @@ Item { StyledText { text: detailRoot.label - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller opacity: 0.7 horizontalAlignment: Text.AlignLeft } @@ -254,23 +254,23 @@ Item { property string value property color colour - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { text: weatherStat.icon - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge color: weatherStat.colour } Column { StyledText { text: weatherStat.label - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller color: Colours.palette.m3onSurfaceVariant } StyledText { text: weatherStat.value - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: 600 color: Colours.palette.m3onSurface } diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 566f616a3..e01dc0f04 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -5,7 +5,7 @@ import Quickshell import Caelestia import qs.components import qs.components.filedialog -import qs.config +import Caelestia.Config import qs.utils Item { @@ -40,8 +40,8 @@ Item { Behavior on offsetScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index 64e43f5c0..d415a031e 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -7,7 +7,7 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config CustomMouseArea { id: root @@ -35,18 +35,18 @@ CustomMouseArea { id: inner anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small RowLayout { id: monthNavigationRow Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Item { implicitWidth: implicitHeight - implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 + implicitHeight: prevMonthText.implicitHeight + Tokens.padding.small * 2 StateLayer { id: prevMonthStateLayer @@ -55,7 +55,7 @@ CustomMouseArea { root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1); } - radius: Appearance.rounding.full + radius: Tokens.rounding.full } MaterialIcon { @@ -64,7 +64,7 @@ CustomMouseArea { anchors.centerIn: parent text: "chevron_left" color: Colours.palette.m3tertiary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 700 } } @@ -72,8 +72,8 @@ CustomMouseArea { Item { Layout.fillWidth: true - implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 - implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 + implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.small * 2 + implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.small * 2 StateLayer { function onClicked(): void { @@ -81,11 +81,11 @@ CustomMouseArea { } anchors.fill: monthYearDisplay - anchors.margins: -Appearance.padding.small - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal + anchors.margins: -Tokens.padding.small + anchors.leftMargin: -Tokens.padding.normal + anchors.rightMargin: -Tokens.padding.normal - radius: Appearance.rounding.full + radius: Tokens.rounding.full disabled: { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); @@ -98,7 +98,7 @@ CustomMouseArea { anchors.centerIn: parent text: grid.title color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 font.capitalization: Font.Capitalize } @@ -106,7 +106,7 @@ CustomMouseArea { Item { implicitWidth: implicitHeight - implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 + implicitHeight: nextMonthText.implicitHeight + Tokens.padding.small * 2 StateLayer { id: nextMonthStateLayer @@ -115,7 +115,7 @@ CustomMouseArea { root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } - radius: Appearance.rounding.full + radius: Tokens.rounding.full } MaterialIcon { @@ -124,7 +124,7 @@ CustomMouseArea { anchors.centerIn: parent text: "chevron_right" color: Colours.palette.m3tertiary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 700 } } @@ -167,7 +167,7 @@ CustomMouseArea { required property var model implicitWidth: implicitHeight - implicitHeight: text.implicitHeight + Appearance.padding.small * 2 + implicitHeight: text.implicitHeight + Tokens.padding.small * 2 StyledText { id: text @@ -184,7 +184,7 @@ CustomMouseArea { return Colours.palette.m3onSurfaceVariant; } opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } } @@ -208,7 +208,7 @@ CustomMouseArea { implicitHeight: today?.implicitHeight ?? 0 clip: true - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary opacity: todayItem ? 1 : 0 @@ -236,15 +236,15 @@ CustomMouseArea { Behavior on x { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on y { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 7f786a198..1b43b1371 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -24,8 +24,8 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.hourStr color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.extraLarge + font.family: Tokens.font.family.clock font.weight: 600 } @@ -33,8 +33,8 @@ Item { Layout.alignment: Qt.AlignHCenter text: "•••" color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.extraLarge * 0.9 - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.extraLarge * 0.9 + font.family: Tokens.font.family.clock } StyledText { @@ -42,8 +42,8 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.minuteStr color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.extraLarge + font.family: Tokens.font.family.clock font.weight: 600 } @@ -57,8 +57,8 @@ Item { sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.clock font.weight: 600 } } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 4f50531c4..d6cfa76ae 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -3,7 +3,7 @@ import QtQuick.Shapes import Caelestia.Services import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -20,7 +20,7 @@ Item { Behavior on playerProgress { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } @@ -43,13 +43,13 @@ Item { fillColor: "transparent" strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) strokeWidth: Config.dashboard.sizes.mediaProgressThickness - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small + radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 sweepAngle: Config.dashboard.sizes.mediaProgressSweep } @@ -63,13 +63,13 @@ Item { fillColor: "transparent" strokeColor: Colours.palette.m3primary strokeWidth: Config.dashboard.sizes.mediaProgressThickness - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small + radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small + radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress } @@ -86,7 +86,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small + anchors.margins: Tokens.padding.large + Config.dashboard.sizes.mediaProgressThickness + Tokens.spacing.small implicitHeight: width color: Colours.tPalette.m3surfaceContainerHigh @@ -119,15 +119,15 @@ Item { anchors.top: cover.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.normal + anchors.topMargin: Tokens.spacing.normal animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal - width: parent.implicitWidth - Appearance.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } @@ -136,15 +136,15 @@ Item { anchors.top: title.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small - width: parent.implicitWidth - Appearance.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } @@ -153,14 +153,14 @@ Item { anchors.top: album.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") color: Colours.palette.m3secondary - width: parent.implicitWidth - Appearance.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } @@ -169,9 +169,9 @@ Item { anchors.top: artist.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.smaller + anchors.topMargin: Tokens.spacing.smaller - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Control { function onClicked(): void { @@ -208,12 +208,12 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Appearance.spacing.small - anchors.bottomMargin: Appearance.padding.large - anchors.margins: Appearance.padding.large * 2 + anchors.topMargin: Tokens.spacing.small + anchors.bottomMargin: Tokens.padding.large + anchors.margins: Tokens.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Appearance.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + speed: Audio.beatTracker.bpm / Tokens.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit @@ -228,7 +228,7 @@ Item { function onClicked(): void { } - implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small + implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small implicitHeight: implicitWidth StateLayer { @@ -237,7 +237,7 @@ Item { } disabled: !control.canUse - radius: Appearance.rounding.full + radius: Tokens.rounding.full } MaterialIcon { @@ -249,7 +249,7 @@ Item { animate: true text: control.icon color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index f5ac45569..3dec14f2d 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -2,7 +2,7 @@ import QtQuick import qs.components import qs.components.misc import qs.services -import qs.config +import Caelestia.Config Row { id: root @@ -10,8 +10,8 @@ Row { anchors.top: parent.top anchors.bottom: parent.bottom - padding: Appearance.padding.large - spacing: Appearance.spacing.normal + padding: Tokens.padding.large + spacing: Tokens.spacing.normal Ref { service: SystemUsage @@ -44,19 +44,19 @@ Row { anchors.top: parent.top anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large implicitWidth: icon.implicitWidth StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: icon.top - anchors.bottomMargin: Appearance.spacing.small + anchors.bottomMargin: Tokens.spacing.small implicitWidth: Config.dashboard.sizes.resourceProgessThickness color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StyledRect { anchors.left: parent.left @@ -65,7 +65,7 @@ Row { implicitHeight: res.value * parent.height color: res.colour - radius: Appearance.rounding.full + radius: Tokens.rounding.full } } @@ -80,7 +80,7 @@ Row { Behavior on value { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } diff --git a/modules/dashboard/dash/SmallWeather.qml b/modules/dashboard/dash/SmallWeather.qml index 666a27010..dbc5ac3ef 100644 --- a/modules/dashboard/dash/SmallWeather.qml +++ b/modules/dashboard/dash/SmallWeather.qml @@ -1,7 +1,7 @@ import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -21,7 +21,7 @@ Item { animate: true text: Weather.icon color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 } Column { @@ -29,9 +29,9 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.large + anchors.leftMargin: Tokens.spacing.large - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { anchors.horizontalCenter: parent.horizontalCenter @@ -39,7 +39,7 @@ Item { animate: true text: Weather.temp color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 } @@ -50,7 +50,7 @@ Item { text: Weather.description elide: Text.ElideRight - width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2) + width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.large * 2) } } } diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 7dc47f748..03ca0e6b7 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -4,7 +4,7 @@ import qs.components.effects import qs.components.filedialog import qs.components.images import qs.services -import qs.config +import Caelestia.Config import qs.utils Row { @@ -13,14 +13,14 @@ Row { required property DrawerVisibilities visibilities required property FileDialog facePicker - padding: Appearance.padding.large - spacing: Appearance.spacing.normal + padding: Tokens.padding.large + spacing: Tokens.spacing.normal StyledClippingRect { implicitWidth: info.implicitHeight implicitHeight: info.implicitHeight - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) MaterialIcon { @@ -52,7 +52,7 @@ Row { Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } } @@ -60,10 +60,10 @@ Row { StyledRect { anchors.centerIn: parent - implicitWidth: selectIcon.implicitHeight + Appearance.padding.small * 2 - implicitHeight: selectIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: selectIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: selectIcon.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.palette.m3primary scale: parent.containsMouse ? 1 : 0.5 opacity: parent.containsMouse ? 1 : 0 @@ -85,19 +85,19 @@ Row { text: "frame_person" color: Colours.palette.m3onPrimary - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge } Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } } @@ -108,7 +108,7 @@ Row { id: info anchors.verticalCenter: parent.verticalCenter - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Item { id: line @@ -123,7 +123,7 @@ Row { anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 source: SysInfo.osLogo - implicitSize: Math.floor(Appearance.font.size.normal * 1.34) + implicitSize: Math.floor(Tokens.font.size.normal * 1.34) colour: Colours.palette.m3primary } @@ -134,7 +134,7 @@ Row { anchors.left: icon.right anchors.leftMargin: icon.anchors.leftMargin text: `: ${SysInfo.osPrettyName || SysInfo.osName}` - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal width: Config.dashboard.sizes.infoWidth elide: Text.ElideRight @@ -175,7 +175,7 @@ Row { fill: 1 text: line.icon color: line.colour - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { @@ -185,7 +185,7 @@ Row { anchors.left: icon.right anchors.leftMargin: icon.anchors.leftMargin text: `: ${line.text}` - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal width: Config.dashboard.sizes.infoWidth elide: Text.ElideRight diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index ff0e847aa..ce2196bfc 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -10,7 +10,7 @@ import Caelestia.Blobs import qs.components import qs.components.containers import qs.services -import qs.config +import Caelestia.Config import qs.modules.bar StyledWindow { @@ -73,25 +73,25 @@ StyledWindow { Behavior on borderThickness { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on borderRounding { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on shadowOpacity { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -221,8 +221,8 @@ StyledWindow { Behavior on extraWidth { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index ed886b31a..a55e42d0c 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.services -import qs.config +import Caelestia.Config Variants { model: Screens.screens diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index ad17a0f2f..fc782ddbe 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -3,7 +3,7 @@ import QtQuick.Controls import Quickshell import qs.components import qs.components.controls -import qs.config +import Caelestia.Config import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 0d5674144..83f304992 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -1,7 +1,7 @@ import QtQuick import Quickshell import qs.components -import qs.config +import Caelestia.Config import qs.modules.bar as Bar import qs.modules.dashboard as Dashboard import qs.modules.launcher as Launcher @@ -138,7 +138,7 @@ Item { anchors.bottom: sidebar.visible ? parent.bottom : utilities.top anchors.right: sidebar.left - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal } Sidebar.Wrapper { diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml index d42817624..022d96615 100644 --- a/modules/drawers/Regions.qml +++ b/modules/drawers/Regions.qml @@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.config +import Caelestia.Config import qs.modules.bar as Bar Region { diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 15d6e44a6..7ce6038d1 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -6,7 +6,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.modules.launcher.items import qs.modules.launcher.services @@ -22,7 +22,7 @@ StyledListView { onValuesChanged: root.currentIndex = 0 } - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small orientation: Qt.Vertical implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing @@ -32,7 +32,7 @@ StyledListView { highlightFollowsCurrentItem: false highlight: StyledRect { - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.palette.m3onSurface opacity: 0.08 @@ -42,8 +42,8 @@ StyledListView { Behavior on y { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } @@ -118,16 +118,16 @@ StyledListView { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardAccel } Anim { target: root property: "scale" from: 1 to: 0.9 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardAccel } } PropertyAction { @@ -140,16 +140,16 @@ StyledListView { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { target: root property: "scale" from: 0.9 to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardDecel } } PropertyAction { @@ -197,7 +197,7 @@ StyledListView { addDisplaced: Transition { Anim { property: "y" - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } Anim { properties: "opacity,scale" diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 8076eb544..49178db7e 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -4,7 +4,7 @@ import QtQuick import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.modules.launcher.services Item { @@ -14,8 +14,8 @@ Item { required property var panels required property real maxHeight - readonly property int padding: Appearance.padding.large - readonly property int rounding: Appearance.rounding.large + readonly property int padding: Tokens.padding.large + readonly property int rounding: Tokens.rounding.large implicitWidth: listWrapper.width + padding * 2 implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 @@ -47,7 +47,7 @@ Item { id: searchWrapper color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full anchors.left: parent.left anchors.right: parent.right @@ -72,11 +72,11 @@ Item { anchors.left: searchIcon.right anchors.right: clearIcon.left - anchors.leftMargin: Appearance.spacing.small - anchors.rightMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small + anchors.rightMargin: Tokens.spacing.small - topPadding: Appearance.padding.larger - bottomPadding: Appearance.padding.larger + topPadding: Tokens.padding.larger + bottomPadding: Tokens.padding.larger placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) @@ -176,13 +176,13 @@ Item { Behavior on width { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 775b193fa..d36cfad97 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -4,7 +4,7 @@ import QtQuick import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -60,7 +60,7 @@ Item { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } PropertyAction {} Anim { @@ -68,7 +68,7 @@ Item { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } } @@ -110,8 +110,8 @@ Item { opacity: root.currentList?.count === 0 ? 1 : 0 scale: root.currentList?.count === 0 ? 1 : 0.5 - spacing: Appearance.spacing.normal - padding: Appearance.padding.large + spacing: Tokens.spacing.normal + padding: Tokens.padding.large anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter @@ -119,7 +119,7 @@ Item { MaterialIcon { text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } @@ -130,14 +130,14 @@ Item { StyledText { text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } StyledText { text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } @@ -154,8 +154,8 @@ Item { enabled: root.visibilities.launcher Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + duration: Tokens.anim.durations.large + easing.bezierCurve: Tokens.anim.curves.emphasizedDecel } } @@ -163,8 +163,8 @@ Item { enabled: root.visibilities.launcher Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + duration: Tokens.anim.durations.large + easing.bezierCurve: Tokens.anim.curves.emphasizedDecel } } } diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index ad03e9dec..dc2dd003d 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -5,7 +5,7 @@ import QtQuick import Quickshell import qs.components.controls import qs.services -import qs.config +import Caelestia.Config PathView { id: root @@ -15,7 +15,7 @@ PathView { required property var panels required property var content - readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 + readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 readonly property int numItems: { const screen = (QsWindow.window as QsWindow)?.screen; diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index 5dfffeaef..fda2a111b 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.components -import qs.config +import Caelestia.Config import qs.modules.launcher.services Item { @@ -16,7 +16,7 @@ Item { readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled readonly property real maxHeight: { - let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; + let max = screen.height - Config.border.thickness * 2 - Tokens.spacing.large; if (visibilities.dashboard) max -= panels.dashboard.nonAnimHeight; return max; @@ -41,8 +41,8 @@ Item { Behavior on offsetScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index a3ef00dda..c0fb4ac2a 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -1,7 +1,7 @@ import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -19,27 +19,27 @@ Item { root.modelData?.onClicked(root.list); } - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller MaterialIcon { id: icon text: root.modelData?.icon ?? "" - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } Item { anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width @@ -49,18 +49,18 @@ Item { id: name text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { id: desc text: root.modelData?.desc ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - Appearance.rounding.normal * 2 + width: root.width - icon.width - Tokens.rounding.normal * 2 anchors.top: name.bottom } diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 0b64c3da0..e021275cd 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Widgets import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.utils import qs.modules.launcher.services @@ -24,14 +24,14 @@ Item { root.visibilities.launcher = false; } - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller IconImage { id: icon @@ -45,7 +45,7 @@ Item { Item { anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width - favouriteIcon.width @@ -55,18 +55,18 @@ Item { id: name text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { id: comment text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - favouriteIcon.width - Appearance.rounding.normal * 2 + width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.normal * 2 anchors.top: name.bottom } diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 369b35ef6..a016f585e 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -4,7 +4,7 @@ import Quickshell import Caelestia import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -32,20 +32,20 @@ Item { root.onClicked(); } - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.larger + anchors.margins: Tokens.padding.larger - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "function" - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge Layout.alignment: Qt.AlignVCenter } @@ -69,11 +69,11 @@ Item { StyledRect { color: Colours.palette.m3tertiary - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal clip: true - implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 + implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small * 2 Layout.alignment: Qt.AlignVCenter @@ -93,11 +93,11 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: icon.left - anchors.rightMargin: Appearance.spacing.small + anchors.rightMargin: Tokens.spacing.small text: qsTr("Open in calculator") color: Colours.palette.m3onTertiary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: stateLayer.containsMouse ? 1 : 0 @@ -111,16 +111,16 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Appearance.padding.normal + anchors.rightMargin: Tokens.padding.normal text: "open_in_new" color: Colours.palette.m3onTertiary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } Behavior on implicitWidth { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } } diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 6cc47aa48..2fbfabaa3 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -1,7 +1,7 @@ import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.modules.launcher.services Item { @@ -20,14 +20,14 @@ Item { root.modelData?.onClicked(root.list); } - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller StyledRect { id: preview @@ -38,7 +38,7 @@ Item { border.color: Qt.alpha(`#${root.modelData?.colours?.outline}`, 0.5) color: `#${root.modelData?.colours?.surface}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitWidth: parent.height * 0.8 implicitHeight: parent.height * 0.8 @@ -57,27 +57,27 @@ Item { implicitWidth: preview.implicitWidth color: `#${root.modelData?.colours?.primary}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full } } } Column { anchors.left: preview.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal anchors.verticalCenter: parent.verticalCenter - width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) + width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) spacing: 0 StyledText { text: root.modelData?.flavour ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -98,7 +98,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index b63539b73..73f669940 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -1,7 +1,7 @@ import QtQuick import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.modules.launcher.services Item { @@ -20,40 +20,40 @@ Item { root.modelData?.onClicked(root.list); } - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller MaterialIcon { id: icon text: root.modelData?.icon ?? "" - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } Column { anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.larger + anchors.leftMargin: Tokens.spacing.larger anchors.verticalCenter: icon.verticalCenter - width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) + width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) spacing: 0 StyledText { text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: root.modelData?.description ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -74,7 +74,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index beb93426a..7ffa75f76 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -4,7 +4,7 @@ import qs.components import qs.components.effects import qs.components.images import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -21,8 +21,8 @@ Item { opacity = Qt.binding(() => PathView.onPath ? 1 : 0); } - implicitWidth: image.width + Appearance.padding.larger * 2 - implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal + implicitWidth: image.width + Tokens.padding.larger * 2 + implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal StateLayer { function onClicked(): void { @@ -30,7 +30,7 @@ Item { root.visibilities.launcher = false; } - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal } Elevation { @@ -48,9 +48,9 @@ Item { id: image anchors.horizontalCenter: parent.horizontalCenter - y: Appearance.padding.large + y: Tokens.padding.large color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal implicitWidth: Config.launcher.sizes.wallpaperWidth implicitHeight: implicitWidth / 16 * 9 @@ -59,7 +59,7 @@ Item { anchors.centerIn: parent text: "image" color: Colours.tPalette.m3outline - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 font.weight: 600 } @@ -76,15 +76,15 @@ Item { id: label anchors.top: image.bottom - anchors.topMargin: Appearance.spacing.small / 2 + anchors.topMargin: Tokens.spacing.small / 2 anchors.horizontalCenter: parent.horizontalCenter - width: image.width - Appearance.padding.normal * 2 + width: image.width - Tokens.padding.normal * 2 horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight renderType: Text.QtRendering text: root.modelData.relativePath - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Behavior on scale { diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 2a76012fe..29ed6905d 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -4,7 +4,7 @@ import ".." import QtQuick import Quickshell import qs.services -import qs.config +import Caelestia.Config import qs.utils Searcher { diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index 1f1f35707..57461ffb3 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -2,7 +2,7 @@ pragma Singleton import Quickshell import Caelestia -import qs.config +import Caelestia.Config import qs.utils Searcher { diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml index a951b8c65..d7d7bf564 100644 --- a/modules/launcher/services/M3Variants.qml +++ b/modules/launcher/services/M3Variants.qml @@ -3,7 +3,7 @@ pragma Singleton import ".." import QtQuick import Quickshell -import qs.config +import Caelestia.Config import qs.utils Searcher { diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml index d389aa176..571cb1013 100644 --- a/modules/launcher/services/Schemes.qml +++ b/modules/launcher/services/Schemes.qml @@ -4,7 +4,7 @@ import ".." import QtQuick import Quickshell import Quickshell.Io -import qs.config +import Caelestia.Config import qs.utils Searcher { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 73b6e6705..95cd60183 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -6,7 +6,7 @@ import qs.components import qs.components.controls import qs.components.images import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { @@ -20,18 +20,18 @@ ColumnLayout { Layout.fillWidth: false Layout.fillHeight: true - spacing: Appearance.spacing.large * 2 + spacing: Tokens.spacing.large * 2 RowLayout { Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.alignment: Qt.AlignVCenter text: Time.hourStr color: Colours.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } @@ -39,8 +39,8 @@ ColumnLayout { Layout.alignment: Qt.AlignVCenter text: ":" color: Colours.palette.m3primary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } @@ -48,14 +48,14 @@ ColumnLayout { Layout.alignment: Qt.AlignVCenter text: Time.minuteStr color: Colours.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } Loader { asynchronous: true - Layout.leftMargin: Appearance.spacing.small + Layout.leftMargin: Tokens.spacing.small Layout.alignment: Qt.AlignVCenter active: Config.services.useTwelveHourClock @@ -64,8 +64,8 @@ ColumnLayout { sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 2 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 2 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } } @@ -73,24 +73,24 @@ ColumnLayout { StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: -Appearance.padding.large * 2 + Layout.topMargin: -Tokens.padding.large * 2 text: Time.format("dddd, d MMMM yyyy") color: Colours.palette.m3tertiary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) - font.family: Appearance.font.family.mono + font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale) + font.family: Tokens.font.family.mono font.bold: true } StyledClippingRect { - Layout.topMargin: Appearance.spacing.large * 2 + Layout.topMargin: Tokens.spacing.large * 2 Layout.alignment: Qt.AlignHCenter implicitWidth: root.centerWidth / 2 implicitHeight: root.centerWidth / 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full MaterialIcon { anchors.centerIn: parent @@ -113,10 +113,10 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter implicitWidth: root.centerWidth * 0.8 - implicitHeight: input.implicitHeight + Appearance.padding.small * 2 + implicitHeight: input.implicitHeight + Tokens.padding.small * 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full focus: true onActiveFocusChanged: { @@ -147,12 +147,12 @@ ColumnLayout { id: input anchors.fill: parent - anchors.margins: Appearance.padding.small - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.small + spacing: Tokens.spacing.normal Item { implicitWidth: implicitHeight - implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small * 2 MaterialIcon { id: fprintIcon @@ -188,10 +188,10 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: enterIcon.implicitHeight + Tokens.padding.small * 2 color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StateLayer { function onClicked(): void { @@ -215,7 +215,7 @@ ColumnLayout { Item { Layout.fillWidth: true - Layout.topMargin: -Appearance.spacing.large + Layout.topMargin: -Tokens.spacing.large implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) @@ -272,7 +272,7 @@ ColumnLayout { color: Colours.palette.m3onSurfaceVariant animateProp: "opacity" - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono horizontalAlignment: Qt.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere lineHeight: 1.2 @@ -327,8 +327,8 @@ ColumnLayout { opacity: 0 color: Colours.palette.m3error - font.pointSize: Appearance.font.size.small - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.small + font.family: Tokens.font.family.mono horizontalAlignment: Qt.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere @@ -397,13 +397,13 @@ ColumnLayout { target: message property: "scale" to: 0.7 - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } Anim { target: message property: "opacity" to: 0 - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -412,7 +412,7 @@ ColumnLayout { component FlashAnim: NumberAnimation { target: message property: "opacity" - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small easing.type: Easing.Linear } } diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index 4427099fb..175780318 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -2,25 +2,25 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config RowLayout { id: root required property var lock - spacing: Appearance.spacing.large * 2 + spacing: Tokens.spacing.large * 2 ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { Layout.fillWidth: true implicitHeight: weather.implicitHeight - topLeftRadius: Appearance.rounding.large - radius: Appearance.rounding.small + topLeftRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer WeatherInfo { @@ -34,7 +34,7 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer Fetch {} @@ -44,8 +44,8 @@ RowLayout { Layout.fillWidth: true implicitHeight: media.implicitHeight - bottomLeftRadius: Appearance.rounding.large - radius: Appearance.rounding.small + bottomLeftRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer Media { @@ -62,14 +62,14 @@ RowLayout { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { Layout.fillWidth: true implicitHeight: resources.implicitHeight - topRightRadius: Appearance.rounding.large - radius: Appearance.rounding.small + topRightRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer Resources { @@ -81,8 +81,8 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true - bottomRightRadius: Appearance.rounding.large - radius: Appearance.rounding.small + bottomRightRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer NotifDock { diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index ab6de7b6c..d3ba56e3a 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -6,36 +6,36 @@ import Quickshell.Services.UPower import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { id: root anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - anchors.topMargin: Appearance.padding.large + anchors.margins: Tokens.padding.large * 2 + anchors.topMargin: Tokens.padding.large - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true Layout.fillHeight: false - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { - implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: prompt.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: prompt.implicitHeight + Tokens.padding.normal * 2 color: Colours.palette.m3primary - radius: Appearance.rounding.small + radius: Tokens.rounding.small MonoText { id: prompt anchors.centerIn: parent text: ">" - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal color: Colours.palette.m3onPrimary } } @@ -43,7 +43,7 @@ ColumnLayout { MonoText { Layout.fillWidth: true text: "caelestiafetch.sh" - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal elide: Text.ElideRight } @@ -71,10 +71,10 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - Layout.topMargin: Appearance.padding.normal - Layout.bottomMargin: Appearance.padding.normal + Layout.topMargin: Tokens.padding.normal + Layout.bottomMargin: Tokens.padding.normal Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal WrappedLoader { Layout.fillWidth: true @@ -125,18 +125,18 @@ ColumnLayout { active: root.height > 180 sourceComponent: RowLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Repeater { - model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large))) + model: Math.max(0, Math.min(8, root.width / (Tokens.font.size.larger * 2 + Tokens.spacing.large))) StyledRect { required property int index implicitWidth: implicitHeight - implicitHeight: Appearance.font.size.larger * 2 + implicitHeight: Tokens.font.size.larger * 2 color: Colours.palette[`term${index}`] - radius: Appearance.rounding.small + radius: Tokens.rounding.small } } } @@ -168,11 +168,11 @@ ColumnLayout { component FetchText: MonoText { Layout.fillWidth: true - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal elide: Text.ElideRight } component MonoText: StyledText { - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono } } diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index 1286267a7..fdae94f7f 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -49,8 +49,8 @@ Item { animate: true color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono opacity: root.buffer ? 0 : 1 @@ -74,10 +74,10 @@ Item { anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal + implicitHeight: Tokens.font.size.normal orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false model: ScriptModel { @@ -91,7 +91,7 @@ Item { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 + radius: Tokens.rounding.small / 2 opacity: 0 scale: 0 @@ -134,8 +134,8 @@ Item { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 46862f83f..c0c1bfd37 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -5,7 +5,7 @@ import QtQuick.Effects import Quickshell.Wayland import qs.components import qs.services -import qs.config +import Caelestia.Config WlSessionLockSurface { id: root @@ -33,8 +33,8 @@ WlSessionLockSurface { target: lockContent properties: "implicitWidth,implicitHeight" to: lockContent.size - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } Anim { target: lockBg @@ -45,30 +45,30 @@ WlSessionLockSurface { target: content property: "scale" to: 0 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } Anim { target: content property: "opacity" to: 0 - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } Anim { target: lockIcon property: "opacity" to: 1 - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } Anim { target: background property: "opacity" to: 0 - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } SequentialAnimation { PauseAnimation { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } Anim { target: lockContent @@ -93,7 +93,7 @@ WlSessionLockSurface { target: background property: "opacity" to: 1 - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } SequentialAnimation { ParallelAnimation { @@ -101,15 +101,15 @@ WlSessionLockSurface { target: lockContent property: "scale" to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } Anim { target: lockContent property: "rotation" to: 360 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.standardAccel } } ParallelAnimation { @@ -117,7 +117,7 @@ WlSessionLockSurface { target: lockIcon property: "rotation" to: 360 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { target: lockIcon @@ -133,27 +133,27 @@ WlSessionLockSurface { target: content property: "scale" to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } Anim { target: lockBg property: "radius" - to: Appearance.rounding.large * 1.5 + to: Tokens.rounding.large * 1.5 } Anim { target: lockContent property: "implicitWidth" to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } Anim { target: lockContent property: "implicitHeight" to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } @@ -179,8 +179,8 @@ WlSessionLockSurface { Item { id: lockContent - readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 - readonly property int radius: size / 4 * Appearance.rounding.scale + readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 + readonly property int radius: size / 4 * Tokens.rounding.scale anchors.centerIn: parent implicitWidth: size @@ -210,7 +210,7 @@ WlSessionLockSurface { anchors.centerIn: parent text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 4 + font.pointSize: Tokens.font.size.extraLarge * 4 font.bold: true rotation: 180 } @@ -219,8 +219,8 @@ WlSessionLockSurface { id: content anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 - height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 + width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Tokens.padding.large * 2 + height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Tokens.padding.large * 2 lock: root opacity: 0 diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 9d2a03677..c93d830e6 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -34,7 +34,7 @@ Item { Behavior on opacity { Anim { - duration: Appearance.anim.durations.extraLarge + duration: Tokens.anim.durations.extraLarge } } } @@ -69,14 +69,14 @@ Item { anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large StyledText { - Layout.topMargin: Appearance.padding.large - Layout.bottomMargin: Appearance.spacing.larger + Layout.topMargin: Tokens.padding.large + Layout.bottomMargin: Tokens.spacing.larger text: qsTr("Now playing") color: Colours.palette.m3onSurfaceVariant - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono font.weight: 500 } @@ -86,8 +86,8 @@ Item { text: Players.active?.trackArtist ?? qsTr("No media") color: Colours.palette.m3primary horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.mono font.weight: 600 elide: Text.ElideRight } @@ -97,17 +97,17 @@ Item { animate: true text: Players.active?.trackTitle ?? qsTr("No media") horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.larger - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.larger + font.family: Tokens.font.family.mono elide: Text.ElideRight } RowLayout { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.large * 1.2 - Layout.bottomMargin: Appearance.padding.large + Layout.topMargin: Tokens.spacing.large * 1.2 + Layout.bottomMargin: Tokens.padding.large - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large PlayerControl { function onClicked(): void { @@ -154,12 +154,12 @@ Item { function onClicked(): void { } - Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) - implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 - implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2 + Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : active ? Tokens.padding.small * 2 : 0) + implicitWidth: controlIcon.implicitWidth + Tokens.padding.large * 2 + implicitHeight: controlIcon.implicitHeight + Tokens.padding.normal * 2 color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`] - radius: active || controlState.pressed ? Appearance.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Appearance.rounding.scale) + radius: active || controlState.pressed ? Tokens.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale) Elevation { anchors.fill: parent @@ -183,7 +183,7 @@ Item { anchors.centerIn: parent color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: control.active ? 1 : 0 Behavior on fill { @@ -193,15 +193,15 @@ Item { Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } Behavior on radius { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 50fe21533..131eeec27 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { @@ -17,15 +17,15 @@ ColumnLayout { required property var lock anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono font.weight: 500 elide: Text.ElideRight } @@ -36,7 +36,7 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: "transparent" Loader { @@ -46,7 +46,7 @@ ColumnLayout { opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Image { asynchronous: true @@ -65,15 +65,15 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") color: Colours.palette.m3outlineVariant - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.mono font.weight: 500 } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.extraLarge + duration: Tokens.anim.durations.extraLarge } } } @@ -81,7 +81,7 @@ ColumnLayout { StyledListView { anchors.fill: parent visible: !Config.lock.hideNotifs - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small clip: true model: ScriptModel { @@ -103,8 +103,8 @@ ColumnLayout { property: "scale" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -126,8 +126,8 @@ ColumnLayout { } Anim { property: "y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -138,8 +138,8 @@ ColumnLayout { } Anim { property: "y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 133dc6266..c8e5426ef 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -8,7 +8,7 @@ import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils StyledRect { @@ -46,10 +46,10 @@ StyledRect { anchors.left: parent?.left anchors.right: parent?.right - implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 clip: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) RowLayout { @@ -58,9 +58,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop @@ -99,14 +99,14 @@ StyledRect { MaterialIcon { text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } ClippingRectangle { anchors.fill: parent color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full Loader { asynchronous: true @@ -126,7 +126,7 @@ StyledRect { implicitHeight: Config.notifs.sizes.badge color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full ColouredIcon { anchors.centerIn: parent @@ -140,21 +140,21 @@ StyledRect { } ColumnLayout { - Layout.topMargin: -Appearance.padding.small - Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing) + Layout.topMargin: -Tokens.padding.small + Layout.bottomMargin: -Tokens.padding.small / 2 - (root.expanded ? 0 : spacing) Layout.fillWidth: true - spacing: Math.round(Appearance.spacing.small / 2) + spacing: Math.round(Tokens.spacing.small / 2) RowLayout { Layout.bottomMargin: -parent.spacing Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } @@ -162,15 +162,15 @@ StyledRect { animate: true text: root.notifs[0]?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Appearance.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 + implicitHeight: groupCount.implicitHeight + Tokens.padding.small color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0 Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 @@ -187,20 +187,20 @@ StyledRect { id: expandBtn anchors.centerIn: parent - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { id: groupCount - Layout.leftMargin: Appearance.padding.small / 2 + Layout.leftMargin: Tokens.padding.small / 2 animate: true text: root.notifs.length color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } MaterialIcon { - Layout.rightMargin: -Appearance.padding.small / 2 + Layout.rightMargin: -Tokens.padding.small / 2 animate: true text: root.expanded ? "expand_less" : "expand_more" color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface @@ -298,8 +298,8 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 8345ebc4d..8f780ea00 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -3,7 +3,7 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Services.Pam -import qs.config +import Caelestia.Config Scope { id: root diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 33c0e4c00..59a470d00 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -4,17 +4,17 @@ import qs.components import qs.components.controls import qs.components.misc import qs.services -import qs.config +import Caelestia.Config GridLayout { id: root anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - rowSpacing: Appearance.spacing.large - columnSpacing: Appearance.spacing.large + rowSpacing: Tokens.spacing.large + columnSpacing: Tokens.spacing.large rows: 2 columns: 2 @@ -23,28 +23,28 @@ GridLayout { } Resource { - Layout.topMargin: Appearance.padding.large + Layout.topMargin: Tokens.padding.large icon: "memory" value: SystemUsage.cpuPerc colour: Colours.palette.m3primary } Resource { - Layout.topMargin: Appearance.padding.large + Layout.topMargin: Tokens.padding.large icon: "thermostat" value: Math.min(1, SystemUsage.cpuTemp / 90) colour: Colours.palette.m3secondary } Resource { - Layout.bottomMargin: Appearance.padding.large + Layout.bottomMargin: Tokens.padding.large icon: "memory_alt" value: SystemUsage.memPerc colour: Colours.palette.m3secondary } Resource { - Layout.bottomMargin: Appearance.padding.large + Layout.bottomMargin: Tokens.padding.large icon: "hard_disk" value: SystemUsage.storagePerc colour: Colours.palette.m3tertiary @@ -61,17 +61,17 @@ GridLayout { implicitHeight: width color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.large + radius: Tokens.rounding.large CircularProgress { id: circ anchors.fill: parent value: res.value - padding: Appearance.padding.large * 3 + padding: Tokens.padding.large * 3 fgColour: res.colour bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) - strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal + strokeWidth: width < 200 ? Tokens.padding.smaller : Tokens.padding.normal } MaterialIcon { @@ -86,7 +86,7 @@ GridLayout { Behavior on value { Anim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index 213bf155d..2a8d058e8 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -4,7 +4,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -13,14 +13,14 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Tokens.padding.large * 2 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Loader { asynchronous: true - Layout.topMargin: Appearance.padding.large * 2 - Layout.bottomMargin: -Appearance.padding.large + Layout.topMargin: Tokens.padding.large * 2 + Layout.bottomMargin: -Tokens.padding.large Layout.alignment: Qt.AlignHCenter active: root.rootHeight > 610 @@ -29,24 +29,24 @@ ColumnLayout { sourceComponent: StyledText { text: qsTr("Weather") color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 } } RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large MaterialIcon { animate: true text: Weather.icon color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge * 2.5 + font.pointSize: Tokens.font.size.extraLarge * 2.5 } ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true @@ -54,7 +54,7 @@ ColumnLayout { animate: true text: Weather.description color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 elide: Text.ElideRight } @@ -65,19 +65,19 @@ ColumnLayout { animate: true text: qsTr("Humidity: %1%").arg(Weather.humidity) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } } Loader { asynchronous: true - Layout.rightMargin: Appearance.padding.smaller + Layout.rightMargin: Tokens.padding.smaller active: root.width > 400 visible: active sourceComponent: ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true @@ -86,7 +86,7 @@ ColumnLayout { text: Weather.temp color: Colours.palette.m3primary horizontalAlignment: Text.AlignRight - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 elide: Text.ElideLeft } @@ -98,7 +98,7 @@ ColumnLayout { text: qsTr("Feels like: %1").arg(Weather.feelsLike) color: Colours.palette.m3outline horizontalAlignment: Text.AlignRight - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller elide: Text.ElideLeft } } @@ -109,15 +109,15 @@ ColumnLayout { id: forecastLoader asynchronous: true - Layout.topMargin: Appearance.spacing.smaller - Layout.bottomMargin: Appearance.padding.large * 2 + Layout.topMargin: Tokens.spacing.smaller + Layout.bottomMargin: Tokens.padding.large * 2 Layout.fillWidth: true active: root.rootHeight > 820 visible: active sourceComponent: RowLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Repeater { model: { @@ -137,7 +137,7 @@ ColumnLayout { required property var modelData Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true @@ -147,13 +147,13 @@ ColumnLayout { } color: Colours.palette.m3outline horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger } MaterialIcon { Layout.alignment: Qt.AlignHCenter text: forecastHour.modelData?.icon ?? "cloud_alert" - font.pointSize: Appearance.font.size.extraLarge * 1.5 + font.pointSize: Tokens.font.size.extraLarge * 1.5 font.weight: 500 } @@ -161,7 +161,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger } } } diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index bd18276ef..c9f934aaf 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -5,7 +5,7 @@ import qs.components import qs.components.containers import qs.components.widgets import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -13,7 +13,7 @@ Item { required property DrawerVisibilities visibilities required property Item osdPanel required property Item sessionPanel - readonly property int padding: Appearance.padding.large + readonly property int padding: Tokens.padding.large anchors.top: parent.top anchors.bottom: parent.bottom @@ -25,7 +25,7 @@ Item { if (count === 0) return 0; - let height = (count - 1) * Appearance.spacing.smaller; + let height = (count - 1) * Tokens.spacing.smaller; for (let i = 0; i < count; i++) height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; @@ -49,7 +49,7 @@ Item { anchors.margins: root.padding color: "transparent" - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StyledListView { id: list @@ -89,9 +89,9 @@ Item { let height = 0; for (let i = 0; i < count; i++) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; - if (height - Appearance.spacing.smaller >= scrollY) + if (height - Tokens.spacing.smaller >= scrollY) return i; } @@ -110,9 +110,9 @@ Item { let height = 0; for (let i = count - 1; i >= 0; i--) { - height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; - if (height - Appearance.spacing.smaller >= scrollY) + if (height - Tokens.spacing.smaller >= scrollY) return count - i - 1; } @@ -140,7 +140,7 @@ Item { } implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.smaller) ListView.onRemove: removeAnim.start() @@ -171,8 +171,8 @@ Item { target: notif property: "x" to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized + duration: Tokens.anim.durations.normal + easing.bezierCurve: Tokens.anim.curves.emphasized } PropertyAction { target: wrapper @@ -183,7 +183,7 @@ Item { ClippingRectangle { anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller + anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.smaller color: "transparent" radius: notif.radius @@ -199,8 +199,8 @@ Item { } component Anim: NumberAnimation { - duration: Appearance.anim.durations.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 7b580a74f..b7d90fe79 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -9,7 +9,7 @@ import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils StyledRect { @@ -23,7 +23,7 @@ StyledRect { property bool expanded: Config.notifs.openExpanded color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal implicitWidth: Config.notifs.sizes.width implicitHeight: inner.implicitHeight @@ -36,7 +36,7 @@ StyledRect { Behavior on x { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + easing.bezierCurve: Tokens.anim.curves.emphasizedDecel } } @@ -95,14 +95,14 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal implicitHeight: root.nonAnimHeight Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -119,7 +119,7 @@ StyledRect { visible: root.hasImage || root.hasAppIcon sourceComponent: ClippingRectangle { - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitWidth: Config.notifs.sizes.image implicitHeight: Config.notifs.sizes.image @@ -147,7 +147,7 @@ StyledRect { anchors.bottom: root.hasImage ? image.bottom : undefined sourceComponent: StyledRect { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image @@ -175,14 +175,14 @@ StyledRect { asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent - anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 - anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 + anchors.horizontalCenterOffset: -Tokens.font.size.large * 0.02 + anchors.verticalCenterOffset: Tokens.font.size.large * 0.02 sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } @@ -207,9 +207,9 @@ StyledRect { PathAngleArc { id: progressArc - radiusX: progressIndicator.width / 2 - Appearance.padding.small / 2 + radiusX: progressIndicator.width / 2 - Tokens.padding.small / 2 centerX: progressIndicator.width / 2 - radiusY: progressIndicator.height / 2 - Appearance.padding.small / 2 + radiusY: progressIndicator.height / 2 - Tokens.padding.small / 2 centerY: progressIndicator.height / 2 startAngle: -90 @@ -217,7 +217,7 @@ StyledRect { Behavior on sweepAngle { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + easing.bezierCurve: Tokens.anim.curves.emphasizedDecel } } } @@ -229,13 +229,13 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Appearance.spacing.smaller + anchors.leftMargin: Tokens.spacing.smaller animate: true text: appNameMetrics.elidedText maximumLineCount: 1 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small opacity: root.expanded ? 1 : 0 @@ -251,7 +251,7 @@ StyledRect { font.family: appName.font.family font.pointSize: appName.font.pointSize elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3 } StyledText { @@ -259,7 +259,7 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Appearance.spacing.smaller + anchors.leftMargin: Tokens.spacing.smaller animate: true text: summaryMetrics.elidedText @@ -286,9 +286,9 @@ StyledRect { property: "maximumLineCount" } AnchorAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } } @@ -304,7 +304,7 @@ StyledRect { font.family: summary.font.family font.pointSize: summary.font.pointSize elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3 } StyledText { @@ -312,11 +312,11 @@ StyledRect { anchors.top: parent.top anchors.left: summary.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small text: "•" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small states: State { name: "expanded" @@ -330,9 +330,9 @@ StyledRect { transitions: Transition { AnchorAnimation { - duration: Appearance.anim.durations.normal + duration: Tokens.anim.durations.normal easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + easing.bezierCurve: Tokens.anim.curves.standard } } } @@ -342,13 +342,13 @@ StyledRect { anchors.top: parent.top anchors.left: timeSep.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignLeft text: root.modelData.timeStr color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Item { @@ -365,7 +365,7 @@ StyledRect { root.expanded = !root.expanded; } - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } @@ -376,7 +376,7 @@ StyledRect { animate: true text: root.expanded ? "expand_less" : "expand_more" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } @@ -386,13 +386,13 @@ StyledRect { anchors.left: summary.left anchors.right: expandBtn.left anchors.top: summary.bottom - anchors.rightMargin: Appearance.spacing.small + anchors.rightMargin: Tokens.spacing.small animate: true textFormat: root.bodyTextFormat text: bodyPreviewMetrics.elidedText color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small opacity: root.expanded ? 0 : 1 @@ -417,13 +417,13 @@ StyledRect { anchors.left: summary.left anchors.right: expandBtn.left anchors.top: summary.bottom - anchors.rightMargin: Appearance.spacing.small + anchors.rightMargin: Tokens.spacing.small animate: true textFormat: root.bodyTextFormat text: root.modelData.body color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere height: text ? implicitHeight : 0 @@ -447,9 +447,9 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.top: body.bottom - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller opacity: root.expanded ? 1 : 0 @@ -483,20 +483,20 @@ StyledRect { required property var modelData - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 - Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 - implicitWidth: actionText.width + Appearance.padding.normal * 2 - implicitHeight: actionText.height + Appearance.padding.small * 2 + Layout.preferredWidth: actionText.width + Tokens.padding.normal * 2 + Layout.preferredHeight: actionText.height + Tokens.padding.small * 2 + implicitWidth: actionText.width + Tokens.padding.normal * 2 + implicitHeight: actionText.height + Tokens.padding.small * 2 StateLayer { function onClicked(): void { action.modelData.invoke(); } - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface } @@ -506,7 +506,7 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } TextMetrics { @@ -518,7 +518,7 @@ StyledRect { elide: Text.ElideRight elideWidth: { const numActions = root.modelData.actions.length + 1; - return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2; + return (inner.width - actions.spacing * (numActions - 1)) / numActions - Tokens.padding.normal * 2; } } } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 5ec6ad983..b0022fd68 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -20,14 +20,14 @@ Item { required property bool sourceMuted required property real brightness - implicitWidth: layout.implicitWidth + Appearance.padding.large * 2 - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.large * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 ColumnLayout { id: layout anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal // Speaker volume CustomMouseArea { @@ -117,7 +117,7 @@ Item { Behavior on Layout.preferredHeight { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + easing.bezierCurve: Tokens.anim.curves.emphasized } } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 487b611fc..1c593e84f 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -46,8 +46,8 @@ Item { Behavior on offsetScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 0f64ec4b4..b84a5a429 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config import qs.utils Column { @@ -12,8 +12,8 @@ Column { required property DrawerVisibilities visibilities - padding: Appearance.padding.large - spacing: Appearance.spacing.large + padding: Tokens.padding.large + spacing: Tokens.spacing.large SessionButton { id: logout @@ -53,7 +53,7 @@ Column { playing: visible asynchronous: true - speed: Appearance.anim.sessionGifSpeed + speed: Tokens.anim.sessionGifSpeed source: Paths.absolutePath(Config.paths.sessionGif) } @@ -85,7 +85,7 @@ Column { implicitWidth: Config.session.sizes.button implicitHeight: Config.session.sizes.button - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer Keys.onEnterPressed: Quickshell.execDetached(button.command) @@ -128,7 +128,7 @@ Column { text: button.icon color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 } } diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 0d2d3b42a..f211cfbe2 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.components -import qs.config +import Caelestia.Config Item { id: root @@ -23,8 +23,8 @@ Item { Behavior on offsetScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index 7dbbd06c3..c6016967c 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -2,7 +2,7 @@ import QtQuick import QtQuick.Layouts import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -14,13 +14,13 @@ Item { id: layout anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainerLow NotifDock { @@ -30,7 +30,7 @@ Item { } StyledRect { - Layout.topMargin: Appearance.padding.large - layout.spacing + Layout.topMargin: Tokens.padding.large - layout.spacing Layout.fillWidth: true implicitHeight: 1 diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 5ccdc97e1..aa80d068d 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell import qs.components import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -16,11 +16,11 @@ StyledRect { required property DrawerVisibilities visibilities readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null - readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height + readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.normal * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: { const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); return expanded ? c : Qt.alpha(c, 0); @@ -32,12 +32,12 @@ StyledRect { name: "expanded" PropertyChanges { - summary.anchors.margins: Appearance.padding.normal - dummySummary.anchors.margins: Appearance.padding.normal - compactBody.anchors.margins: Appearance.padding.normal - timeStr.anchors.margins: Appearance.padding.normal - expandedContent.anchors.margins: Appearance.padding.normal - summary.width: root.width - Appearance.padding.normal * 2 - timeStr.implicitWidth - Appearance.spacing.small + summary.anchors.margins: Tokens.padding.normal + dummySummary.anchors.margins: Tokens.padding.normal + compactBody.anchors.margins: Tokens.padding.normal + timeStr.anchors.margins: Tokens.padding.normal + expandedContent.anchors.margins: Tokens.padding.normal + summary.width: root.width - Tokens.padding.normal * 2 - timeStr.implicitWidth - Tokens.spacing.small summary.maximumLineCount: Number.MAX_SAFE_INTEGER } } @@ -86,7 +86,7 @@ StyledRect { anchors.top: parent.top anchors.left: dummySummary.right anchors.right: parent.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small sourceComponent: StyledText { text: String(root.modelData?.body ?? "").replace(/\n/g, " ") @@ -106,7 +106,7 @@ StyledRect { animate: true text: root.modelData?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } @@ -117,22 +117,22 @@ StyledRect { anchors.top: summary.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Appearance.spacing.small / 2 + anchors.topMargin: Tokens.spacing.small / 2 sourceComponent: ExpandedBody {} } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } component ExpandedBody: ColumnLayout { readonly property alias body: bodyText - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { id: bodyText diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 84e6103c3..320fd1a36 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.containers import qs.components.effects import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -94,7 +94,7 @@ Item { id: actionList anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: [ @@ -114,11 +114,11 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: actionInner.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: actionInner.implicitHeight + Appearance.padding.small * 2 + implicitWidth: actionInner.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: actionInner.implicitHeight + Tokens.padding.small * 2 - Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Appearance.padding.large : 0) - radius: actionStateLayer.pressed ? Appearance.rounding.small / 2 : Appearance.rounding.small + Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0) + radius: actionStateLayer.pressed ? Tokens.rounding.small / 2 : Tokens.rounding.small color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4) Timer { @@ -183,15 +183,15 @@ Item { Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } Behavior on radius { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 11ce4dac2..a819f52b1 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -8,7 +8,7 @@ import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils Item { @@ -19,7 +19,7 @@ Item { readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal Component.onCompleted: Notifs.list.forEach(n => n.popup = false) @@ -29,7 +29,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) @@ -43,8 +43,8 @@ Item { text: root.notifCount color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono font.weight: 500 Behavior on anchors.leftMargin { @@ -62,12 +62,12 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: count.right anchors.right: parent.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono font.weight: 500 elide: Text.ElideRight } @@ -80,9 +80,9 @@ Item { anchors.right: parent.right anchors.top: title.bottom anchors.bottom: parent.bottom - anchors.topMargin: Appearance.spacing.smaller + anchors.topMargin: Tokens.spacing.smaller - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: "transparent" Loader { @@ -92,7 +92,7 @@ Item { opacity: root.notifCount > 0 ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Image { asynchronous: true @@ -111,15 +111,15 @@ Item { Layout.alignment: Qt.AlignHCenter text: qsTr("No Notifications") color: Colours.palette.m3outlineVariant - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.mono font.weight: 500 } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.extraLarge + duration: Tokens.anim.durations.extraLarge } } } @@ -177,7 +177,7 @@ Item { asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal scale: root.notifCount > 0 ? 1 : 0.5 opacity: root.notifCount > 0 ? 1 : 0 @@ -187,9 +187,9 @@ Item { id: clearBtn icon: "clear_all" - radius: Appearance.rounding.normal - padding: Appearance.padding.normal - font.pointSize: Math.round(Appearance.font.size.large * 1.2) + radius: Tokens.rounding.normal + padding: Tokens.padding.normal + font.pointSize: Math.round(Tokens.font.size.large * 1.2) onClicked: clearTimer.start() Elevation { @@ -202,14 +202,14 @@ Item { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index c40a1e01f..80d091c33 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -5,7 +5,7 @@ import Quickshell import Caelestia.Components import qs.components import qs.services -import qs.config +import Caelestia.Config LazyListView { id: root @@ -18,7 +18,7 @@ LazyListView { anchors.right: parent?.right implicitHeight: contentHeight - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small readyDelay: 1 cacheBuffer: 400 asynchronous: true @@ -33,7 +33,7 @@ LazyListView { useCustomViewport: true viewport: Qt.rect(0, container.contentY, width, container.height) - removeDuration: Appearance.anim.durations.normal + removeDuration: Tokens.anim.durations.normal model: ScriptModel { values: { @@ -129,8 +129,8 @@ LazyListView { enabled: notif.LazyListView.ready Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -140,15 +140,15 @@ LazyListView { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on x { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } @@ -159,7 +159,7 @@ LazyListView { target: root.container property: "contentY" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 0efea5427..3d624bfcc 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -7,7 +7,7 @@ import Quickshell.Services.Notifications import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config import qs.utils StyledRect { @@ -51,9 +51,9 @@ StyledRect { readonly property int urgency: groupProps.urgency readonly property int nonAnimHeight: { - const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0); + const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); const columnHeight = headerHeight + notifList.layoutHeight + column.Layout.topMargin + column.Layout.bottomMargin; - return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2); + return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.normal * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) @@ -73,10 +73,10 @@ StyledRect { anchors.left: parent?.left anchors.right: parent?.right - implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 clip: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) RowLayout { @@ -85,9 +85,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop @@ -126,14 +126,14 @@ StyledRect { MaterialIcon { text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } StyledClippingRect { anchors.fill: parent color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full Loader { asynchronous: true @@ -153,7 +153,7 @@ StyledRect { implicitHeight: Config.notifs.sizes.badge color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full ColouredIcon { anchors.centerIn: parent @@ -169,23 +169,23 @@ StyledRect { ColumnLayout { id: column - Layout.topMargin: -Appearance.padding.small - Layout.bottomMargin: -Appearance.padding.small / 2 + Layout.topMargin: -Tokens.padding.small + Layout.bottomMargin: -Tokens.padding.small / 2 Layout.fillWidth: true spacing: 0 RowLayout { id: header - Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0 + Layout.bottomMargin: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } @@ -193,15 +193,15 @@ StyledRect { animate: true text: root.notifs.find(n => !n.closed)?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Appearance.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 + implicitHeight: groupCount.implicitHeight + Tokens.padding.small color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StateLayer { function onClicked(): void { @@ -215,36 +215,36 @@ StyledRect { id: expandBtn anchors.centerIn: parent - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { id: groupCount - Layout.leftMargin: Appearance.padding.small / 2 + Layout.leftMargin: Tokens.padding.small / 2 animate: true text: root.notifCount color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } MaterialIcon { - Layout.rightMargin: -Appearance.padding.small / 2 + Layout.rightMargin: -Tokens.padding.small / 2 text: "expand_more" color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface rotation: root.expanded ? 180 : 0 - Layout.topMargin: root.expanded ? -Math.floor(Appearance.padding.smaller / 2) : 0 + Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.smaller / 2) : 0 Behavior on rotation { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on Layout.topMargin { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index aeec4c628..32d8d678b 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -6,7 +6,7 @@ import Quickshell import Caelestia.Components import qs.components import qs.services -import qs.config +import Caelestia.Config LazyListView { id: root @@ -22,12 +22,12 @@ LazyListView { Layout.fillWidth: true implicitHeight: contentHeight - spacing: Math.round(Appearance.spacing.small / 2) + spacing: Math.round(Tokens.spacing.small / 2) asynchronous: true readyDelay: 1 cacheBuffer: 400 - removeDuration: Appearance.anim.durations.normal + removeDuration: Tokens.anim.durations.normal useCustomViewport: true viewport: { @@ -132,8 +132,8 @@ LazyListView { enabled: notif.LazyListView.ready Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -147,8 +147,8 @@ LazyListView { Behavior on x { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index eefd11a0f..446a5b30a 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -2,7 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import qs.components -import qs.config +import Caelestia.Config Item { id: root @@ -20,8 +20,8 @@ Item { Behavior on offsetScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -31,13 +31,13 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large anchors.bottomMargin: 0 active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2 + implicitWidth: Config.sidebar.sizes.width - Tokens.padding.large * 2 props: root.props visibilities: root.visibilities } diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 6dcfedcdf..7ecb46fff 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -2,7 +2,7 @@ import "cards" import QtQuick import QtQuick.Layouts import qs.components -import qs.config +import Caelestia.Config import qs.modules.bar.popouts as BarPopouts Item { @@ -19,7 +19,7 @@ Item { id: layout anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IdleInhibit {} diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 4d6d8af2d..9d89fb8f7 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -8,7 +8,7 @@ import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config +import Caelestia.Config Loader { id: root @@ -33,9 +33,9 @@ Loader { Item { anchors.fill: parent - anchors.margins: -Appearance.padding.large - anchors.rightMargin: -Appearance.padding.large - Config.border.thickness - anchors.bottomMargin: -Appearance.padding.large - Config.border.thickness + anchors.margins: -Tokens.padding.large + anchors.rightMargin: -Tokens.padding.large - Config.border.thickness + anchors.bottomMargin: -Tokens.padding.large - Config.border.thickness opacity: 0.5 StyledRect { @@ -130,15 +130,15 @@ Loader { StyledRect { anchors.centerIn: parent - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: Colours.palette.m3surfaceContainerHigh scale: 0 Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0) - width: Math.min(parent.width - Appearance.padding.large * 2, implicitWidth) - implicitWidth: deleteConfirmationLayout.implicitWidth + Appearance.padding.large * 3 - implicitHeight: deleteConfirmationLayout.implicitHeight + Appearance.padding.large * 3 + width: Math.min(parent.width - Tokens.padding.large * 2, implicitWidth) + implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.large * 3 + implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.large * 3 MouseArea { anchors.fill: parent @@ -155,26 +155,26 @@ Loader { id: deleteConfirmationLayout anchors.fill: parent - anchors.margins: Appearance.padding.large * 1.5 - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large * 1.5 + spacing: Tokens.spacing.normal StyledText { text: qsTr("Delete recording?") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } StyledText { Layout.fillWidth: true text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.alignment: Qt.AlignRight - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { text: qsTr("Cancel") @@ -195,8 +195,8 @@ Loader { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index cb4bcaa01..6d666894c 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.components -import qs.config +import Caelestia.Config import qs.modules.sidebar as Sidebar import qs.modules.bar.popouts as BarPopouts @@ -47,8 +47,8 @@ Item { Anim { property: "sidebarLerp" - duration: Appearance.anim.durations.expressiveDefaultSpatial / 2 - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 + easing.bezierCurve: Tokens.anim.curves.standardAccel } }, Transition { @@ -56,16 +56,16 @@ Item { Anim { property: "sidebarLerp" - duration: Appearance.anim.durations.expressiveDefaultSpatial / 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 + easing.bezierCurve: Tokens.anim.curves.standardDecel } } ] Behavior on offsetScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } @@ -74,7 +74,7 @@ Item { anchors.top: parent.top anchors.left: parent.left - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large asynchronous: true active: root.shouldBeActive || root.visible diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 20d232a37..30176f29b 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -3,15 +3,15 @@ import QtQuick.Layouts import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root Layout.fillWidth: true - implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2 + implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer clip: true @@ -21,14 +21,14 @@ StyledRect { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { @@ -37,7 +37,7 @@ StyledRect { anchors.centerIn: parent text: "coffee" color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } @@ -48,7 +48,7 @@ StyledRect { StyledText { Layout.fillWidth: true text: qsTr("Keep Awake") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } @@ -56,7 +56,7 @@ StyledRect { Layout.fillWidth: true text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } } @@ -73,9 +73,9 @@ StyledRect { asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left - anchors.topMargin: Appearance.spacing.larger - anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight - anchors.leftMargin: Appearance.padding.large + anchors.topMargin: Tokens.spacing.larger + anchors.bottomMargin: IdleInhibitor.enabled ? Tokens.padding.large : -implicitHeight + anchors.leftMargin: Tokens.padding.large opacity: IdleInhibitor.enabled ? 1 : 0 scale: IdleInhibitor.enabled ? 1 : 0.5 @@ -83,10 +83,10 @@ StyledRect { Component.onCompleted: active = Qt.binding(() => opacity > 0) sourceComponent: StyledRect { - implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2 + implicitWidth: activeText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: activeText.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary StyledText { @@ -95,20 +95,20 @@ StyledRect { anchors.centerIn: parent text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) color: Colours.palette.m3onPrimary - font.pointSize: Math.round(Appearance.font.size.small * 0.9) + font.pointSize: Math.round(Tokens.font.size.small * 0.9) } } Behavior on anchors.bottomMargin { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } } @@ -119,8 +119,8 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 9dd16ec51..97d88dde8 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -16,28 +16,28 @@ StyledRect { Layout.fillWidth: true implicitHeight: layout.implicitHeight + layout.anchors.margins * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: layout anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal RowLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal z: 1 StyledRect { implicitWidth: implicitHeight implicitHeight: { - const h = icon.implicitHeight + Appearance.padding.smaller * 2; + const h = icon.implicitHeight + Tokens.padding.smaller * 2; return h - (h % 2); } - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Recorder.running ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { @@ -48,7 +48,7 @@ StyledRect { anchors.verticalCenterOffset: 1.5 text: "screen_record" color: Recorder.running ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } @@ -59,7 +59,7 @@ StyledRect { StyledText { Layout.fillWidth: true text: qsTr("Screen Recorder") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } @@ -67,7 +67,7 @@ StyledRect { Layout.fillWidth: true text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } } @@ -131,15 +131,15 @@ StyledRect { target: listOrControls property: "scale" to: 0.7 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardAccel } Anim { target: listOrControls property: "opacity" to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardAccel } } PropertyAction { @@ -158,15 +158,15 @@ StyledRect { target: listOrControls property: "scale" to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardDecel } Anim { target: listOrControls property: "opacity" to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.small + easing.bezierCurve: Tokens.anim.curves.standardDecel } } } @@ -187,14 +187,14 @@ StyledRect { id: recordingControls RowLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error - implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: recText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: recText.implicitHeight + Tokens.padding.smaller * 2 StyledText { id: recText @@ -203,7 +203,7 @@ StyledRect { animate: true text: Recorder.paused ? "PAUSED" : "REC" color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono } Behavior on implicitWidth { @@ -218,14 +218,14 @@ StyledRect { Anim { from: 1 to: 0 - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasizedAccel + duration: Tokens.anim.durations.large + easing.bezierCurve: Tokens.anim.curves.emphasizedAccel } Anim { from: 0 to: 1 - duration: Appearance.anim.durations.extraLarge - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + duration: Tokens.anim.durations.extraLarge + easing.bezierCurve: Tokens.anim.curves.emphasizedDecel } } } @@ -246,7 +246,7 @@ StyledRect { return qsTr("Recording for %1").arg(time); } - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -259,7 +259,7 @@ StyledRect { toggle: true checked: Recorder.paused type: IconButton.Tonal - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large onClicked: { Recorder.togglePause(); internalChecked = Recorder.paused; @@ -270,7 +270,7 @@ StyledRect { icon: "stop" inactiveColour: Colours.palette.m3error inactiveOnColour: Colours.palette.m3onError - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large onClicked: Recorder.stop() } } diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 7fb56f3ba..57b7ad64f 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -9,7 +9,7 @@ import qs.components import qs.components.containers import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.utils ColumnLayout { @@ -27,19 +27,19 @@ ColumnLayout { onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller MaterialIcon { Layout.alignment: Qt.AlignVCenter text: "list" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } StyledText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true text: qsTr("Recordings") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } IconButton { @@ -61,8 +61,8 @@ ColumnLayout { } Layout.fillWidth: true - Layout.rightMargin: -Appearance.spacing.small - implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3) + Layout.rightMargin: -Tokens.spacing.small + implicitHeight: (Tokens.font.size.larger + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) clip: true StyledScrollBar.vertical: StyledScrollBar { @@ -77,14 +77,14 @@ ColumnLayout { anchors.left: list.contentItem.left anchors.right: list.contentItem.right - anchors.rightMargin: Appearance.spacing.small - spacing: Appearance.spacing.small / 2 + anchors.rightMargin: Tokens.spacing.small + spacing: Tokens.spacing.small / 2 Component.onCompleted: baseName = modelData.baseName StyledText { Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 text: { const time = recording.baseName; const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/); @@ -169,13 +169,13 @@ ColumnLayout { active: opacity > 0 sourceComponent: ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge opacity: root.props.recordingListExpanded ? 1 : 0 scale: root.props.recordingListExpanded ? 1 : 0 @@ -195,7 +195,7 @@ ColumnLayout { } RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller MaterialIcon { Layout.alignment: Qt.AlignHCenter @@ -233,8 +233,8 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 2b14a9ae0..65b5a6273 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -6,7 +6,7 @@ import Quickshell.Bluetooth import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import qs.modules.bar.popouts as BarPopouts StyledRect { @@ -38,21 +38,21 @@ StyledRect { readonly property bool needExtraRow: quickToggles.length > 6 Layout.fillWidth: true - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: layout anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { text: qsTr("Quick Toggles") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } QuickToggleRow { @@ -69,7 +69,7 @@ StyledRect { property var rowModel: [] Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: parent.rowModel @@ -154,17 +154,17 @@ StyledRect { component Toggle: IconButton { Layout.fillWidth: true - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) - radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) + radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) toggle: true - radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial - radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial + radiusAnim.easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial } } } diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 52473a48e..756160fc7 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -4,7 +4,7 @@ import Caelestia import qs.components import qs.components.effects import qs.services -import qs.config +import Caelestia.Config StyledRect { id: root @@ -13,9 +13,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right - implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3successContainer; @@ -50,13 +50,13 @@ StyledRect { id: layout anchors.fill: parent - anchors.margins: Appearance.padding.smaller - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.normal + spacing: Tokens.spacing.normal StyledRect { - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3success; @@ -68,7 +68,7 @@ StyledRect { } implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 MaterialIcon { id: icon @@ -84,7 +84,7 @@ StyledRect { return Colours.palette.m3onError; return Colours.palette.m3onSurfaceVariant; } - font.pointSize: Math.round(Appearance.font.size.large * 1.2) + font.pointSize: Math.round(Tokens.font.size.large * 1.2) } } @@ -106,7 +106,7 @@ StyledRect { return Colours.palette.m3onErrorContainer; return Colours.palette.m3onSurface; } - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 1ef072e9e..32fb56fc2 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -5,12 +5,12 @@ import Quickshell import Caelestia import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root - readonly property int spacing: Appearance.spacing.small + readonly property int spacing: Tokens.spacing.small property bool flag function shouldShowToast(toast: Toast): bool { @@ -23,7 +23,7 @@ Item { return false; } - implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 + implicitWidth: Config.utilities.sizes.toastWidth - Tokens.padding.normal * 2 implicitHeight: { let h = -spacing; for (let i = 0; i < repeater.count; i++) { @@ -111,8 +111,8 @@ Item { properties: "opacity,scale" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } ParallelAnimation { @@ -148,8 +148,8 @@ Item { Behavior on anchors.bottomMargin { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } } } diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 3f0bf4de3..6ddda9b3a 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -5,7 +5,7 @@ import QtQuick.Layouts import Quickshell.Widgets import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -14,14 +14,14 @@ ColumnLayout { property bool moveToWsExpanded anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { - Layout.topMargin: Appearance.padding.large - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large + Layout.topMargin: Tokens.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -31,10 +31,10 @@ ColumnLayout { StyledRect { color: Colours.palette.m3primary - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: moveToWsIcon.implicitWidth + Appearance.padding.small * 2 - implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small + implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small * 2 + implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.small StateLayer { function onClicked(): void { @@ -52,27 +52,27 @@ ColumnLayout { animate: true text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right" color: Colours.palette.m3onPrimary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } WrapperItem { Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0 clip: true - topMargin: Appearance.spacing.normal - bottomMargin: Appearance.spacing.normal + topMargin: Tokens.spacing.normal + bottomMargin: Tokens.spacing.normal GridLayout { id: wsGrid - rowSpacing: Appearance.spacing.smaller - columnSpacing: Appearance.spacing.normal + rowSpacing: Tokens.spacing.smaller + columnSpacing: Tokens.spacing.normal columns: 5 Repeater { @@ -102,11 +102,11 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large - Layout.bottomMargin: Appearance.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large + Layout.bottomMargin: Tokens.padding.large - spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small + spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.normal : Tokens.spacing.small Button { function onClicked(): void { @@ -155,10 +155,10 @@ ColumnLayout { function onClicked(): void { } - radius: Appearance.rounding.small + radius: Tokens.rounding.small Layout.fillWidth: true - implicitHeight: label.implicitHeight + Appearance.padding.small * 2 + implicitHeight: label.implicitHeight + Tokens.padding.small * 2 StateLayer { id: stateLayer @@ -177,7 +177,7 @@ ColumnLayout { animate: true color: parent.onColor - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } } diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index 3934a993d..1f274ca8e 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -3,7 +3,7 @@ import QtQuick.Layouts import Quickshell.Hyprland import qs.components import qs.services -import qs.config +import Caelestia.Config ColumnLayout { id: root @@ -11,15 +11,15 @@ ColumnLayout { required property HyprlandToplevel client anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Label { - Layout.topMargin: Appearance.padding.large * 2 + Layout.topMargin: Tokens.padding.large * 2 text: root.client?.title ?? qsTr("No active client") wrapMode: Text.WrapAtWordBoundaryOrAnywhere - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -27,16 +27,16 @@ ColumnLayout { text: root.client?.lastIpcObject.class ?? qsTr("No active client") color: Colours.palette.m3tertiary - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger } StyledRect { Layout.fillWidth: true Layout.preferredHeight: 1 - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.spacing.normal - Layout.bottomMargin: Appearance.spacing.large + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.spacing.normal + Layout.bottomMargin: Tokens.spacing.large color: Colours.palette.m3secondary } @@ -130,11 +130,11 @@ ColumnLayout { required property string text property alias color: icon.color - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller MaterialIcon { id: icon @@ -149,13 +149,13 @@ ColumnLayout { text: detail.text elide: Text.ElideRight - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } component Label: StyledText { - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large Layout.fillWidth: true elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 2683e5447..4946205cc 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -7,7 +7,7 @@ import Quickshell.Hyprland import Quickshell.Wayland import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -15,7 +15,7 @@ Item { required property ShellScreen screen required property HyprlandToplevel client - Layout.preferredWidth: preview.implicitWidth + Appearance.padding.large * 2 + Layout.preferredWidth: preview.implicitWidth + Tokens.padding.large * 2 Layout.fillHeight: true StyledClippingRect { @@ -24,13 +24,13 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: label.top - anchors.topMargin: Appearance.padding.large - anchors.bottomMargin: Appearance.spacing.normal + anchors.topMargin: Tokens.padding.large + anchors.bottomMargin: Tokens.spacing.normal implicitWidth: view.implicitWidth color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.small + radius: Tokens.rounding.small Loader { asynchronous: true @@ -44,14 +44,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: "web_asset_off" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge * 3 + font.pointSize: Tokens.font.size.extraLarge * 3 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No active client") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 } @@ -59,7 +59,7 @@ Item { Layout.alignment: Qt.AlignHCenter text: qsTr("Try switching to a window") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } @@ -82,7 +82,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: Appearance.padding.large + anchors.bottomMargin: Tokens.padding.large animate: true text: { diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index d7cfc2fa1..a00791288 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -4,7 +4,7 @@ import Quickshell import Quickshell.Hyprland import qs.components import qs.services -import qs.config +import Caelestia.Config Item { id: root @@ -19,9 +19,9 @@ Item { id: child anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Preview { screen: root.screen @@ -29,7 +29,7 @@ Item { } ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Layout.preferredWidth: Config.winfo.sizes.detailsWidth Layout.fillHeight: true @@ -39,7 +39,7 @@ Item { Layout.fillHeight: true color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Details { client: root.client @@ -51,7 +51,7 @@ Item { Layout.preferredHeight: buttons.implicitHeight color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Buttons { id: buttons diff --git a/services/Audio.qml b/services/Audio.qml index e2d936a04..ad6791499 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -6,7 +6,7 @@ import Quickshell.Io import Quickshell.Services.Pipewire import Caelestia import Caelestia.Services -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/Brightness.qml b/services/Brightness.qml index 6e828d918..5e7d7f81c 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -5,7 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import qs.components.misc -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/Colours.qml b/services/Colours.qml index 8a26e1195..fd4698de6 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -6,7 +6,7 @@ import Quickshell import Quickshell.Io import Caelestia import qs.services -import qs.config +import Caelestia.Config import qs.utils Singleton { @@ -115,9 +115,9 @@ Singleton { } component Transparency: QtObject { - readonly property bool enabled: Appearance.transparency.enabled - readonly property real base: Math.max(0, Math.min(1, Appearance.transparency.base - (root.light ? 0.1 : 0))) - readonly property real layers: Appearance.transparency.layers + readonly property bool enabled: Tokens.transparency.enabled + readonly property real base: Math.max(0, Math.min(1, Tokens.transparency.base - (root.light ? 0.1 : 0))) + readonly property real layers: Tokens.transparency.layers onEnabledChanged: debounceTimer.restart() onBaseChanged: debounceTimer.restart() diff --git a/services/GameMode.qml b/services/GameMode.qml index 3a3fb7bda..ac1b235de 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Io import Caelestia import qs.services -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/Hypr.qml b/services/Hypr.qml index 98c6e7186..2c307e2a8 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -7,7 +7,7 @@ import Quickshell.Io import Caelestia import Caelestia.Internal import qs.components.misc -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/LyricsService.qml b/services/LyricsService.qml index a6b94f275..7fd63670f 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -5,7 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import Caelestia -import qs.config +import Caelestia.Config import qs.utils Singleton { diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index 1f74c97bf..e0cc61b30 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import Caelestia.Internal -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/NotifData.qml b/services/NotifData.qml index d84183e50..ac781ad5b 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Services.Notifications import Caelestia import qs.services -import qs.config +import Caelestia.Config import qs.utils QtObject { diff --git a/services/Notifs.qml b/services/Notifs.qml index 9ea5beffa..f2205fc07 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -8,7 +8,7 @@ import Quickshell.Services.Notifications import Caelestia import qs.components.misc import qs.services -import qs.config +import Caelestia.Config import qs.utils Singleton { diff --git a/services/Players.qml b/services/Players.qml index 526b00446..f3c6ab3f4 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -6,7 +6,7 @@ import Quickshell.Io import Quickshell.Services.Mpris import Caelestia import qs.components.misc -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/Screens.qml b/services/Screens.qml index 9fed887f3..7e1b85df6 100644 --- a/services/Screens.qml +++ b/services/Screens.qml @@ -1,7 +1,7 @@ pragma Singleton import Quickshell -import qs.config +import Caelestia.Config import qs.utils Singleton { diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index 79691fead..e1dc4947d 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -3,7 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/Time.qml b/services/Time.qml index d41c9142c..f92c69a14 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -2,7 +2,7 @@ pragma Singleton import QtQuick import Quickshell -import qs.config +import Caelestia.Config Singleton { property alias enabled: clock.enabled diff --git a/services/VPN.qml b/services/VPN.qml index bc0addce4..ac82a394a 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -4,7 +4,7 @@ import QtQuick import Quickshell import Quickshell.Io import Caelestia -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 602abb205..a42d926e5 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -5,7 +5,7 @@ import Quickshell import Quickshell.Io import Caelestia.Models import qs.services -import qs.config +import Caelestia.Config import qs.utils Searcher { diff --git a/services/Weather.qml b/services/Weather.qml index 40c4d895b..e5037c40f 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -3,7 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Caelestia -import qs.config +import Caelestia.Config import qs.utils Singleton { diff --git a/utils/Icons.qml b/utils/Icons.qml index aded0f4bc..1d958cffe 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -3,7 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Services.Notifications -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/utils/Paths.qml b/utils/Paths.qml index b0926348d..aa967913a 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -3,7 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Caelestia -import qs.config +import Caelestia.Config Singleton { id: root diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index 74c94e9bc..8081340ea 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -3,7 +3,7 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io -import qs.config +import Caelestia.Config import qs.utils Singleton { From 2b2d5a487358867c790fb4a770bf5eadbcd2438f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:22:58 +1000 Subject: [PATCH 288/409] feat: move size tokens to subobject --- plugin/src/Caelestia/Config/tokens.cpp | 31 ++++++++----------------- plugin/src/Caelestia/Config/tokens.hpp | 32 ++++++++++++++++++++++---- 2 files changed, 37 insertions(+), 26 deletions(-) diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 3c987ac12..281d232f8 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -20,17 +20,7 @@ QString configDir() { TokenConfig::TokenConfig(QObject* parent) : RootConfig(parent) , m_appearance(new AppearanceTokens(this)) - , m_bar(new BarTokens(this)) - , m_dashboard(new DashboardTokens(this)) - , m_launcher(new LauncherTokens(this)) - , m_notifs(new NotifsTokens(this)) - , m_osd(new OsdTokens(this)) - , m_session(new SessionTokens(this)) - , m_sidebar(new SidebarTokens(this)) - , m_utilities(new UtilitiesTokens(this)) - , m_lock(new LockTokens(this)) - , m_winfo(new WInfoTokens(this)) - , m_controlCenter(new ControlCenterTokens(this)) { + , m_sizes(new SizeTokens(this)) { s_instance = this; setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); @@ -43,17 +33,7 @@ TokenConfig::TokenConfig(QObject* parent) TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent) : RootConfig(parent) , m_appearance(new AppearanceTokens(this)) - , m_bar(new BarTokens(this)) - , m_dashboard(new DashboardTokens(this)) - , m_launcher(new LauncherTokens(this)) - , m_notifs(new NotifsTokens(this)) - , m_osd(new OsdTokens(this)) - , m_session(new SessionTokens(this)) - , m_sidebar(new SidebarTokens(this)) - , m_utilities(new UtilitiesTokens(this)) - , m_lock(new LockTokens(this)) - , m_winfo(new WInfoTokens(this)) - , m_controlCenter(new ControlCenterTokens(this)) { + , m_sizes(new SizeTokens(this)) { setSparse(true); if (!filePath.isEmpty()) setupFileBackend(filePath); @@ -132,6 +112,13 @@ const AppearanceTransparency* Tokens::transparency() const { return a ? a->transparency() : nullptr; } +const SizeTokens* Tokens::sizes() const { + if (m_scope && m_scope->tokens()) + return m_scope->tokens()->sizes(); + auto* global = TokenConfig::instance(); + return global ? global->sizes() : nullptr; +} + Tokens* Tokens::qmlAttachedProperties(QObject* object) { // Ensure GlobalConfig singleton is created before any attached property access if (!GlobalConfig::instance()) { diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 7b38804cf..27858ccb7 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -290,12 +290,10 @@ class ControlCenterTokens : public ConfigObject { : ConfigObject(parent) {} }; -class TokenConfig : public RootConfig { +class SizeTokens : public ConfigObject { Q_OBJECT - QML_ELEMENT - QML_SINGLETON + QML_ANONYMOUS - CONFIG_SUBOBJECT(AppearanceTokens, appearance) CONFIG_SUBOBJECT(BarTokens, bar) CONFIG_SUBOBJECT(DashboardTokens, dashboard) CONFIG_SUBOBJECT(LauncherTokens, launcher) @@ -308,6 +306,30 @@ class TokenConfig : public RootConfig { CONFIG_SUBOBJECT(WInfoTokens, winfo) CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter) +public: + explicit SizeTokens(QObject* parent = nullptr) + : ConfigObject(parent) + , m_bar(new BarTokens(this)) + , m_dashboard(new DashboardTokens(this)) + , m_launcher(new LauncherTokens(this)) + , m_notifs(new NotifsTokens(this)) + , m_osd(new OsdTokens(this)) + , m_session(new SessionTokens(this)) + , m_sidebar(new SidebarTokens(this)) + , m_utilities(new UtilitiesTokens(this)) + , m_lock(new LockTokens(this)) + , m_winfo(new WInfoTokens(this)) + , m_controlCenter(new ControlCenterTokens(this)) {} +}; + +class TokenConfig : public RootConfig { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + CONFIG_SUBOBJECT(AppearanceTokens, appearance) + CONFIG_SUBOBJECT(SizeTokens, sizes) + public: static TokenConfig* instance(); [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); @@ -336,6 +358,7 @@ class Tokens : public QObject { Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceAnim* anim READ anim NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) public: explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); @@ -346,6 +369,7 @@ class Tokens : public QObject { [[nodiscard]] const AppearanceFont* font() const; [[nodiscard]] const AppearanceAnim* anim() const; [[nodiscard]] const AppearanceTransparency* transparency() const; + [[nodiscard]] const SizeTokens* sizes() const; static Tokens* qmlAttachedProperties(QObject* object); From b5dcd07ab8e8c4b1238feb4c49792fe9a2c927ef Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:24:16 +1000 Subject: [PATCH 289/409] fix: use correct size tokens in QML --- modules/background/Background.qml | 2 +- modules/bar/BarWrapper.qml | 2 +- modules/bar/components/Clock.qml | 2 +- modules/bar/components/StatusIcons.qml | 2 +- modules/bar/components/Tray.qml | 2 +- .../components/workspaces/ActiveIndicator.qml | 2 +- .../bar/components/workspaces/OccupiedBg.qml | 2 +- .../workspaces/SpecialWorkspaces.qml | 2 +- .../bar/components/workspaces/Workspace.qml | 4 ++-- .../bar/components/workspaces/Workspaces.qml | 2 +- modules/bar/popouts/ActiveWindow.qml | 4 ++-- modules/bar/popouts/Battery.qml | 2 +- modules/bar/popouts/Network.qml | 2 +- modules/bar/popouts/TrayMenu.qml | 4 ++-- modules/bar/popouts/kblayout/KbLayout.qml | 2 +- modules/controlcenter/ControlCenter.qml | 4 ++-- modules/controlcenter/launcher/Settings.qml | 8 +++---- modules/dashboard/Dash.qml | 2 +- modules/dashboard/Media.qml | 14 +++++------ modules/dashboard/Tabs.qml | 2 +- modules/dashboard/dash/DateTime.qml | 2 +- modules/dashboard/dash/Media.qml | 24 +++++++++---------- modules/dashboard/dash/Resources.qml | 2 +- modules/dashboard/dash/User.qml | 8 +++---- modules/launcher/AppList.qml | 2 +- modules/launcher/ContentList.qml | 6 ++--- modules/launcher/WallpaperList.qml | 2 +- modules/launcher/items/ActionItem.qml | 2 +- modules/launcher/items/AppItem.qml | 2 +- modules/launcher/items/CalcItem.qml | 2 +- modules/launcher/items/SchemeItem.qml | 2 +- modules/launcher/items/VariantItem.qml | 2 +- modules/launcher/items/WallpaperItem.qml | 2 +- modules/lock/Center.qml | 2 +- modules/lock/LockSurface.qml | 8 +++---- modules/lock/NotifGroup.qml | 20 ++++++++-------- modules/notifications/Content.qml | 4 ++-- modules/notifications/Notification.qml | 22 ++++++++--------- modules/osd/Content.qml | 14 +++++------ modules/session/Content.qml | 8 +++---- modules/sidebar/NotifGroup.qml | 22 ++++++++--------- modules/sidebar/Wrapper.qml | 4 ++-- modules/utilities/Wrapper.qml | 2 +- modules/utilities/toasts/Toasts.qml | 2 +- modules/windowinfo/WindowInfo.qml | 4 ++-- services/NotifData.qml | 6 ++--- 46 files changed, 121 insertions(+), 121 deletions(-) diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 14669eabf..1fe3be741 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -57,7 +57,7 @@ Variants { active: Config.background.desktopClock.enabled anchors.margins: Tokens.padding.large * 2 - anchors.leftMargin: Tokens.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) + anchors.leftMargin: Tokens.padding.large * 2 + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) state: Config.background.desktopClock.position states: [ diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 8ea61505e..52050e05b 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -19,7 +19,7 @@ Item { readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Tokens.padding.smaller, Config.border.thickness) - readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 + readonly property int contentWidth: Tokens.sizes.bar.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) property bool isHovered diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index ead13d9e6..693659c53 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -11,7 +11,7 @@ StyledRect { readonly property color colour: Colours.palette.m3tertiary readonly property int padding: Config.bar.clock.background ? Tokens.padding.normal : Tokens.padding.small - implicitWidth: Config.bar.sizes.innerWidth + implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: layout.implicitHeight + root.padding * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index c32bc21f9..c2a4b0471 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -20,7 +20,7 @@ StyledRect { radius: Tokens.rounding.full clip: true - implicitWidth: Config.bar.sizes.innerWidth + implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: iconColumn.implicitHeight + Tokens.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) ColumnLayout { diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 76785813f..98650fae5 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -28,7 +28,7 @@ StyledRect { clip: true visible: height > 0 - implicitWidth: Config.bar.sizes.innerWidth + implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: nonAnimHeight color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index 0c785901d..c76f94572 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -42,7 +42,7 @@ StyledRect { clip: true y: offset + mask.y - implicitWidth: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 implicitHeight: size radius: Tokens.rounding.full color: Colours.palette.m3primary diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 891b1250f..da7e70c61 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -65,7 +65,7 @@ Item { anchors.horizontalCenter: root.horizontalCenter y: (start?.y ?? 0) - 1 - implicitWidth: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 + 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + 2 implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 741d43401..70eb22a08 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -284,7 +284,7 @@ Item { asynchronous: true Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 sourceComponent: ws.icon.length === 1 ? letterComp : iconComp diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index af0370513..62d09d4b1 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -33,7 +33,7 @@ ColumnLayout { id: indicator Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Tokens.padding.small * 2 + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 animate: true text: { @@ -61,7 +61,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true - Layout.topMargin: -Config.bar.sizes.innerWidth / 10 + Layout.topMargin: -Tokens.sizes.bar.innerWidth / 10 visible: active active: root.hasWindows diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index a0ccb7759..ae43f9749 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -27,7 +27,7 @@ StyledClippingRect { property real blur: onSpecial ? 1 : 0 - implicitWidth: Config.bar.sizes.innerWidth + implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: layout.implicitHeight + Tokens.padding.small * 2 color: Colours.tPalette.m3surfaceContainer diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index a37d81f91..e0ef85d9d 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -95,8 +95,8 @@ Item { captureSource: Hypr.activeToplevel?.wayland ?? null // qmllint disable unresolved-type live: visible - constraintSize.width: Config.bar.sizes.windowPreviewSize - constraintSize.height: Config.bar.sizes.windowPreviewSize + constraintSize.width: Tokens.sizes.bar.windowPreviewSize + constraintSize.height: Tokens.sizes.bar.windowPreviewSize } } } diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 2a11071ec..149e21c28 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -10,7 +10,7 @@ Column { id: root spacing: Tokens.spacing.normal - width: Config.bar.sizes.batteryWidth + width: Tokens.sizes.bar.batteryWidth StyledText { text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected") diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 6bcf66041..cec936d9f 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -20,7 +20,7 @@ ColumnLayout { property bool showPasswordDialog: false spacing: Tokens.spacing.small - width: Config.bar.sizes.networkWidth + width: Tokens.sizes.bar.networkWidth // Wireless section StyledText { diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index f39d64b07..9576049bd 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -78,7 +78,7 @@ StackView { required property QsMenuEntry modelData - implicitWidth: Config.bar.sizes.trayMenuWidth + implicitWidth: Tokens.sizes.bar.trayMenuWidth implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight radius: Tokens.rounding.full @@ -152,7 +152,7 @@ StackView { font.family: label.font.family elide: Text.ElideRight - elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Tokens.spacing.normal : 0) + elideWidth: Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Tokens.spacing.normal : 0) } Loader { diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 5767de46b..265d3ab89 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -15,7 +15,7 @@ ColumnLayout { } spacing: Tokens.spacing.small - width: Config.bar.sizes.kbLayoutWidth + width: Tokens.sizes.bar.kbLayoutWidth Component.onCompleted: kb.start() diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 9b1f01eaa..b2c07e52e 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -27,8 +27,8 @@ Item { signal close - implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio - implicitHeight: screen.height * Config.controlCenter.sizes.heightMult + implicitWidth: implicitHeight * Tokens.sizes.controlCenter.ratio + implicitHeight: screen.height * Tokens.sizes.controlCenter.heightMult GridLayout { anchors.fill: parent diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 1641570c8..ec29aeb2f 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -178,25 +178,25 @@ ColumnLayout { PropertyRow { label: qsTr("Item width") - value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemWidth) } PropertyRow { showTopMargin: true label: qsTr("Item height") - value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemHeight) } PropertyRow { showTopMargin: true label: qsTr("Wallpaper width") - value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperWidth) } PropertyRow { showTopMargin: true label: qsTr("Wallpaper height") - value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperHeight) } } diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 3de9fb6bd..cc6c51716 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -34,7 +34,7 @@ GridLayout { Rect { Layout.row: 0 Layout.columnSpan: 2 - Layout.preferredWidth: Config.dashboard.sizes.weatherWidth + Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth Layout.fillHeight: true radius: Tokens.rounding.large * 1.5 diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index d8ed44b3e..43a2311e2 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -18,7 +18,7 @@ Item { required property DrawerVisibilities visibilities readonly property bool needsKeyboard: lyricMenuOpen - readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 + readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Tokens.sizes.dashboard.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight property bool lyricMenuOpen: false @@ -52,7 +52,7 @@ Item { } } - implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 + implicitWidth: cover.implicitWidth + Tokens.sizes.dashboard.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 implicitHeight: nonAnimHeight Behavior on implicitHeight { @@ -111,7 +111,7 @@ Item { property color colour: Colours.palette.m3primary anchors.fill: cover - anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize + anchors.margins: -Tokens.sizes.dashboard.mediaVisualiserSize asynchronous: true preferredRendererType: Shape.CurveRenderer @@ -132,7 +132,7 @@ Item { readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars - readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize + readonly property real magnitude: value * Tokens.sizes.dashboard.mediaVisualiserSize readonly property real cos: Math.cos(angle) readonly property real sin: Math.sin(angle) @@ -159,10 +159,10 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Tokens.padding.large + Config.dashboard.sizes.mediaVisualiserSize + anchors.leftMargin: Tokens.padding.large + Tokens.sizes.dashboard.mediaVisualiserSize - implicitWidth: Config.dashboard.sizes.mediaCoverArtSize - implicitHeight: Config.dashboard.sizes.mediaCoverArtSize + implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize + implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize color: Colours.tPalette.m3surfaceContainerHigh radius: Infinity diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 94556acac..04728d2e5 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -182,7 +182,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2 + implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2 color: "transparent" radius: Tokens.rounding.small diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 1b43b1371..2df22d150 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -11,7 +11,7 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom - implicitWidth: Config.dashboard.sizes.dateTimeWidth + implicitWidth: Tokens.sizes.dashboard.dateTimeWidth ColumnLayout { anchors.left: parent.left diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index d6cfa76ae..07d2352e6 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -16,7 +16,7 @@ Item { anchors.top: parent.top anchors.bottom: parent.bottom - implicitWidth: Config.dashboard.sizes.mediaWidth + implicitWidth: Tokens.sizes.dashboard.mediaWidth Behavior on playerProgress { Anim { @@ -42,16 +42,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - strokeWidth: Config.dashboard.sizes.mediaProgressThickness + strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small - startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 - sweepAngle: Config.dashboard.sizes.mediaProgressSweep + radiusX: (cover.width + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small + radiusY: (cover.height + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small + startAngle: -90 - Tokens.sizes.dashboard.mediaProgressSweep / 2 + sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep } Behavior on strokeColor { @@ -62,16 +62,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.palette.m3primary - strokeWidth: Config.dashboard.sizes.mediaProgressThickness + strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Tokens.spacing.small - startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 - sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress + radiusX: (cover.width + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small + radiusY: (cover.height + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small + startAngle: -90 - Tokens.sizes.dashboard.mediaProgressSweep / 2 + sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress } Behavior on strokeColor { @@ -86,7 +86,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Tokens.padding.large + Config.dashboard.sizes.mediaProgressThickness + Tokens.spacing.small + anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + Tokens.spacing.small implicitHeight: width color: Colours.tPalette.m3surfaceContainerHigh diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index 3dec14f2d..f492cc58d 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -53,7 +53,7 @@ Row { anchors.bottom: icon.top anchors.bottomMargin: Tokens.spacing.small - implicitWidth: Config.dashboard.sizes.resourceProgessThickness + implicitWidth: Tokens.sizes.dashboard.resourceProgessThickness color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) radius: Tokens.rounding.full diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 03ca0e6b7..3b13bc7ce 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -120,7 +120,7 @@ Row { id: icon anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 source: SysInfo.osLogo implicitSize: Math.floor(Tokens.font.size.normal * 1.34) @@ -136,7 +136,7 @@ Row { text: `: ${SysInfo.osPrettyName || SysInfo.osName}` font.pointSize: Tokens.font.size.normal - width: Config.dashboard.sizes.infoWidth + width: Tokens.sizes.dashboard.infoWidth elide: Text.ElideRight } } @@ -170,7 +170,7 @@ Row { id: icon anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 fill: 1 text: line.icon @@ -187,7 +187,7 @@ Row { text: `: ${line.text}` font.pointSize: Tokens.font.size.normal - width: Config.dashboard.sizes.infoWidth + width: Tokens.sizes.dashboard.infoWidth elide: Text.ElideRight } } diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 7ce6038d1..bdad31e08 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -24,7 +24,7 @@ StyledListView { spacing: Tokens.spacing.small orientation: Qt.Vertical - implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing + implicitHeight: (Tokens.sizes.launcher.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing preferredHighlightBegin: 0 preferredHighlightEnd: height diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index d36cfad97..e59dd382b 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -32,7 +32,7 @@ Item { name: "apps" PropertyChanges { - root.implicitWidth: Config.launcher.sizes.itemWidth + root.implicitWidth: Tokens.sizes.launcher.itemWidth root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) appList.active: true } @@ -46,8 +46,8 @@ Item { name: "wallpapers" PropertyChanges { - root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) - root.implicitHeight: Config.launcher.sizes.wallpaperHeight + root.implicitWidth: Math.max(Tokens.sizes.launcher.itemWidth * 1.2, wallpaperList.implicitWidth) + root.implicitHeight: Tokens.sizes.launcher.wallpaperHeight wallpaperList.active: true } } diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index dc2dd003d..705563a8d 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -15,7 +15,7 @@ PathView { required property var panels required property var content - readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 + readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 readonly property int numItems: { const screen = (QsWindow.window as QsWindow)?.screen; diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index c0fb4ac2a..06d3e0c9f 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -9,7 +9,7 @@ Item { required property var modelData required property var list - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index e021275cd..f65a6b650 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -13,7 +13,7 @@ Item { required property DesktopEntry modelData required property DrawerVisibilities visibilities - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index a016f585e..d18cbf90d 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -22,7 +22,7 @@ Item { Qalculator.evalAsync(math); } - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 2fbfabaa3..c5d9d8bb3 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -10,7 +10,7 @@ Item { required property Schemes.Scheme modelData required property var list - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index 73f669940..eb486ea18 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -10,7 +10,7 @@ Item { required property M3Variants.Variant modelData required property var list - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 7ffa75f76..cd41a315d 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -52,7 +52,7 @@ Item { color: Colours.tPalette.m3surfaceContainer radius: Tokens.rounding.normal - implicitWidth: Config.launcher.sizes.wallpaperWidth + implicitWidth: Tokens.sizes.launcher.wallpaperWidth implicitHeight: implicitWidth / 16 * 9 MaterialIcon { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 95cd60183..aa4bd51bb 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -14,7 +14,7 @@ ColumnLayout { required property var lock readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) - readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale + readonly property int centerWidth: Tokens.sizes.lock.centerWidth * centerScale Layout.preferredWidth: centerWidth Layout.fillWidth: false diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index c0c1bfd37..d0001e238 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -144,14 +144,14 @@ WlSessionLockSurface { Anim { target: lockContent property: "implicitWidth" - to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio + to: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio duration: Tokens.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } Anim { target: lockContent property: "implicitHeight" - to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult + to: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult duration: Tokens.anim.durations.expressiveDefaultSpatial easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial } @@ -219,8 +219,8 @@ WlSessionLockSurface { id: content anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Tokens.padding.large * 2 - height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Tokens.padding.large * 2 + width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 + height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 lock: root opacity: 0 diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index c8e5426ef..2fdb6fb11 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -64,8 +64,8 @@ StyledRect { Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + implicitWidth: Tokens.sizes.notifs.image + implicitHeight: Tokens.sizes.notifs.image Component { id: imageComp @@ -73,12 +73,12 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: Config.notifs.sizes.image - sourceSize.height: Config.notifs.sizes.image + sourceSize.width: Tokens.sizes.notifs.image + sourceSize.height: Tokens.sizes.notifs.image cache: false asynchronous: true - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image + width: Tokens.sizes.notifs.image + height: Tokens.sizes.notifs.image } } @@ -86,7 +86,7 @@ StyledRect { id: appIconComp ColouredIcon { - implicitSize: Math.round(Config.notifs.sizes.image * 0.6) + implicitSize: Math.round(Tokens.sizes.notifs.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") @@ -122,15 +122,15 @@ StyledRect { active: root.appIcon && root.image sourceComponent: StyledRect { - implicitWidth: Config.notifs.sizes.badge - implicitHeight: Config.notifs.sizes.badge + implicitWidth: Tokens.sizes.notifs.badge + implicitHeight: Tokens.sizes.notifs.badge color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3secondaryContainer radius: Tokens.rounding.full ColouredIcon { anchors.centerIn: parent - implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) + implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index c9f934aaf..e32b84c6a 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -19,7 +19,7 @@ Item { anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: Config.notifs.sizes.width + padding * 2 + implicitWidth: Tokens.sizes.notifs.width + padding * 2 implicitHeight: { const count = list.count; if (count === 0) @@ -170,7 +170,7 @@ Item { Anim { target: notif property: "x" - to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 + to: (notif.x >= 0 ? Tokens.sizes.notifs.width : -Tokens.sizes.notifs.width) * 2 duration: Tokens.anim.durations.normal easing.bezierCurve: Tokens.anim.curves.emphasized } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index b7d90fe79..2d4c49010 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -24,10 +24,10 @@ StyledRect { color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer radius: Tokens.rounding.normal - implicitWidth: Config.notifs.sizes.width + implicitWidth: Tokens.sizes.notifs.width implicitHeight: inner.implicitHeight - x: Config.notifs.sizes.width + x: Tokens.sizes.notifs.width Component.onCompleted: { x = 0; modelData.lock(this); @@ -68,7 +68,7 @@ StyledRect { if (!containsMouse) root.modelData.timer.start(); - if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) + if (Math.abs(root.x) < Tokens.sizes.notifs.width * Config.notifs.clearThreshold) root.x = 0; else root.modelData.popup = false; @@ -114,21 +114,21 @@ StyledRect { anchors.left: parent.left anchors.top: parent.top - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image + width: Tokens.sizes.notifs.image + height: Tokens.sizes.notifs.image visible: root.hasImage || root.hasAppIcon sourceComponent: ClippingRectangle { radius: Tokens.rounding.full - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + implicitWidth: Tokens.sizes.notifs.image + implicitHeight: Tokens.sizes.notifs.image Image { anchors.fill: parent source: Qt.resolvedUrl(root.modelData.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: Config.notifs.sizes.image - sourceSize.height: Config.notifs.sizes.image + sourceSize.width: Tokens.sizes.notifs.image + sourceSize.height: Tokens.sizes.notifs.image cache: false asynchronous: true } @@ -149,8 +149,8 @@ StyledRect { sourceComponent: StyledRect { radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer - implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : Tokens.sizes.notifs.image + implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : Tokens.sizes.notifs.image Loader { id: icon diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index b0022fd68..3a82f0d05 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -38,8 +38,8 @@ Item { Audio.decrementVolume(); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight + implicitWidth: Tokens.sizes.osd.sliderWidth + implicitHeight: Tokens.sizes.osd.sliderHeight FilledSlider { anchors.fill: parent @@ -63,8 +63,8 @@ Item { Audio.decrementSourceVolume(); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight + implicitWidth: Tokens.sizes.osd.sliderWidth + implicitHeight: Tokens.sizes.osd.sliderHeight FilledSlider { anchors.fill: parent @@ -92,8 +92,8 @@ Item { monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); } - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight + implicitWidth: Tokens.sizes.osd.sliderWidth + implicitHeight: Tokens.sizes.osd.sliderHeight FilledSlider { anchors.fill: parent @@ -110,7 +110,7 @@ Item { required property bool shouldBeActive asynchronous: true - Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 + Layout.preferredHeight: shouldBeActive ? Tokens.sizes.osd.sliderHeight : 0 opacity: shouldBeActive ? 1 : 0 active: opacity > 0 visible: active diff --git a/modules/session/Content.qml b/modules/session/Content.qml index b84a5a429..45fc0283d 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -46,8 +46,8 @@ Column { } AnimatedImage { - width: Config.session.sizes.button - height: Config.session.sizes.button + width: Tokens.sizes.session.button + height: Tokens.sizes.session.button sourceSize.width: width sourceSize.height: height @@ -82,8 +82,8 @@ Column { required property string icon required property list command - implicitWidth: Config.session.sizes.button - implicitHeight: Config.session.sizes.button + implicitWidth: Tokens.sizes.session.button + implicitHeight: Tokens.sizes.session.button radius: Tokens.rounding.large color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 3d624bfcc..2616d8054 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -53,7 +53,7 @@ StyledRect { readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); const columnHeight = headerHeight + notifList.layoutHeight + column.Layout.topMargin + column.Layout.bottomMargin; - return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Tokens.padding.normal * 2); + return Math.round(Math.max(Tokens.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) @@ -91,8 +91,8 @@ StyledRect { Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + implicitWidth: Tokens.sizes.notifs.image + implicitHeight: Tokens.sizes.notifs.image Component { id: imageComp @@ -100,12 +100,12 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: Config.notifs.sizes.image - sourceSize.height: Config.notifs.sizes.image + sourceSize.width: Tokens.sizes.notifs.image + sourceSize.height: Tokens.sizes.notifs.image cache: false asynchronous: true - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image + width: Tokens.sizes.notifs.image + height: Tokens.sizes.notifs.image } } @@ -113,7 +113,7 @@ StyledRect { id: appIconComp ColouredIcon { - implicitSize: Math.round(Config.notifs.sizes.image * 0.6) + implicitSize: Math.round(Tokens.sizes.notifs.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") @@ -149,15 +149,15 @@ StyledRect { active: root.appIcon && root.image sourceComponent: StyledRect { - implicitWidth: Config.notifs.sizes.badge - implicitHeight: Config.notifs.sizes.badge + implicitWidth: Tokens.sizes.notifs.badge + implicitHeight: Tokens.sizes.notifs.badge color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer radius: Tokens.rounding.full ColouredIcon { anchors.centerIn: parent - implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) + implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 446a5b30a..e2d8f5a66 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -15,7 +15,7 @@ Item { visible: offsetScale < 1 anchors.rightMargin: (-implicitWidth - 5) * offsetScale - implicitWidth: Config.sidebar.sizes.width + implicitWidth: Tokens.sizes.sidebar.width opacity: 1 - offsetScale Behavior on offsetScale { @@ -37,7 +37,7 @@ Item { active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: Config.sidebar.sizes.width - Tokens.padding.large * 2 + implicitWidth: Tokens.sizes.sidebar.width - Tokens.padding.large * 2 props: root.props visibilities: root.visibilities } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 6d666894c..c8f87c363 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -29,7 +29,7 @@ Item { visible: offsetScale < 1 anchors.bottomMargin: (-implicitHeight - 5) * offsetScale implicitHeight: content.implicitHeight + content.anchors.margins * 2 - implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Config.utilities.sizes.width * (1 - sidebarLerp) + implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Tokens.sizes.utilities.width * (1 - sidebarLerp) opacity: 1 - offsetScale states: State { diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 32fb56fc2..7c15bf827 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -23,7 +23,7 @@ Item { return false; } - implicitWidth: Config.utilities.sizes.toastWidth - Tokens.padding.normal * 2 + implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.normal * 2 implicitHeight: { let h = -spacing; for (let i = 0; i < repeater.count; i++) { diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index a00791288..2c31d209e 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -13,7 +13,7 @@ Item { required property HyprlandToplevel client implicitWidth: child.implicitWidth - implicitHeight: screen.height * Config.winfo.sizes.heightMult + implicitHeight: screen.height * Tokens.sizes.winfo.heightMult RowLayout { id: child @@ -31,7 +31,7 @@ Item { ColumnLayout { spacing: Tokens.spacing.normal - Layout.preferredWidth: Config.winfo.sizes.detailsWidth + Layout.preferredWidth: Tokens.sizes.winfo.detailsWidth Layout.fillHeight: true StyledRect { diff --git a/services/NotifData.qml b/services/NotifData.qml index ac781ad5b..31ff7d2f4 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -54,14 +54,14 @@ QtObject { // qmllint disable uncreatable-type PanelWindow { // qmllint enable uncreatable-type - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + implicitWidth: Tokens.sizes.notifs.image + implicitHeight: Tokens.sizes.notifs.image color: "transparent" mask: Region {} Image { function tryCache(): void { - if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) + if (status !== Image.Ready || width != Tokens.sizes.notifs.image || height != Tokens.sizes.notifs.image) return; const cacheKey = notif.appName + notif.summary + notif.id; From 180120801a66d24f5064536824e80c58a7f4970f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:33:18 +1000 Subject: [PATCH 290/409] feat: add proper anim tokens to Tokens --- plugin/src/Caelestia/Config/CMakeLists.txt | 1 + plugin/src/Caelestia/Config/anim.cpp | 62 +++++++++++++++++ plugin/src/Caelestia/Config/anim.hpp | 78 ++++++++++++++++++++++ plugin/src/Caelestia/Config/tokens.cpp | 36 ++++++---- plugin/src/Caelestia/Config/tokens.hpp | 9 ++- 5 files changed, 172 insertions(+), 14 deletions(-) create mode 100644 plugin/src/Caelestia/Config/anim.cpp create mode 100644 plugin/src/Caelestia/Config/anim.hpp diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 9d8a9d994..92ee2da9f 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -6,6 +6,7 @@ qml_module(caelestia-config configscope.cpp appearanceconfig.cpp tokens.cpp + anim.cpp monitorconfigmanager.cpp backgroundconfig.hpp barconfig.hpp diff --git a/plugin/src/Caelestia/Config/anim.cpp b/plugin/src/Caelestia/Config/anim.cpp new file mode 100644 index 000000000..9c2b91118 --- /dev/null +++ b/plugin/src/Caelestia/Config/anim.cpp @@ -0,0 +1,62 @@ +#include "anim.hpp" +#include "appearanceconfig.hpp" +#include "tokens.hpp" + +#include + +namespace caelestia::config { + +AnimTokens::AnimTokens(QObject* parent) + : QObject(parent) {} + +QEasingCurve AnimTokens::buildCurve(const QList& points) { + QEasingCurve curve(QEasingCurve::BezierSpline); + + // Points come in pairs of (x, y) forming cubic bezier segments. + // Each segment needs 3 control points: c1, c2, endPoint. + // So 6 values per segment: c1x, c1y, c2x, c2y, endX, endY. + for (int i = 0; i + 5 < points.size(); i += 6) { + QPointF c1(points[i], points[i + 1]); + QPointF c2(points[i + 2], points[i + 3]); + QPointF end(points[i + 4], points[i + 5]); + curve.addCubicBezierSegment(c1, c2, end); + } + + return curve; +} + +void AnimTokens::rebuildCurves() { + if (!m_curves) + return; + + m_emphasized = buildCurve(m_curves->emphasized()); + m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel()); + m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel()); + m_standard = buildCurve(m_curves->standard()); + m_standardAccel = buildCurve(m_curves->standardAccel()); + m_standardDecel = buildCurve(m_curves->standardDecel()); + m_expressiveFastSpatial = buildCurve(m_curves->expressiveFastSpatial()); + m_expressiveDefaultSpatial = buildCurve(m_curves->expressiveDefaultSpatial()); + m_expressiveSlowSpatial = buildCurve(m_curves->expressiveSlowSpatial()); + + emit curvesChanged(); +} + +void AnimTokens::bindCurves(AnimCurves* curves) { + m_curves = curves; + + // Rebuild when any curve control points change + connect(curves, &AnimCurves::propertiesChanged, this, &AnimTokens::rebuildCurves); + + rebuildCurves(); +} + +void AnimTokens::bindDurations(AnimDurations* durations) { + if (m_durations == durations) + return; + + m_durations = durations; + emit durationsChanged(); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/anim.hpp b/plugin/src/Caelestia/Config/anim.hpp new file mode 100644 index 000000000..464ee32a0 --- /dev/null +++ b/plugin/src/Caelestia/Config/anim.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::config { + +class AnimCurves; +class AnimDurations; +class ConfigScope; + +class AnimTokens : public QObject { + Q_OBJECT + Q_MOC_INCLUDE("appearanceconfig.hpp") + QML_ANONYMOUS + + Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve emphasizedAccel READ emphasizedAccel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve emphasizedDecel READ emphasizedDecel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve standard READ standard NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve standardAccel READ standardAccel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve standardDecel READ standardDecel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveFastSpatial READ expressiveFastSpatial NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY curvesChanged) + + Q_PROPERTY(caelestia::config::AnimDurations* durations READ durations NOTIFY durationsChanged) + +public: + explicit AnimTokens(QObject* parent = nullptr); + + void bindCurves(AnimCurves* curves); + void bindDurations(AnimDurations* durations); + + [[nodiscard]] QEasingCurve emphasized() const { return m_emphasized; } + + [[nodiscard]] QEasingCurve emphasizedAccel() const { return m_emphasizedAccel; } + + [[nodiscard]] QEasingCurve emphasizedDecel() const { return m_emphasizedDecel; } + + [[nodiscard]] QEasingCurve standard() const { return m_standard; } + + [[nodiscard]] QEasingCurve standardAccel() const { return m_standardAccel; } + + [[nodiscard]] QEasingCurve standardDecel() const { return m_standardDecel; } + + [[nodiscard]] QEasingCurve expressiveFastSpatial() const { return m_expressiveFastSpatial; } + + [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const { return m_expressiveDefaultSpatial; } + + [[nodiscard]] QEasingCurve expressiveSlowSpatial() const { return m_expressiveSlowSpatial; } + + [[nodiscard]] AnimDurations* durations() const { return m_durations; } + +signals: + void curvesChanged(); + void durationsChanged(); + +private: + void rebuildCurves(); + static QEasingCurve buildCurve(const QList& points); + + AnimCurves* m_curves = nullptr; + AnimDurations* m_durations = nullptr; + + QEasingCurve m_emphasized; + QEasingCurve m_emphasizedAccel; + QEasingCurve m_emphasizedDecel; + QEasingCurve m_standard; + QEasingCurve m_standardAccel; + QEasingCurve m_standardDecel; + QEasingCurve m_expressiveFastSpatial; + QEasingCurve m_expressiveDefaultSpatial; + QEasingCurve m_expressiveSlowSpatial; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 281d232f8..bf8082496 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -62,24 +62,41 @@ TokenConfig* TokenConfig::create(QQmlEngine* engine, QJSEngine*) { // Tokens (attached type) +// Resolve appearance from per-monitor GlobalConfig overlay or global GlobalConfig +static const AppearanceConfig* resolveAppearance(ConfigScope* scope) { + if (scope && scope->config()) + return scope->config()->appearance(); + auto* global = GlobalConfig::instance(); + return global ? global->appearance() : nullptr; +} + Tokens::Tokens(ConfigScope* scope, QObject* parent) : QObject(parent) - , m_scope(scope) { + , m_scope(scope) + , m_anim(new AnimTokens(this)) { connectScope(); + bindAnim(); } void Tokens::connectScope() { if (!m_scope) return; connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged); + connect(m_scope, &ConfigScope::configChanged, this, &Tokens::bindAnim); } -// Resolve appearance from per-monitor GlobalConfig overlay or global GlobalConfig -static const AppearanceConfig* resolveAppearance(ConfigScope* scope) { - if (scope && scope->config()) - return scope->config()->appearance(); - auto* global = GlobalConfig::instance(); - return global ? global->appearance() : nullptr; +void Tokens::bindAnim() { + auto* appearance = resolveAppearance(m_scope); + if (!appearance) + return; + + // Bind durations from resolved GlobalConfig appearance + m_anim->bindDurations(appearance->anim()->durations()); + + // Bind curves from TokenConfig + auto* tokens = TokenConfig::instance(); + if (tokens) + m_anim->bindCurves(tokens->appearance()->curves()); } const AppearanceRounding* Tokens::rounding() const { @@ -102,11 +119,6 @@ const AppearanceFont* Tokens::font() const { return a ? a->font() : nullptr; } -const AppearanceAnim* Tokens::anim() const { - auto* a = resolveAppearance(m_scope); - return a ? a->anim() : nullptr; -} - const AppearanceTransparency* Tokens::transparency() const { auto* a = resolveAppearance(m_scope); return a ? a->transparency() : nullptr; diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 27858ccb7..a9d879b47 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -1,5 +1,6 @@ #pragma once +#include "anim.hpp" #include "appearanceconfig.hpp" #include "configobject.hpp" @@ -356,9 +357,9 @@ class Tokens : public QObject { Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceAnim* anim READ anim NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) public: explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); @@ -367,10 +368,12 @@ class Tokens : public QObject { [[nodiscard]] const AppearanceSpacing* spacing() const; [[nodiscard]] const AppearancePadding* padding() const; [[nodiscard]] const AppearanceFont* font() const; - [[nodiscard]] const AppearanceAnim* anim() const; [[nodiscard]] const AppearanceTransparency* transparency() const; + [[nodiscard]] const SizeTokens* sizes() const; + [[nodiscard]] const AnimTokens* anim() const { return m_anim; } + static Tokens* qmlAttachedProperties(QObject* object); signals: @@ -378,8 +381,10 @@ class Tokens : public QObject { private: void connectScope(); + void bindAnim(); ConfigScope* m_scope; + AnimTokens* m_anim = nullptr; }; } // namespace caelestia::config From d99fefdd3465be5bf940a5c336824bf1f1e626e1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:40:19 +1000 Subject: [PATCH 291/409] fix: use correct anim tokens in QML --- components/Anim.qml | 3 +-- components/CAnim.qml | 3 +-- components/StateLayer.qml | 2 +- components/StyledText.qml | 5 ++--- components/controls/CollapsibleSection.qml | 8 ++++---- components/controls/FilledSlider.qml | 4 ++-- components/controls/Menu.qml | 2 +- components/controls/SplitButton.qml | 2 +- components/controls/StyledSwitch.qml | 3 +-- components/controls/ToggleButton.qml | 4 ++-- components/controls/Tooltip.qml | 4 ++-- components/filedialog/FolderContents.qml | 6 +++--- components/widgets/ExtraIndicator.qml | 2 +- modules/areapicker/Picker.qml | 2 +- modules/background/Background.qml | 2 +- modules/background/DesktopClock.qml | 2 +- modules/bar/BarWrapper.qml | 4 ++-- modules/bar/components/ActiveWindow.qml | 2 +- modules/bar/components/StatusIcons.qml | 4 ++-- modules/bar/components/Tray.qml | 6 +++--- .../components/workspaces/ActiveIndicator.qml | 2 +- .../bar/components/workspaces/OccupiedBg.qml | 2 +- .../workspaces/SpecialWorkspaces.qml | 14 ++++++------- .../bar/components/workspaces/Workspace.qml | 4 ++-- modules/bar/popouts/Battery.qml | 3 +-- modules/bar/popouts/ClipWrapper.qml | 6 +++--- modules/bar/popouts/WirelessPassword.qml | 2 +- modules/bar/popouts/Wrapper.qml | 8 ++++---- modules/controlcenter/NavRail.qml | 6 +++--- modules/controlcenter/bluetooth/Details.qml | 11 +++++----- modules/controlcenter/bluetooth/Settings.qml | 11 +++++----- .../components/ConnectedButtonGroup.qml | 4 ++-- .../components/PaneTransition.qml | 12 ++++------- modules/controlcenter/network/VpnDetails.qml | 8 ++++---- modules/controlcenter/network/VpnList.qml | 20 +++++++++---------- .../network/WirelessPasswordDialog.qml | 2 +- modules/dashboard/Content.qml | 4 ++-- modules/dashboard/Media.qml | 4 ++-- modules/dashboard/Tabs.qml | 5 ++--- modules/dashboard/Wrapper.qml | 2 +- modules/dashboard/dash/Calendar.qml | 4 ++-- modules/dashboard/dash/User.qml | 2 +- modules/drawers/ContentWindow.qml | 11 ++++------ modules/launcher/AppList.qml | 10 +++++----- modules/launcher/ContentList.qml | 4 ++-- modules/launcher/Wrapper.qml | 2 +- modules/launcher/items/CalcItem.qml | 2 +- modules/lock/InputField.qml | 2 +- modules/lock/LockSurface.qml | 16 +++++++-------- modules/lock/Media.qml | 4 ++-- modules/lock/NotifDock.qml | 6 +++--- modules/lock/NotifGroup.qml | 2 +- modules/notifications/Content.qml | 5 ++--- modules/notifications/Notification.qml | 12 +++++------ modules/osd/Content.qml | 2 +- modules/osd/Wrapper.qml | 2 +- modules/session/Wrapper.qml | 2 +- modules/sidebar/Notif.qml | 2 +- modules/sidebar/NotifActionList.qml | 4 ++-- modules/sidebar/NotifDock.qml | 2 +- modules/sidebar/NotifDockList.qml | 8 ++++---- modules/sidebar/NotifGroup.qml | 4 ++-- modules/sidebar/NotifGroupList.qml | 4 ++-- modules/sidebar/Wrapper.qml | 2 +- modules/utilities/RecordingDeleteModal.qml | 2 +- modules/utilities/Wrapper.qml | 6 +++--- modules/utilities/cards/IdleInhibit.qml | 4 ++-- modules/utilities/cards/Record.qml | 12 +++++------ modules/utilities/cards/RecordingList.qml | 2 +- modules/utilities/cards/Toggles.qml | 4 ++-- modules/utilities/toasts/Toasts.qml | 4 ++-- 71 files changed, 165 insertions(+), 183 deletions(-) diff --git a/components/Anim.qml b/components/Anim.qml index bf8eefb1c..69484591e 100644 --- a/components/Anim.qml +++ b/components/Anim.qml @@ -3,6 +3,5 @@ import Caelestia.Config NumberAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } diff --git a/components/CAnim.qml b/components/CAnim.qml index 3b7b7b2f8..b86fcce1f 100644 --- a/components/CAnim.qml +++ b/components/CAnim.qml @@ -3,6 +3,5 @@ import Caelestia.Config ColorAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } diff --git a/components/StateLayer.qml b/components/StateLayer.qml index 59c5cb23a..545a7583d 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -63,7 +63,7 @@ MouseArea { properties: "implicitWidth,implicitHeight" from: 0 to: rippleAnim.radius * 2 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { target: ripple diff --git a/components/StyledText.qml b/components/StyledText.qml index 051f58043..83dc146d8 100644 --- a/components/StyledText.qml +++ b/components/StyledText.qml @@ -29,12 +29,12 @@ Text { SequentialAnimation { Anim { to: root.animateFrom - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } PropertyAction {} Anim { to: root.animateTo - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } } @@ -43,6 +43,5 @@ Text { target: root property: root.animateProp duration: root.animateDuration / 2 - easing.type: Easing.BezierSpline } } diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 19c7da9b8..95aae8eee 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -56,7 +56,7 @@ ColumnLayout { Behavior on rotation { Anim { duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } } @@ -84,7 +84,7 @@ ColumnLayout { Behavior on Layout.preferredHeight { Anim { - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } @@ -99,7 +99,7 @@ ColumnLayout { Behavior on opacity { Anim { - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } } @@ -118,7 +118,7 @@ ColumnLayout { Behavior on opacity { Anim { - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index 5a65722f1..c71f65e98 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -97,7 +97,7 @@ Slider { property: "scale" to: 0 duration: Tokens.anim.durations.normal / 2 - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } ScriptAction { script: icon.update() @@ -107,7 +107,7 @@ Slider { property: "scale" to: 1 duration: Tokens.anim.durations.normal / 2 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } } diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 6c3757674..966837ead 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -109,7 +109,7 @@ Elevation { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index 8759635c7..cdea38f02 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -86,7 +86,7 @@ Row { Behavior on Layout.preferredWidth { Anim { - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index 92cde7dad..be7eb9c55 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -146,7 +146,6 @@ Switch { component PropAnim: PropertyAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 4e4174b0b..fce6215b7 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -88,14 +88,14 @@ StyledRect { Behavior on radius { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } Behavior on Layout.preferredWidth { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index fdb27bea1..06b292de5 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -101,7 +101,7 @@ Popup { from: 0 to: 1 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } @@ -111,7 +111,7 @@ Popup { from: 1 to: 0 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index 980bd3e1b..ebd5098dc 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -121,7 +121,7 @@ Item { from: 0 to: 1 duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -140,12 +140,12 @@ Item { Anim { properties: "opacity,scale" to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index 938f1ec16..1c766bf0c 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -45,7 +45,7 @@ StyledRect { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 6234d192d..6399981fd 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -308,6 +308,6 @@ MouseArea { component ExAnim: Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 1fe3be741..547fdc9ba 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -147,7 +147,7 @@ Variants { transitions: Transition { AnchorAnimation { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 43f3fcef8..c184df784 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -159,7 +159,7 @@ Item { Behavior on clockScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 52050e05b..1f10d366f 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -58,7 +58,7 @@ Item { target: root property: "implicitWidth" duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } }, Transition { @@ -68,7 +68,7 @@ Item { Anim { target: root property: "implicitWidth" - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } ] diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 63cdb04a9..5979edd4d 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -102,7 +102,7 @@ Item { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index c2a4b0471..3b11036b6 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -212,13 +212,13 @@ StyledRect { from: 1 to: 0 duration: Tokens.anim.durations.large - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } Anim { from: 0 to: 1 duration: Tokens.anim.durations.large - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 98650fae5..5adc5e572 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -49,7 +49,7 @@ StyledRect { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -57,7 +57,7 @@ StyledRect { Anim { properties: "scale" to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" @@ -117,7 +117,7 @@ StyledRect { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index c76f94572..e9a32b45f 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -93,6 +93,6 @@ StyledRect { } component EAnim: Anim { - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index da7e70c61..c54db55c6 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -76,7 +76,7 @@ Item { Behavior on scale { Anim { - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 70eb22a08..a825c1e54 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -119,7 +119,7 @@ Item { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -140,7 +140,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" @@ -151,7 +151,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" @@ -192,13 +192,13 @@ Item { Behavior on y { Anim { - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } Behavior on implicitHeight { Anim { - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } @@ -328,7 +328,7 @@ Item { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -336,7 +336,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index 62d09d4b1..e57f6f5e4 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -74,7 +74,7 @@ ColumnLayout { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -82,7 +82,7 @@ ColumnLayout { Anim { properties: "scale" to: 1 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index 149e21c28..cbcdc7152 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -149,8 +149,7 @@ Column { transitions: Transition { AnchorAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index 810ac2e56..68c301a8e 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -36,14 +36,14 @@ Item { Behavior on offsetScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } Behavior on x { Anim { duration: content.animLength - easing.bezierCurve: content.animCurve + easing: content.animCurve } } @@ -52,7 +52,7 @@ Item { Anim { duration: content.animLength - easing.bezierCurve: content.animCurve + easing: content.animCurve } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 153170695..f1a79366b 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -455,7 +455,7 @@ ColumnLayout { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index fbf5075fa..d99ffdabd 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -33,12 +33,12 @@ Item { property string queuedMode property int animLength: Tokens.anim.durations.expressiveDefaultSpatial - property list animCurve: Tokens.anim.curves.expressiveDefaultSpatial + property easingCurve animCurve: Tokens.anim.expressiveDefaultSpatial function setAnims(detach: bool): void { const type = `expressive${detach ? "Slow" : "Default"}Spatial`; animLength = Tokens.anim.durations[type]; - animCurve = Tokens.anim.curves[type]; + animCurve = Tokens.anim[type]; } function detach(mode: string): void { @@ -140,7 +140,7 @@ Item { Behavior on implicitWidth { Anim { duration: root.animLength - easing.bezierCurve: root.animCurve + easing: root.animCurve } } @@ -149,7 +149,7 @@ Item { Anim { duration: root.animLength - easing.bezierCurve: root.animCurve + easing: root.animCurve } } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 706d9d98a..fe80493ae 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -104,14 +104,14 @@ Item { Behavior on implicitWidth { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } @@ -162,7 +162,7 @@ Item { Anim { properties: "implicitWidth,implicitHeight" duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 04b4ba01a..42ceace2d 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -174,8 +174,7 @@ StyledFlickable { transitions: Transition { AnchorAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } Anim { properties: "implicitHeight,opacity,padding" @@ -266,7 +265,7 @@ StyledFlickable { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } @@ -500,7 +499,7 @@ StyledFlickable { Anim { property: "implicitWidth" duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } Anim { property: "opacity" @@ -520,7 +519,7 @@ StyledFlickable { Anim { property: "implicitWidth" duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } Anim { property: "opacity" @@ -613,7 +612,7 @@ StyledFlickable { Anim { properties: "implicitWidth,implicitHeight" duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } Anim { properties: "radius,font.pointSize" diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index bd09deebc..2588a28c4 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -171,7 +171,7 @@ ColumnLayout { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } @@ -251,14 +251,14 @@ ColumnLayout { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } @@ -315,8 +315,7 @@ ColumnLayout { transitions: Transition { AnchorAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } Anim { properties: "implicitHeight,opacity,padding" @@ -406,7 +405,7 @@ ColumnLayout { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index e95fa094a..b4ddfc22e 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -99,14 +99,14 @@ StyledRect { Behavior on Layout.preferredWidth { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } Behavior on radius { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml index a025f7d08..489d3c784 100644 --- a/modules/controlcenter/components/PaneTransition.qml +++ b/modules/controlcenter/components/PaneTransition.qml @@ -21,8 +21,7 @@ SequentialAnimation { from: root.opacityFrom to: root.opacityTo duration: Tokens.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } NumberAnimation { @@ -31,8 +30,7 @@ SequentialAnimation { from: root.scaleFrom to: root.scaleTo duration: Tokens.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } } @@ -54,8 +52,7 @@ SequentialAnimation { from: root.opacityTo to: root.opacityFrom duration: Tokens.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } NumberAnimation { @@ -64,8 +61,7 @@ SequentialAnimation { from: root.scaleTo to: root.scaleFrom duration: Tokens.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } } diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 214e4eb34..9906ef37e 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -277,14 +277,14 @@ DeviceDetails { from: 0 to: 1 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } Anim { property: "scale" from: 0.7 to: 1 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } @@ -294,14 +294,14 @@ DeviceDetails { from: 1 to: 0 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } Anim { property: "scale" from: 1 to: 0.7 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 5ca385cb8..05054876a 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -382,14 +382,14 @@ ColumnLayout { from: 0 to: 1 duration: Tokens.anim.durations.normal - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } Anim { property: "scale" from: 0.7 to: 1 duration: Tokens.anim.durations.normal - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } @@ -401,14 +401,14 @@ ColumnLayout { from: 1 to: 0 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } Anim { property: "scale" from: 1 to: 0.7 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } @@ -435,7 +435,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.normal - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } @@ -446,7 +446,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.normal - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } @@ -461,7 +461,7 @@ ColumnLayout { Behavior on opacity { Anim { duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } @@ -584,7 +584,7 @@ ColumnLayout { Behavior on opacity { Anim { duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } @@ -823,7 +823,7 @@ ColumnLayout { property: "opacity" to: 0 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } @@ -839,7 +839,7 @@ ColumnLayout { property: "opacity" to: 1 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 14fb565a5..4222ddf49 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -379,7 +379,7 @@ Item { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index a07aca412..6fc7faeac 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -200,14 +200,14 @@ Item { Behavior on implicitWidth { Anim { duration: Tokens.anim.durations.large - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.large - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 43a2311e2..29de39343 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -501,12 +501,12 @@ Item { Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : implicitHeight / 2 radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial - radiusAnim.easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + radiusAnim.easing: Tokens.anim.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 04728d2e5..54e80c7af 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -164,15 +164,14 @@ Item { from: 0 to: rippleAnim.radius * 2 duration: Tokens.anim.durations.normal - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { target: ripple property: "opacity" to: 0 duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index e01dc0f04..7a810dc00 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -41,7 +41,7 @@ Item { Behavior on offsetScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index d415a031e..38d2d563b 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -237,14 +237,14 @@ CustomMouseArea { Behavior on x { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } Behavior on y { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 3b13bc7ce..83ddcb2e1 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -91,7 +91,7 @@ Row { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index ce2196bfc..8d6654775 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -74,24 +74,21 @@ StyledWindow { Behavior on borderThickness { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } Behavior on borderRounding { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } Behavior on shadowOpacity { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -222,7 +219,7 @@ StyledWindow { Behavior on extraWidth { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index bdad31e08..dc3ee16cb 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -43,7 +43,7 @@ StyledListView { Behavior on y { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } @@ -119,7 +119,7 @@ StyledListView { from: 1 to: 0 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } Anim { target: root @@ -127,7 +127,7 @@ StyledListView { from: 1 to: 0.9 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } } PropertyAction { @@ -141,7 +141,7 @@ StyledListView { from: 0 to: 1 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { target: root @@ -149,7 +149,7 @@ StyledListView { from: 0.9 to: 1 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } PropertyAction { diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index e59dd382b..ac10fd4d8 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -155,7 +155,7 @@ Item { Anim { duration: Tokens.anim.durations.large - easing.bezierCurve: Tokens.anim.curves.emphasizedDecel + easing: Tokens.anim.emphasizedDecel } } @@ -164,7 +164,7 @@ Item { Anim { duration: Tokens.anim.durations.large - easing.bezierCurve: Tokens.anim.curves.emphasizedDecel + easing: Tokens.anim.emphasizedDecel } } } diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index fda2a111b..b0b8753bb 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -42,7 +42,7 @@ Item { Behavior on offsetScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index d18cbf90d..0585276f6 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -120,7 +120,7 @@ Item { Behavior on implicitWidth { Anim { - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } } diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index fdae94f7f..bc0b3312d 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -135,7 +135,7 @@ Item { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index d0001e238..cbb2e4f03 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -34,7 +34,7 @@ WlSessionLockSurface { properties: "implicitWidth,implicitHeight" to: lockContent.size duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } Anim { target: lockBg @@ -46,7 +46,7 @@ WlSessionLockSurface { property: "scale" to: 0 duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } Anim { target: content @@ -102,14 +102,14 @@ WlSessionLockSurface { property: "scale" to: 1 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } Anim { target: lockContent property: "rotation" to: 360 duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } } ParallelAnimation { @@ -117,7 +117,7 @@ WlSessionLockSurface { target: lockIcon property: "rotation" to: 360 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { target: lockIcon @@ -134,7 +134,7 @@ WlSessionLockSurface { property: "scale" to: 1 duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } Anim { target: lockBg @@ -146,14 +146,14 @@ WlSessionLockSurface { property: "implicitWidth" to: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } Anim { target: lockContent property: "implicitHeight" to: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index c93d830e6..1f876daf1 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -194,14 +194,14 @@ Item { Behavior on Layout.preferredWidth { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } Behavior on radius { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 131eeec27..57587f8f0 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -104,7 +104,7 @@ ColumnLayout { from: 0 to: 1 duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -127,7 +127,7 @@ ColumnLayout { Anim { property: "y" duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -139,7 +139,7 @@ ColumnLayout { Anim { property: "y" duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 2fdb6fb11..e7d77a20d 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -299,7 +299,7 @@ StyledRect { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index e32b84c6a..a0a964b01 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -172,7 +172,7 @@ Item { property: "x" to: (notif.x >= 0 ? Tokens.sizes.notifs.width : -Tokens.sizes.notifs.width) * 2 duration: Tokens.anim.durations.normal - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } PropertyAction { target: wrapper @@ -200,7 +200,6 @@ Item { component Anim: NumberAnimation { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 2d4c49010..63d27e692 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -36,7 +36,7 @@ StyledRect { Behavior on x { Anim { - easing.bezierCurve: Tokens.anim.curves.emphasizedDecel + easing: Tokens.anim.emphasizedDecel } } @@ -102,7 +102,7 @@ StyledRect { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -217,7 +217,7 @@ StyledRect { Behavior on sweepAngle { Anim { - easing.bezierCurve: Tokens.anim.curves.emphasizedDecel + easing: Tokens.anim.emphasizedDecel } } } @@ -287,8 +287,7 @@ StyledRect { } AnchorAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } @@ -331,8 +330,7 @@ StyledRect { transitions: Transition { AnchorAnimation { duration: Tokens.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Tokens.anim.curves.standard + easing: Tokens.anim.standard } } } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 3a82f0d05..521b803c0 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -117,7 +117,7 @@ Item { Behavior on Layout.preferredHeight { Anim { - easing.bezierCurve: Tokens.anim.curves.emphasized + easing: Tokens.anim.emphasized } } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 1c593e84f..2d7986a73 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -47,7 +47,7 @@ Item { Behavior on offsetScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index f211cfbe2..417bd95ad 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -24,7 +24,7 @@ Item { Behavior on offsetScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index aa80d068d..6394a0e66 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -125,7 +125,7 @@ StyledRect { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 320fd1a36..09da396c0 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -184,14 +184,14 @@ Item { Behavior on Layout.preferredWidth { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } Behavior on radius { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index a819f52b1..b9930409f 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -203,7 +203,7 @@ Item { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 80d091c33..9803155d0 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -130,7 +130,7 @@ LazyListView { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -141,14 +141,14 @@ LazyListView { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } Behavior on x { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } @@ -160,6 +160,6 @@ LazyListView { target: root.container property: "contentY" duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 2616d8054..2314dfe59 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -237,14 +237,14 @@ StyledRect { Behavior on rotation { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } Behavior on Layout.topMargin { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 32d8d678b..4aecf75f2 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -133,7 +133,7 @@ LazyListView { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -148,7 +148,7 @@ LazyListView { Behavior on x { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index e2d8f5a66..aff42f693 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -21,7 +21,7 @@ Item { Behavior on offsetScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 9d89fb8f7..7d7bcac88 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -196,7 +196,7 @@ Loader { Behavior on scale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index c8f87c363..f36444eb5 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -48,7 +48,7 @@ Item { Anim { property: "sidebarLerp" duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } }, Transition { @@ -57,7 +57,7 @@ Item { Anim { property: "sidebarLerp" duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } ] @@ -65,7 +65,7 @@ Item { Behavior on offsetScale { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 30176f29b..08427e7cb 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -102,7 +102,7 @@ StyledRect { Behavior on anchors.bottomMargin { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } @@ -120,7 +120,7 @@ StyledRect { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 97d88dde8..dca2d375c 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -132,14 +132,14 @@ StyledRect { property: "scale" to: 0.7 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } Anim { target: listOrControls property: "opacity" to: 0 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } } PropertyAction { @@ -159,14 +159,14 @@ StyledRect { property: "scale" to: 1 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { target: listOrControls property: "opacity" to: 1 duration: Tokens.anim.durations.small - easing.bezierCurve: Tokens.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } } @@ -219,13 +219,13 @@ StyledRect { from: 1 to: 0 duration: Tokens.anim.durations.large - easing.bezierCurve: Tokens.anim.curves.emphasizedAccel + easing: Tokens.anim.emphasizedAccel } Anim { from: 0 to: 1 duration: Tokens.anim.durations.extraLarge - easing.bezierCurve: Tokens.anim.curves.emphasizedDecel + easing: Tokens.anim.emphasizedDecel } } } diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 57b7ad64f..210b92afd 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -234,7 +234,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 65b5a6273..41eee68fe 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -159,12 +159,12 @@ StyledRect { inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) toggle: true radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial - radiusAnim.easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + radiusAnim.easing: Tokens.anim.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { duration: Tokens.anim.durations.expressiveFastSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveFastSpatial + easing: Tokens.anim.expressiveFastSpatial } } } diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 7c15bf827..517175bfd 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -112,7 +112,7 @@ Item { from: 0 to: 1 duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } ParallelAnimation { @@ -149,7 +149,7 @@ Item { Behavior on anchors.bottomMargin { Anim { duration: Tokens.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Tokens.anim.curves.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } } From ba959245785676745ef1763eb33b31999db42f4f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:44:03 +1000 Subject: [PATCH 292/409] fix: dashboard showXXX confs in the wrong file --- plugin/src/Caelestia/Config/dashboardconfig.hpp | 4 ++++ plugin/src/Caelestia/Config/tokens.hpp | 4 ---- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Config/dashboardconfig.hpp b/plugin/src/Caelestia/Config/dashboardconfig.hpp index d78bc1af4..d4b0ba893 100644 --- a/plugin/src/Caelestia/Config/dashboardconfig.hpp +++ b/plugin/src/Caelestia/Config/dashboardconfig.hpp @@ -26,6 +26,10 @@ class DashboardConfig : public ConfigObject { CONFIG_PROPERTY(bool, enabled, true) CONFIG_PROPERTY(bool, showOnHover, true) + CONFIG_PROPERTY(bool, showDashboard, true) + CONFIG_PROPERTY(bool, showMedia, true) + CONFIG_PROPERTY(bool, showPerformance, true) + CONFIG_PROPERTY(bool, showWeather, true) CONFIG_PROPERTY(int, mediaUpdateInterval, 500) CONFIG_PROPERTY(int, resourceUpdateInterval, 1000) CONFIG_PROPERTY(int, dragThreshold, 50) diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index a9d879b47..203ba9a8d 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -158,10 +158,6 @@ class DashboardTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, showDashboard, true) - CONFIG_PROPERTY(bool, showMedia, true) - CONFIG_PROPERTY(bool, showPerformance, true) - CONFIG_PROPERTY(bool, showWeather, true) CONFIG_PROPERTY(int, tabIndicatorHeight, 3) CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) CONFIG_PROPERTY(int, infoWidth, 200) From 9cf3ad40945c87d711e2be96c162cde82c627b14 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 21:54:27 +1000 Subject: [PATCH 293/409] fix: broken config uses + move gif speed adjustments to Config.general --- modules/dashboard/Media.qml | 2 +- modules/dashboard/dash/Media.qml | 2 +- modules/dashboard/dash/Resources.qml | 2 +- modules/session/Content.qml | 2 +- plugin/src/Caelestia/Config/appearanceconfig.hpp | 2 -- plugin/src/Caelestia/Config/generalconfig.hpp | 2 ++ 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 29de39343..3cc17d577 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -402,7 +402,7 @@ Item { height: visualiser.height * 0.75 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Tokens.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 07d2352e6..0788d3806 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -213,7 +213,7 @@ Item { anchors.margins: Tokens.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / Tokens.anim.mediaGifSpeedAdjustment // qmllint disable unresolved-type + speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index f492cc58d..17d0f4a20 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -53,7 +53,7 @@ Row { anchors.bottom: icon.top anchors.bottomMargin: Tokens.spacing.small - implicitWidth: Tokens.sizes.dashboard.resourceProgessThickness + implicitWidth: Tokens.sizes.dashboard.resourceProgressThickness color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) radius: Tokens.rounding.full diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 45fc0283d..48dd9c5b2 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -53,7 +53,7 @@ Column { playing: visible asynchronous: true - speed: Tokens.anim.sessionGifSpeed + speed: Config.general.sessionGifSpeed source: Paths.absolutePath(Config.paths.sessionGif) } diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index c13d07792..c58105a71 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -204,8 +204,6 @@ class AppearanceAnim : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) - CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) CONFIG_SUBOBJECT(AnimDurations, durations) public: diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp index 88bb7ebfd..24982e61b 100644 --- a/plugin/src/Caelestia/Config/generalconfig.hpp +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -53,6 +53,8 @@ class GeneralConfig : public ConfigObject { CONFIG_PROPERTY(QString, logo) CONFIG_PROPERTY(QStringList, excludedScreens) + CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) + CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) CONFIG_SUBOBJECT(GeneralApps, apps) CONFIG_SUBOBJECT(GeneralIdle, idle) CONFIG_SUBOBJECT(GeneralBattery, battery) From f65723ad719b89adc8a0341d9cb1e1373168b28f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:11:30 +1000 Subject: [PATCH 294/409] feat: never destroy config singletons There are lots of issues with it, so instead give ownership to C++ and don't destroy on reloads (this does mean it does not reload, but it doesn't need to anyways) --- plugin/src/Caelestia/Config/config.cpp | 48 +++++--------------------- plugin/src/Caelestia/Config/config.hpp | 2 -- plugin/src/Caelestia/Config/tokens.cpp | 26 +++----------- plugin/src/Caelestia/Config/tokens.hpp | 2 -- 4 files changed, 13 insertions(+), 65 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index da9fdcbea..410677350 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -9,8 +9,6 @@ namespace caelestia::config { namespace { -GlobalConfig* s_instance = nullptr; - QString configDir() { return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); } @@ -36,14 +34,7 @@ GlobalConfig::GlobalConfig(QObject* parent) , m_sidebar(new SidebarConfig(this)) , m_services(new ServiceConfig(this)) , m_paths(new UserPaths(this)) { - // Set global instance - s_instance = this; - setupFileBackend(configDir() + QStringLiteral("shell.json")); - - // If TokenConfig was created before us, bind now - if (TokenConfig::instance()) - bindAppearanceTokens(); } GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent) @@ -72,13 +63,10 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObj syncFromGlobal(fallback); } -GlobalConfig::~GlobalConfig() { - if (s_instance == this) - s_instance = nullptr; -} - GlobalConfig* GlobalConfig::instance() { - return s_instance; + static GlobalConfig instance; + instance.bindAppearanceTokens(); + return &instance; } GlobalConfig* GlobalConfig::defaults() { @@ -91,14 +79,8 @@ void GlobalConfig::bindAppearanceTokens() { if (m_tokensBound) return; - auto* tokens = TokenConfig::instance(); - if (!tokens) { - qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: TokenConfig not yet available"; - return; - } - qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: binding appearance to token values"; - auto* tokenAppearance = tokens->appearance(); + auto* const tokenAppearance = TokenConfig::instance()->appearance(); m_appearance->rounding()->bindTokens(tokenAppearance->rounding()); m_appearance->spacing()->bindTokens(tokenAppearance->spacing()); m_appearance->padding()->bindTokens(tokenAppearance->padding()); @@ -107,17 +89,9 @@ void GlobalConfig::bindAppearanceTokens() { m_tokensBound = true; } -GlobalConfig* GlobalConfig::create(QQmlEngine* engine, QJSEngine* jsEngine) { - auto* config = new GlobalConfig(engine); - - // Ensure TokenConfig is created — appearance computed properties depend on token binding. - if (!TokenConfig::instance()) - TokenConfig::create(engine, jsEngine); - - // Bind now that both singletons exist - config->bindAppearanceTokens(); - - return config; +GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { + QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); + return instance(); } // Config (attached type) @@ -140,8 +114,7 @@ void Config::connectScope() { const Type* Config::name() const { \ if (m_scope && m_scope->config()) \ return m_scope->config()->name(); \ - auto* global = GlobalConfig::instance(); \ - return global ? global->name() : nullptr; \ + return GlobalConfig::instance()->name(); \ } CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) @@ -165,11 +138,6 @@ CONFIG_ATTACHED_GETTER(UserPaths, paths) #undef CONFIG_ATTACHED_GETTER Config* Config::qmlAttachedProperties(QObject* object) { - // Ensure GlobalConfig singleton is created before any attached property access - if (!GlobalConfig::instance()) { - if (auto* engine = qmlEngine(object)) - engine->singletonInstance("Caelestia.Config", "GlobalConfig"); - } return new Config(ConfigScope::find(object), object); } diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index ddced1317..d683df11b 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -50,8 +50,6 @@ class GlobalConfig : public RootConfig { [[nodiscard]] Q_INVOKABLE GlobalConfig* defaults(); static GlobalConfig* create(QQmlEngine*, QJSEngine*); - ~GlobalConfig() override; - void bindAppearanceTokens(); private: diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index bf8082496..902dbfc49 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -9,8 +9,6 @@ namespace caelestia::config { namespace { -TokenConfig* s_instance = nullptr; - QString configDir() { return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); } @@ -21,13 +19,7 @@ TokenConfig::TokenConfig(QObject* parent) : RootConfig(parent) , m_appearance(new AppearanceTokens(this)) , m_sizes(new SizeTokens(this)) { - s_instance = this; - setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); - - // If GlobalConfig was created before us, trigger its binding - if (auto* global = GlobalConfig::instance()) - global->bindAppearanceTokens(); } TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent) @@ -41,13 +33,9 @@ TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject syncFromGlobal(fallback); } -TokenConfig::~TokenConfig() { - if (s_instance == this) - s_instance = nullptr; -} - TokenConfig* TokenConfig::instance() { - return s_instance; + static TokenConfig instance; + return &instance; } TokenConfig* TokenConfig::defaults() { @@ -56,8 +44,9 @@ TokenConfig* TokenConfig::defaults() { return m_defaults; } -TokenConfig* TokenConfig::create(QQmlEngine* engine, QJSEngine*) { - return new TokenConfig(engine); +TokenConfig* TokenConfig::create(QQmlEngine*, QJSEngine*) { + QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); + return instance(); } // Tokens (attached type) @@ -132,11 +121,6 @@ const SizeTokens* Tokens::sizes() const { } Tokens* Tokens::qmlAttachedProperties(QObject* object) { - // Ensure GlobalConfig singleton is created before any attached property access - if (!GlobalConfig::instance()) { - if (auto* engine = qmlEngine(object)) - engine->singletonInstance("Caelestia.Config", "GlobalConfig"); - } return new Tokens(ConfigScope::find(object), object); } diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 203ba9a8d..69740aab0 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -332,8 +332,6 @@ class TokenConfig : public RootConfig { [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); static TokenConfig* create(QQmlEngine*, QJSEngine*); - ~TokenConfig() override; - private: friend class MonitorConfigManager; explicit TokenConfig(QObject* parent = nullptr); From 71dd7b4db8348319d48b30ef0abd0893fae1069f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:17:24 +1000 Subject: [PATCH 295/409] chore: format QML imports --- components/ConnectionHeader.qml | 2 +- components/ConnectionInfoSection.qml | 2 +- components/MaterialIcon.qml | 2 +- components/PropertyRow.qml | 2 +- components/SectionContainer.qml | 2 +- components/SectionHeader.qml | 2 +- components/StateLayer.qml | 2 +- components/StyledText.qml | 2 +- components/controls/CircularIndicator.qml | 2 +- components/controls/CircularProgress.qml | 2 +- components/controls/CollapsibleSection.qml | 2 +- components/controls/CustomSpinBox.qml | 2 +- components/controls/FilledSlider.qml | 2 +- components/controls/IconButton.qml | 2 +- components/controls/IconTextButton.qml | 2 +- components/controls/Menu.qml | 2 +- components/controls/SpinBoxRow.qml | 2 +- components/controls/SplitButton.qml | 2 +- components/controls/SplitButtonRow.qml | 2 +- components/controls/StyledInputField.qml | 2 +- components/controls/StyledRadioButton.qml | 2 +- components/controls/StyledScrollBar.qml | 2 +- components/controls/StyledSlider.qml | 2 +- components/controls/StyledSwitch.qml | 2 +- components/controls/StyledTextField.qml | 2 +- components/controls/SwitchRow.qml | 2 +- components/controls/TextButton.qml | 2 +- components/controls/ToggleButton.qml | 2 +- components/controls/ToggleRow.qml | 2 +- components/controls/Tooltip.qml | 2 +- components/effects/InnerBorder.qml | 2 +- components/filedialog/CurrentItem.qml | 2 +- components/filedialog/DialogButtons.qml | 2 +- components/filedialog/FolderContents.qml | 2 +- components/filedialog/HeaderBar.qml | 2 +- components/filedialog/Sidebar.qml | 2 +- components/widgets/ExtraIndicator.qml | 2 +- modules/IdleMonitors.qml | 2 +- modules/areapicker/Picker.qml | 2 +- modules/background/Background.qml | 2 +- modules/background/DesktopClock.qml | 2 +- modules/background/Visualiser.qml | 2 +- modules/background/Wallpaper.qml | 2 +- modules/bar/Bar.qml | 2 +- modules/bar/BarWrapper.qml | 2 +- modules/bar/components/ActiveWindow.qml | 2 +- modules/bar/components/Clock.qml | 2 +- modules/bar/components/OsIcon.qml | 2 +- modules/bar/components/Power.qml | 2 +- modules/bar/components/Settings.qml | 2 +- modules/bar/components/SettingsIcon.qml | 2 +- modules/bar/components/StatusIcons.qml | 2 +- modules/bar/components/Tray.qml | 2 +- modules/bar/components/TrayItem.qml | 2 +- modules/bar/components/workspaces/ActiveIndicator.qml | 2 +- modules/bar/components/workspaces/OccupiedBg.qml | 2 +- modules/bar/components/workspaces/SpecialWorkspaces.qml | 2 +- modules/bar/components/workspaces/Workspace.qml | 2 +- modules/bar/components/workspaces/Workspaces.qml | 2 +- modules/bar/popouts/ActiveWindow.qml | 2 +- modules/bar/popouts/Audio.qml | 2 +- modules/bar/popouts/Battery.qml | 2 +- modules/bar/popouts/Bluetooth.qml | 2 +- modules/bar/popouts/ClipWrapper.qml | 2 +- modules/bar/popouts/Content.qml | 2 +- modules/bar/popouts/LockStatus.qml | 2 +- modules/bar/popouts/Network.qml | 2 +- modules/bar/popouts/TrayMenu.qml | 2 +- modules/bar/popouts/WirelessPassword.qml | 2 +- modules/bar/popouts/Wrapper.qml | 2 +- modules/bar/popouts/kblayout/KbLayout.qml | 2 +- modules/controlcenter/ControlCenter.qml | 2 +- modules/controlcenter/NavRail.qml | 2 +- modules/controlcenter/Panes.qml | 2 +- modules/controlcenter/WindowTitle.qml | 2 +- modules/controlcenter/appearance/AppearancePane.qml | 2 +- modules/controlcenter/appearance/sections/AnimationsSection.qml | 2 +- modules/controlcenter/appearance/sections/BackgroundSection.qml | 2 +- modules/controlcenter/appearance/sections/BorderSection.qml | 2 +- .../controlcenter/appearance/sections/ColorSchemeSection.qml | 2 +- .../controlcenter/appearance/sections/ColorVariantSection.qml | 2 +- modules/controlcenter/appearance/sections/FontsSection.qml | 2 +- modules/controlcenter/appearance/sections/ScalesSection.qml | 2 +- modules/controlcenter/appearance/sections/ThemeModeSection.qml | 2 +- .../controlcenter/appearance/sections/TransparencySection.qml | 2 +- modules/controlcenter/audio/AudioPane.qml | 2 +- modules/controlcenter/bluetooth/BtPane.qml | 2 +- modules/controlcenter/bluetooth/Details.qml | 2 +- modules/controlcenter/bluetooth/DeviceList.qml | 2 +- modules/controlcenter/bluetooth/Settings.qml | 2 +- modules/controlcenter/components/ConnectedButtonGroup.qml | 2 +- modules/controlcenter/components/DeviceDetails.qml | 2 +- modules/controlcenter/components/DeviceList.qml | 2 +- modules/controlcenter/components/ReadonlySlider.qml | 2 +- modules/controlcenter/components/SettingsHeader.qml | 2 +- modules/controlcenter/components/SliderInput.qml | 2 +- modules/controlcenter/components/SplitPaneLayout.qml | 2 +- modules/controlcenter/components/SplitPaneWithDetails.qml | 2 +- modules/controlcenter/components/WallpaperGrid.qml | 2 +- modules/controlcenter/dashboard/DashboardPane.qml | 2 +- modules/controlcenter/dashboard/GeneralSection.qml | 2 +- modules/controlcenter/dashboard/PerformanceSection.qml | 2 +- modules/controlcenter/launcher/LauncherPane.qml | 2 +- modules/controlcenter/launcher/Settings.qml | 2 +- modules/controlcenter/network/EthernetDetails.qml | 2 +- modules/controlcenter/network/EthernetList.qml | 2 +- modules/controlcenter/network/EthernetPane.qml | 2 +- modules/controlcenter/network/EthernetSettings.qml | 2 +- modules/controlcenter/network/NetworkSettings.qml | 2 +- modules/controlcenter/network/NetworkingPane.qml | 2 +- modules/controlcenter/network/VpnDetails.qml | 2 +- modules/controlcenter/network/VpnList.qml | 2 +- modules/controlcenter/network/VpnSettings.qml | 2 +- modules/controlcenter/network/WirelessDetails.qml | 2 +- modules/controlcenter/network/WirelessList.qml | 2 +- modules/controlcenter/network/WirelessPane.qml | 2 +- modules/controlcenter/network/WirelessPasswordDialog.qml | 2 +- modules/controlcenter/network/WirelessSettings.qml | 2 +- modules/controlcenter/notifications/NotificationsPane.qml | 2 +- modules/controlcenter/taskbar/TaskbarPane.qml | 2 +- modules/dashboard/Content.qml | 2 +- modules/dashboard/Dash.qml | 2 +- modules/dashboard/LyricMenu.qml | 2 +- modules/dashboard/LyricsView.qml | 2 +- modules/dashboard/Media.qml | 2 +- modules/dashboard/Performance.qml | 2 +- modules/dashboard/Tabs.qml | 2 +- modules/dashboard/WeatherTab.qml | 2 +- modules/dashboard/Wrapper.qml | 2 +- modules/dashboard/dash/Calendar.qml | 2 +- modules/dashboard/dash/DateTime.qml | 2 +- modules/dashboard/dash/Media.qml | 2 +- modules/dashboard/dash/Resources.qml | 2 +- modules/dashboard/dash/SmallWeather.qml | 2 +- modules/dashboard/dash/User.qml | 2 +- modules/drawers/ContentWindow.qml | 2 +- modules/drawers/Drawers.qml | 2 +- modules/drawers/Interactions.qml | 2 +- modules/drawers/Panels.qml | 2 +- modules/launcher/AppList.qml | 2 +- modules/launcher/Content.qml | 2 +- modules/launcher/ContentList.qml | 2 +- modules/launcher/WallpaperList.qml | 2 +- modules/launcher/Wrapper.qml | 2 +- modules/launcher/items/ActionItem.qml | 2 +- modules/launcher/items/AppItem.qml | 2 +- modules/launcher/items/CalcItem.qml | 2 +- modules/launcher/items/SchemeItem.qml | 2 +- modules/launcher/items/VariantItem.qml | 2 +- modules/launcher/items/WallpaperItem.qml | 2 +- modules/launcher/services/Actions.qml | 2 +- modules/lock/Center.qml | 2 +- modules/lock/Content.qml | 2 +- modules/lock/Fetch.qml | 2 +- modules/lock/InputField.qml | 2 +- modules/lock/LockSurface.qml | 2 +- modules/lock/Media.qml | 2 +- modules/lock/NotifDock.qml | 2 +- modules/lock/NotifGroup.qml | 2 +- modules/lock/Resources.qml | 2 +- modules/lock/WeatherInfo.qml | 2 +- modules/notifications/Content.qml | 2 +- modules/notifications/Notification.qml | 2 +- modules/osd/Content.qml | 2 +- modules/osd/Wrapper.qml | 2 +- modules/session/Content.qml | 2 +- modules/session/Wrapper.qml | 2 +- modules/sidebar/Content.qml | 2 +- modules/sidebar/Notif.qml | 2 +- modules/sidebar/NotifActionList.qml | 2 +- modules/sidebar/NotifDock.qml | 2 +- modules/sidebar/NotifDockList.qml | 2 +- modules/sidebar/NotifGroup.qml | 2 +- modules/sidebar/NotifGroupList.qml | 2 +- modules/sidebar/Wrapper.qml | 2 +- modules/utilities/Content.qml | 2 +- modules/utilities/RecordingDeleteModal.qml | 2 +- modules/utilities/Wrapper.qml | 2 +- modules/utilities/cards/IdleInhibit.qml | 2 +- modules/utilities/cards/Record.qml | 2 +- modules/utilities/cards/RecordingList.qml | 2 +- modules/utilities/cards/Toggles.qml | 2 +- modules/utilities/toasts/ToastItem.qml | 2 +- modules/utilities/toasts/Toasts.qml | 2 +- modules/windowinfo/Buttons.qml | 2 +- modules/windowinfo/Details.qml | 2 +- modules/windowinfo/Preview.qml | 2 +- modules/windowinfo/WindowInfo.qml | 2 +- services/Audio.qml | 2 +- services/Brightness.qml | 2 +- services/Colours.qml | 2 +- services/GameMode.qml | 2 +- services/Hypr.qml | 2 +- services/NetworkUsage.qml | 2 +- services/NotifData.qml | 2 +- services/Notifs.qml | 2 +- services/Players.qml | 2 +- services/Wallpapers.qml | 2 +- 198 files changed, 198 insertions(+), 198 deletions(-) diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml index 36886803f..691fd3c50 100644 --- a/components/ConnectionHeader.qml +++ b/components/ConnectionHeader.qml @@ -1,7 +1,7 @@ import QtQuick import QtQuick.Layouts -import qs.components import Caelestia.Config +import qs.components ColumnLayout { id: root diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml index bac3cc7cb..d94c3a6e8 100644 --- a/components/ConnectionInfoSection.qml +++ b/components/ConnectionInfoSection.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/components/MaterialIcon.qml b/components/MaterialIcon.qml index e85d39be6..739c50bae 100644 --- a/components/MaterialIcon.qml +++ b/components/MaterialIcon.qml @@ -1,5 +1,5 @@ -import qs.services import Caelestia.Config +import qs.services StyledText { property real fill diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml index 7f1a68bdc..4d3025095 100644 --- a/components/PropertyRow.qml +++ b/components/PropertyRow.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 05b4523c7..7aa3f13f1 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml index 107c6d368..09364143f 100644 --- a/components/SectionHeader.qml +++ b/components/SectionHeader.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/components/StateLayer.qml b/components/StateLayer.qml index 545a7583d..a18e41f98 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -1,6 +1,6 @@ import QtQuick -import qs.services import Caelestia.Config +import qs.services MouseArea { id: root diff --git a/components/StyledText.qml b/components/StyledText.qml index 83dc146d8..d3624c58c 100644 --- a/components/StyledText.qml +++ b/components/StyledText.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.services import Caelestia.Config +import qs.services Text { id: root diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index 8bfee428e..763b112ca 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -1,9 +1,9 @@ import ".." import QtQuick import QtQuick.Templates +import Caelestia.Config import Caelestia.Internal import qs.services -import Caelestia.Config BusyIndicator { id: root diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index 4cd5f9f7a..9e3122807 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,8 +1,8 @@ import ".." import QtQuick import QtQuick.Shapes -import qs.services import Caelestia.Config +import qs.services Shape { id: root diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 95aae8eee..75a6b9aa1 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -1,9 +1,9 @@ import ".." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index 6adb137c8..b5f4f8ddd 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound import ".." import QtQuick import QtQuick.Layouts -import qs.services import Caelestia.Config +import qs.services RowLayout { id: root diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index c71f65e98..e8593b96d 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -2,8 +2,8 @@ import ".." import "../effects" import QtQuick import QtQuick.Templates -import qs.services import Caelestia.Config +import qs.services Slider { id: root diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index 609730943..d0e6ac318 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -1,7 +1,7 @@ import ".." import QtQuick -import qs.services import Caelestia.Config +import qs.services StyledRect { id: root diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index 1fbf74ebc..919c1f1d1 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -1,8 +1,8 @@ import ".." import QtQuick import QtQuick.Layouts -import qs.services import Caelestia.Config +import qs.services StyledRect { id: root diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 966837ead..3f4878e6b 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -4,8 +4,8 @@ import ".." import "../effects" import QtQuick import QtQuick.Layouts -import qs.services import Caelestia.Config +import qs.services Elevation { id: root diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index c5e5c0829..d15414f0f 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -1,9 +1,9 @@ import ".." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index cdea38f02..ba75a6781 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -1,8 +1,8 @@ import ".." import QtQuick import QtQuick.Layouts -import qs.services import Caelestia.Config +import qs.services Row { id: root diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index 9082ce1de..c097bfd53 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import ".." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index 7cbf26c57..38b1208b1 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import ".." import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index a2e993277..a141b2451 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Templates +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config RadioButton { id: root diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index 4cd86a4a3..aaeaff29d 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -1,8 +1,8 @@ import ".." import QtQuick import QtQuick.Templates -import qs.services import Caelestia.Config +import qs.services ScrollBar { id: root diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index 09349635b..f169691b4 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Templates +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Slider { id: root diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index be7eb9c55..a3439130d 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -2,8 +2,8 @@ import ".." import QtQuick import QtQuick.Shapes import QtQuick.Templates -import qs.services import Caelestia.Config +import qs.services Switch { id: root diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 9a810f797..7eec78b84 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound import ".." import QtQuick import QtQuick.Controls -import qs.services import Caelestia.Config +import qs.services TextField { id: root diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 01a42dd94..8b2d617dd 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -1,9 +1,9 @@ import ".." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index a74c49e11..159d63b4a 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -1,7 +1,7 @@ import ".." import QtQuick -import qs.services import Caelestia.Config +import qs.services StyledRect { id: root diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index fce6215b7..0ece6d4ef 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -3,10 +3,10 @@ pragma ComponentBehavior: Bound import ".." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml index 01e9cc0e2..beb6a15cb 100644 --- a/components/controls/ToggleRow.qml +++ b/components/controls/ToggleRow.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls -import Caelestia.Config RowLayout { id: root diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index 06b292de5..d00c144ba 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -1,9 +1,9 @@ import ".." import QtQuick import QtQuick.Controls +import Caelestia.Config import qs.components.effects import qs.services -import Caelestia.Config Popup { id: root diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index 2770c9bc8..2723a04e1 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound import ".." import QtQuick import QtQuick.Effects -import qs.services import Caelestia.Config +import qs.services StyledRect { property alias innerRadius: maskInner.radius diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index d410b13c4..036828e55 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -1,8 +1,8 @@ import ".." import QtQuick import QtQuick.Shapes -import qs.services import Caelestia.Config +import qs.services Item { id: root diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index 94cbd72d4..abd49d339 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -1,7 +1,7 @@ import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index ebd5098dc..b5f34a487 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -4,13 +4,13 @@ import QtQuick import QtQuick.Effects import QtQuick.Layouts import Quickshell +import Caelestia.Config import Caelestia.Models import qs.components import qs.components.controls import qs.components.filedialog import qs.components.images import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index c114bbd85..7bd66b2cd 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -3,8 +3,8 @@ pragma ComponentBehavior: Bound import ".." import QtQuick import QtQuick.Layouts -import qs.services import Caelestia.Config +import qs.services StyledRect { id: root diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index 885185c93..ad10afbaf 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.filedialog import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index 1c766bf0c..1fa34837a 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -1,8 +1,8 @@ import ".." import "../effects" import QtQuick -import qs.services import Caelestia.Config +import qs.services StyledRect { required property int extra diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml index c396bc8b1..839cd6f63 100644 --- a/modules/IdleMonitors.qml +++ b/modules/IdleMonitors.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import "lock" import Quickshell import Quickshell.Wayland +import Caelestia.Config import Caelestia.Internal import qs.services -import Caelestia.Config Scope { id: root diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 6399981fd..30c47afdc 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -6,9 +6,9 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import Caelestia +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config MouseArea { id: root diff --git a/modules/background/Background.qml b/modules/background/Background.qml index 547fdc9ba..fde27ef45 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Wayland +import Caelestia.Config import qs.components.containers import qs.services -import Caelestia.Config Variants { model: Config.background.enabled ? Screens.screens : [] diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index c184df784..b7d22417d 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index ebb28dfab..450558792 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -3,11 +3,11 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import Quickshell +import Caelestia.Config import Caelestia.Internal import Caelestia.Services import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index f226d062a..b5eac8dd1 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia.Config import qs.components import qs.components.filedialog import qs.components.images import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 5d127f57e..27483d9f2 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -6,9 +6,9 @@ import "components/workspaces" import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 1f10d366f..7c8c5973d 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.components import Caelestia.Config +import qs.components import qs.utils import qs.modules.bar.popouts as BarPopouts diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 5979edd4d..6bd5ef3fd 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 693659c53..d6528c641 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index 47a8c88fa..94d1a1c4d 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -1,8 +1,8 @@ import QtQuick +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index f3f4706ae..8fca7adce 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -1,7 +1,7 @@ import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml index f060225c6..5424e4145 100644 --- a/modules/bar/components/Settings.qml +++ b/modules/bar/components/Settings.qml @@ -1,7 +1,7 @@ import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.modules.controlcenter Item { diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml index f060225c6..5424e4145 100644 --- a/modules/bar/components/SettingsIcon.qml +++ b/modules/bar/components/SettingsIcon.qml @@ -1,7 +1,7 @@ import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.modules.controlcenter Item { diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index 3b11036b6..900e55747 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -5,9 +5,9 @@ import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth import Quickshell.Services.UPower +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.utils StyledRect { diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 5adc5e572..ceb5e180a 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Services.SystemTray +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index 1320eb2aa..fefb532c9 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell.Services.SystemTray +import Caelestia.Config import qs.components.effects import qs.services -import Caelestia.Config import qs.utils MouseArea { diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index e9a32b45f..ec782e3bf 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -1,8 +1,8 @@ import QtQuick +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index c54db55c6..2bd3c8cb8 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index a825c1e54..7f43e3a74 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -4,10 +4,10 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Hyprland +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index e57f6f5e4..39c55ec66 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index ae43f9749..e7f617c64 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -4,9 +4,9 @@ import QtQuick import QtQuick.Effects import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledClippingRect { id: root diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index e0ef85d9d..59aabce13 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -2,9 +2,9 @@ import QtQuick import QtQuick.Layouts import Quickshell.Wayland import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 50bf1915d..e8dc92032 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -4,10 +4,10 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.Pipewire +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index cbcdc7152..fdfb1089c 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell.Services.UPower +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Column { id: root diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index d5cc0f619..931ad222b 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -4,10 +4,10 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index 68c301a8e..30fd4aaa1 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.components import Caelestia.Config +import qs.components import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others Item { diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 381d3ef0d..8b5d0db53 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -4,8 +4,8 @@ import "./kblayout" import QtQuick import Quickshell import Quickshell.Services.SystemTray -import qs.components import Caelestia.Config +import qs.components Item { id: root diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml index 4d646a0cd..caab7487f 100644 --- a/modules/bar/popouts/LockStatus.qml +++ b/modules/bar/popouts/LockStatus.qml @@ -1,7 +1,7 @@ import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { spacing: Tokens.spacing.small diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index cec936d9f..956d4bac7 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -3,10 +3,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 9576049bd..9dfcb4a76 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -4,9 +4,9 @@ import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StackView { id: root diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index f1a79366b..0c3bae708 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -3,10 +3,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index d99ffdabd..48fe05f87 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -4,9 +4,9 @@ import QtQuick import Quickshell import Quickshell.Hyprland import Quickshell.Wayland +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.modules.controlcenter import qs.modules.windowinfo diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 265d3ab89..2d6f94120 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index b2c07e52e..9bf097f76 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -3,10 +3,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index fe80493ae..5a0ba37bb 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.modules.controlcenter Item { diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index c87185283..c3a8e21be 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -11,9 +11,9 @@ import "dashboard" import QtQuick import QtQuick.Layouts import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.modules.controlcenter ClippingRectangle { diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index 1993446f8..70ea22ea4 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -1,8 +1,8 @@ import QtQuick import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 472c654a8..1c787f36e 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -8,6 +8,7 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import Caelestia.Models import qs.components import qs.components.containers @@ -15,7 +16,6 @@ import qs.components.controls import qs.components.effects import qs.components.images import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index e5e973e24..412441abc 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -4,11 +4,11 @@ import ".." import "../../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 95670d247..3f31f10b4 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -4,11 +4,11 @@ import ".." import "../../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index e92a28da2..7dbd2dbe6 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -4,11 +4,11 @@ import ".." import "../../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 69dff698f..90eaeb25a 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -5,11 +5,11 @@ import "../../../launcher/services" import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { title: qsTr("Color scheme") diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index 8fbd80d9a..a5de4ad8a 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -5,11 +5,11 @@ import "../../../launcher/services" import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { title: qsTr("Color variant") diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 562ea8d5a..1791fc69b 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -4,11 +4,11 @@ import ".." import "../../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index 8113f28c7..dac6226e8 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -4,11 +4,11 @@ import ".." import "../../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { id: root diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index 8857da56e..aab53b8bd 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import QtQuick +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { title: qsTr("Theme mode") diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 2dc91b65c..f2f15ba13 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -4,11 +4,11 @@ import ".." import "../../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config CollapsibleSection { id: root diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 13885656e..a1da681e9 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -5,12 +5,12 @@ import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index d57942541..b2b9c0f69 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -6,10 +6,10 @@ import "." import QtQuick import Quickshell.Bluetooth import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls -import Caelestia.Config SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 42ceace2d..a398c34d5 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -5,12 +5,12 @@ import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils StyledFlickable { diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index ca76c8c1e..9833eacd6 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -6,11 +6,11 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config import qs.utils DeviceList { diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index 2588a28c4..d5c9a28a7 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -5,11 +5,11 @@ import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index b4ddfc22e..90f9614a8 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -1,11 +1,11 @@ import ".." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index 5fc4d0ed9..075024f6c 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -3,11 +3,11 @@ pragma ComponentBehavior: Bound import ".." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects -import Caelestia.Config Item { id: root diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 8a65fc0c9..f02cc3c08 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -4,11 +4,11 @@ import ".." import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml index e1bff1378..8cd493f40 100644 --- a/modules/controlcenter/components/ReadonlySlider.qml +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -2,10 +2,10 @@ import ".." import "../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index 425a9e8ee..c6ccbb67a 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts -import qs.components import Caelestia.Config +import qs.components Item { id: root diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 1c1f29bef..73fd00ae6 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index cff20ec72..64c7c9fc8 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.effects -import Caelestia.Config RowLayout { id: root diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index dcc472d9e..ad1c444d4 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -4,10 +4,10 @@ import ".." import QtQuick import QtQuick.Layouts import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.effects -import Caelestia.Config Item { id: root diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index b85ec556a..efab91452 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import QtQuick +import Caelestia.Config import Caelestia.Models import qs.components import qs.components.controls import qs.components.effects import qs.components.images import qs.services -import Caelestia.Config GridView { id: root diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index d2e95801e..83bc1e169 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -6,12 +6,12 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml index 6294a05d5..be67eab27 100644 --- a/modules/controlcenter/dashboard/GeneralSection.qml +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -2,10 +2,10 @@ import ".." import "../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config SectionContainer { id: root diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml index 1a0b6482c..ea6c68b3e 100644 --- a/modules/controlcenter/dashboard/PerformanceSection.qml +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -3,10 +3,10 @@ import "../components" import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config SectionContainer { id: root diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 51989c630..83c2232b4 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -9,12 +9,12 @@ import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Caelestia +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index ec29aeb2f..61bab0c15 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -4,11 +4,11 @@ import ".." import "../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index e6128a317..1daeb4a29 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -4,12 +4,12 @@ import ".." import "../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config DeviceDetails { id: root diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 440482652..595233c5d 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -4,11 +4,11 @@ import ".." import "../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config DeviceList { id: root diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index a2699c789..d66620f1f 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -4,9 +4,9 @@ import ".." import "../components" import QtQuick import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers -import Caelestia.Config SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 7a37316c2..f13b6fca8 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -4,11 +4,11 @@ import ".." import "../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 6a1bc6df0..22d85188b 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -5,12 +5,12 @@ import "../components" import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 2829a1148..e02bfafbc 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -7,12 +7,12 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 9906ef37e..3ea85d32a 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -5,12 +5,12 @@ import "../components" import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils DeviceDetails { diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 05054876a..e0376fc63 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -5,11 +5,11 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index d8dd09d18..e0f0e7f50 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -6,12 +6,12 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index b6d7e77e1..e39744caa 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -5,12 +5,12 @@ import "../components" import "." import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils DeviceDetails { diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 878986d1b..9eefb1729 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -6,12 +6,12 @@ import "." import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils DeviceList { diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 02af096db..436891765 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -4,9 +4,9 @@ import ".." import "../components" import QtQuick import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers -import Caelestia.Config SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 4222ddf49..f5b3afa97 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -5,12 +5,12 @@ import "." import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index bce381bae..f3f1fd4b7 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -4,11 +4,11 @@ import ".." import "../components" import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml index 5768e4789..919a2549d 100644 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -6,12 +6,12 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 7ec577753..5715b8e27 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -6,12 +6,12 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 6fc7faeac..771d4bb40 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -4,9 +4,9 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.filedialog -import Caelestia.Config Item { id: root diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index cc6c51716..6785ede8a 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -1,9 +1,9 @@ import "dash" import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.filedialog import qs.services -import Caelestia.Config GridLayout { id: root diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml index b311ff7c8..1e5461f56 100644 --- a/modules/dashboard/LyricMenu.qml +++ b/modules/dashboard/LyricMenu.qml @@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml index b0ef99a3a..6ccbd26ea 100644 --- a/modules/dashboard/LyricsView.qml +++ b/modules/dashboard/LyricsView.qml @@ -1,10 +1,10 @@ import QtQuick import QtQuick.Effects import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.services -import Caelestia.Config StyledListView { id: root diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 3cc17d577..e46388430 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -5,11 +5,11 @@ import QtQuick.Layouts import QtQuick.Shapes import Quickshell import Quickshell.Services.Mpris +import Caelestia.Config import Caelestia.Services import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 13ab57a75..0c405fd3b 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -2,11 +2,11 @@ import QtQuick import QtQuick.Controls import QtQuick.Layouts import Quickshell.Services.UPower +import Caelestia.Config import Caelestia.Internal import qs.components import qs.components.misc import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 54e80c7af..fc5d7ec15 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -4,10 +4,10 @@ import QtQuick import QtQuick.Controls import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/dashboard/WeatherTab.qml b/modules/dashboard/WeatherTab.qml index 78be9ad7b..312372b28 100644 --- a/modules/dashboard/WeatherTab.qml +++ b/modules/dashboard/WeatherTab.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 7a810dc00..dd097e7b6 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Caelestia +import Caelestia.Config import qs.components import qs.components.filedialog -import Caelestia.Config import qs.utils Item { diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index 38d2d563b..990e380d5 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -3,11 +3,11 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config CustomMouseArea { id: root diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 2df22d150..248d41f3e 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 0788d3806..2dd8a4af6 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,9 +1,9 @@ import QtQuick import QtQuick.Shapes +import Caelestia.Config import Caelestia.Services import qs.components import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index 17d0f4a20..df5603691 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -1,8 +1,8 @@ import QtQuick +import Caelestia.Config import qs.components import qs.components.misc import qs.services -import Caelestia.Config Row { id: root diff --git a/modules/dashboard/dash/SmallWeather.qml b/modules/dashboard/dash/SmallWeather.qml index dbc5ac3ef..998c7125a 100644 --- a/modules/dashboard/dash/SmallWeather.qml +++ b/modules/dashboard/dash/SmallWeather.qml @@ -1,7 +1,7 @@ import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 83ddcb2e1..791297fff 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -1,10 +1,10 @@ import QtQuick +import Caelestia.Config import qs.components import qs.components.effects import qs.components.filedialog import qs.components.images import qs.services -import Caelestia.Config import qs.utils Row { diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 8d6654775..2842a33f0 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -7,10 +7,10 @@ import Quickshell import Quickshell.Hyprland import Quickshell.Wayland import Caelestia.Blobs +import Caelestia.Config import qs.components import qs.components.containers import qs.services -import Caelestia.Config import qs.modules.bar StyledWindow { diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index a55e42d0c..d2cd05f9d 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.services import Caelestia.Config +import qs.services Variants { model: Screens.screens diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fc782ddbe..bef8f4b8e 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -1,9 +1,9 @@ import QtQuick import QtQuick.Controls import Quickshell +import Caelestia.Config import qs.components import qs.components.controls -import Caelestia.Config import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 83f304992..52e20d269 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -1,7 +1,7 @@ import QtQuick import Quickshell -import qs.components import Caelestia.Config +import qs.components import qs.modules.bar as Bar import qs.modules.dashboard as Dashboard import qs.modules.launcher as Launcher diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index dc3ee16cb..52b9b1b59 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config import qs.modules.launcher.items import qs.modules.launcher.services diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 49178db7e..2ca417391 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.modules.launcher.services Item { diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index ac10fd4d8..e36c0d32e 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound import QtQuick +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index 705563a8d..3663f4bc4 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import "items" import QtQuick import Quickshell +import Caelestia.Config import qs.components.controls import qs.services -import Caelestia.Config PathView { id: root diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index b0b8753bb..e6cc8114e 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.components import Caelestia.Config +import qs.components import qs.modules.launcher.services Item { diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index 06d3e0c9f..a0473844a 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -1,7 +1,7 @@ import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index f65a6b650..7a3995496 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -1,9 +1,9 @@ import QtQuick import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.utils import qs.modules.launcher.services diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 0585276f6..668829911 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -2,9 +2,9 @@ import QtQuick import QtQuick.Layouts import Quickshell import Caelestia +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index c5d9d8bb3..96e08548e 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -1,7 +1,7 @@ import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.modules.launcher.services Item { diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index eb486ea18..a95839ad2 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -1,7 +1,7 @@ import QtQuick +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.modules.launcher.services Item { diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index cd41a315d..771c26caf 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -1,10 +1,10 @@ import QtQuick +import Caelestia.Config import Caelestia.Models import qs.components import qs.components.effects import qs.components.images import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 29ed6905d..30b8bb2c8 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -3,8 +3,8 @@ pragma Singleton import ".." import QtQuick import Quickshell -import qs.services import Caelestia.Config +import qs.services import qs.utils Searcher { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index aa4bd51bb..be7bb22d5 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.images import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index 175780318..0ff7daad4 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config RowLayout { id: root diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index d3ba56e3a..0afbe4557 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -3,10 +3,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Services.UPower +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index bc0b3312d..c1c1e31fe 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index cbb2e4f03..b269e1ea8 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Effects import Quickshell.Wayland +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config WlSessionLockSurface { id: root diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 1f876daf1..169d06f4b 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 57587f8f0..070d4a33f 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -4,11 +4,11 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.effects import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index e7d77a20d..07a6a1ca2 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -5,10 +5,10 @@ import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Quickshell.Services.Notifications +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config import qs.utils StyledRect { diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 59a470d00..5babda203 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -1,10 +1,10 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.misc import qs.services -import Caelestia.Config GridLayout { id: root diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index 2a8d058e8..e76be9e6a 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index a0a964b01..fcbf41f2f 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,11 +1,11 @@ import QtQuick import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.widgets import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 63d27e692..ca7596dc8 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -6,10 +6,10 @@ import QtQuick.Shapes import Quickshell import Quickshell.Widgets import Quickshell.Services.Notifications +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config import qs.utils StyledRect { diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 521b803c0..012c4fe56 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 2d7986a73..7884c3190 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 48dd9c5b2..ed6dff117 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -2,9 +2,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config import qs.utils Column { diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 417bd95ad..081752d3e 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.components import Caelestia.Config +import qs.components Item { id: root diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index c6016967c..f6b8eb051 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -1,8 +1,8 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 6394a0e66..26dea1957 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 09da396c0..843d6ef9f 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -4,11 +4,11 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.effects import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index b9930409f..914164e6b 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -3,12 +3,12 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config import qs.utils Item { diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 9803155d0..991802bb6 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Caelestia.Components +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config LazyListView { id: root diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 2314dfe59..dee6f20b6 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -4,10 +4,10 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Services.Notifications +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config import qs.utils StyledRect { diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 4aecf75f2..935068568 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -4,9 +4,9 @@ import QtQuick import QtQuick.Layouts import Quickshell import Caelestia.Components +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config LazyListView { id: root diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index aff42f693..0ccc62b33 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound import QtQuick -import qs.components import Caelestia.Config +import qs.components Item { id: root diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 7ecb46fff..5ec2d33a7 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -1,8 +1,8 @@ import "cards" import QtQuick import QtQuick.Layouts -import qs.components import Caelestia.Config +import qs.components import qs.modules.bar.popouts as BarPopouts Item { diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 7d7bcac88..2b7e14965 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -4,11 +4,11 @@ import QtQuick import QtQuick.Layouts import QtQuick.Shapes import Caelestia +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import Caelestia.Config Loader { id: root diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index f36444eb5..a87f8a590 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -2,8 +2,8 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import qs.components import Caelestia.Config +import qs.components import qs.modules.sidebar as Sidebar import qs.modules.bar.popouts as BarPopouts diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 08427e7cb..bf80dee34 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -1,9 +1,9 @@ import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index dca2d375c..43a84076c 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -2,10 +2,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index 210b92afd..c239e06b4 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -4,12 +4,12 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Widgets +import Caelestia.Config import Caelestia.Models import qs.components import qs.components.containers import qs.components.controls import qs.services -import Caelestia.Config import qs.utils ColumnLayout { diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 41eee68fe..49147a624 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -3,10 +3,10 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import Caelestia.Config import qs.modules.bar.popouts as BarPopouts StyledRect { diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index 756160fc7..5247e7737 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -1,10 +1,10 @@ import QtQuick import QtQuick.Layouts import Caelestia +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import Caelestia.Config StyledRect { id: root diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 517175bfd..bceb4ea67 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Caelestia +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 6ddda9b3a..5d9752ca9 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -3,9 +3,9 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index 1f274ca8e..1820409f0 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -1,9 +1,9 @@ import QtQuick import QtQuick.Layouts import Quickshell.Hyprland +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config ColumnLayout { id: root diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 4946205cc..fed858915 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -5,9 +5,9 @@ import QtQuick.Layouts import Quickshell import Quickshell.Hyprland import Quickshell.Wayland +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index 2c31d209e..7ee35c29b 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -2,9 +2,9 @@ import QtQuick import QtQuick.Layouts import Quickshell import Quickshell.Hyprland +import Caelestia.Config import qs.components import qs.services -import Caelestia.Config Item { id: root diff --git a/services/Audio.qml b/services/Audio.qml index ad6791499..faf86a932 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -5,8 +5,8 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Pipewire import Caelestia -import Caelestia.Services import Caelestia.Config +import Caelestia.Services Singleton { id: root diff --git a/services/Brightness.qml b/services/Brightness.qml index 5e7d7f81c..2cd628ec0 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -4,8 +4,8 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import Quickshell.Io -import qs.components.misc import Caelestia.Config +import qs.components.misc Singleton { id: root diff --git a/services/Colours.qml b/services/Colours.qml index fd4698de6..922b51dbc 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -5,8 +5,8 @@ import QtQuick import Quickshell import Quickshell.Io import Caelestia -import qs.services import Caelestia.Config +import qs.services import qs.utils Singleton { diff --git a/services/GameMode.qml b/services/GameMode.qml index ac1b235de..44b0fe4c8 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -4,8 +4,8 @@ import QtQuick import Quickshell import Quickshell.Io import Caelestia -import qs.services import Caelestia.Config +import qs.services Singleton { id: root diff --git a/services/Hypr.qml b/services/Hypr.qml index 2c307e2a8..054b611c5 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -5,9 +5,9 @@ import Quickshell import Quickshell.Hyprland import Quickshell.Io import Caelestia +import Caelestia.Config import Caelestia.Internal import qs.components.misc -import Caelestia.Config Singleton { id: root diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index e0cc61b30..a6598713b 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -3,8 +3,8 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io -import Caelestia.Internal import Caelestia.Config +import Caelestia.Internal Singleton { id: root diff --git a/services/NotifData.qml b/services/NotifData.qml index 31ff7d2f4..1cec15100 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -4,8 +4,8 @@ import QtQuick import Quickshell import Quickshell.Services.Notifications import Caelestia -import qs.services import Caelestia.Config +import qs.services import qs.utils QtObject { diff --git a/services/Notifs.qml b/services/Notifs.qml index f2205fc07..510ee0bc3 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -6,9 +6,9 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Notifications import Caelestia +import Caelestia.Config import qs.components.misc import qs.services -import Caelestia.Config import qs.utils Singleton { diff --git a/services/Players.qml b/services/Players.qml index f3c6ab3f4..0724afbc5 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -5,8 +5,8 @@ import Quickshell import Quickshell.Io import Quickshell.Services.Mpris import Caelestia -import qs.components.misc import Caelestia.Config +import qs.components.misc Singleton { id: root diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index a42d926e5..4ef522bad 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -3,9 +3,9 @@ pragma Singleton import QtQuick import Quickshell import Quickshell.Io +import Caelestia.Config import Caelestia.Models import qs.services -import Caelestia.Config import qs.utils Searcher { From ae8d344f84ca381a3716a81c364de77fd54b6d0c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:43:31 +1000 Subject: [PATCH 296/409] fix: qualify attached props on non-Item components They do not have parents, so they won't inherit the config scope otherwise Animations are not per monitor --- modules/controlcenter/NavRail.qml | 4 ++-- modules/controlcenter/bluetooth/Details.qml | 4 ++-- modules/launcher/ContentList.qml | 6 +++--- modules/sidebar/Notif.qml | 12 ++++++------ 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 5a0ba37bb..4522573b3 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -31,7 +31,7 @@ Item { when: root.session.navExpanded PropertyChanges { - layout.spacing: Tokens.spacing.small + layout.spacing: root.Tokens.spacing.small } } @@ -148,7 +148,7 @@ Item { expandedLabel.opacity: 1 smallLabel.opacity: 0 background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth - background.implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + background.implicitHeight: icon.implicitHeight + root.Tokens.padding.normal * 2 item.implicitHeight: background.implicitHeight } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index a398c34d5..ec47e5f1c 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -167,7 +167,7 @@ StyledFlickable { PropertyChanges { renameDevice.implicitHeight: deviceNameEdit.implicitHeight renameLabel.opacity: 0 - deviceNameEdit.padding: Tokens.padding.normal + deviceNameEdit.padding: root.Tokens.padding.normal } } @@ -481,7 +481,7 @@ StyledFlickable { when: root.session.bt.fabMenuOpen PropertyChanges { - fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Tokens.padding.large * 2 + fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.large * 2 fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index e36c0d32e..4667a540d 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -32,7 +32,7 @@ Item { name: "apps" PropertyChanges { - root.implicitWidth: Tokens.sizes.launcher.itemWidth + root.implicitWidth: root.Tokens.sizes.launcher.itemWidth root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) appList.active: true } @@ -46,8 +46,8 @@ Item { name: "wallpapers" PropertyChanges { - root.implicitWidth: Math.max(Tokens.sizes.launcher.itemWidth * 1.2, wallpaperList.implicitWidth) - root.implicitHeight: Tokens.sizes.launcher.wallpaperHeight + root.implicitWidth: Math.max(root.Tokens.sizes.launcher.itemWidth * 1.2, wallpaperList.implicitWidth) + root.implicitHeight: root.Tokens.sizes.launcher.wallpaperHeight wallpaperList.active: true } } diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 26dea1957..54dfb5ff3 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -32,12 +32,12 @@ StyledRect { name: "expanded" PropertyChanges { - summary.anchors.margins: Tokens.padding.normal - dummySummary.anchors.margins: Tokens.padding.normal - compactBody.anchors.margins: Tokens.padding.normal - timeStr.anchors.margins: Tokens.padding.normal - expandedContent.anchors.margins: Tokens.padding.normal - summary.width: root.width - Tokens.padding.normal * 2 - timeStr.implicitWidth - Tokens.spacing.small + summary.anchors.margins: root.Tokens.padding.normal + dummySummary.anchors.margins: root.Tokens.padding.normal + compactBody.anchors.margins: root.Tokens.padding.normal + timeStr.anchors.margins: root.Tokens.padding.normal + expandedContent.anchors.margins: root.Tokens.padding.normal + summary.width: root.width - root.Tokens.padding.normal * 2 - timeStr.implicitWidth - root.Tokens.spacing.small summary.maximumLineCount: Number.MAX_SAFE_INTEGER } } From 37b61fedc9268d66e73569461f872fa35861d75b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 23:27:27 +1000 Subject: [PATCH 297/409] chore: update readme --- README.md | 56 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 48 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 43d04fb13..8c6763bd8 100644 --- a/README.md +++ b/README.md @@ -205,22 +205,40 @@ git pull ## Configuring All configuration options should be put in `~/.config/caelestia/shell.json`. This file is _not_ created by -default, you must create it manually. +default, you must create it manually. Options that you omit from the config file will use their default +values. + +### Per-monitor configuration + +You can configure options per-monitor in `~/.config/caelestia/monitors//shell.json`. Options +set in this file will **override** the respective options in the global config. Otherwise, the options will +use their values from the global config. + +For example, to disable the bar on DP-1: + +**`~/.config/caelestia/monitors/DP-1/shell.json`** + +```json +{ + "bar": { + "persistent": false + } +} +``` ### Example configuration > [!NOTE] -> The example configuration only includes recommended configuration options. For more advanced customisation -> such as modifying the size of individual items or changing constants in the code, there are some other -> options which can be found in the source files in the `config` directory. +> The example configuration includes ALL configuration options in `shell.json`. You are +> **not** recommended to copy and paste this entire configuration into `shell.json`. +> This is meant to serve as a reference of all the available options, and you should +> only add the ones you want to change to `shell.json`.
Example ```json { "appearance": { - "mediaGifSpeedAdjustment": 300, - "sessionGifSpeed": 0.7, "anim": { "durations": { "scale": 1 @@ -254,6 +272,8 @@ default, you must create it manually. }, "general": { "logo": "caelestia", + "mediaGifSpeedAdjustment": 300, + "sessionGifSpeed": 0.7, "apps": { "terminal": ["foot"], "audio": ["pavucontrol"], @@ -442,9 +462,13 @@ default, you must create it manually. }, "dashboard": { "enabled": true, + "showOnHover": true, + "showDashboard": true, + "showMedia": true, + "showPerformance": true, + "showWeather": true, "dragThreshold": 50, - "mediaUpdateInterval": 500, - "showOnHover": true + "mediaUpdateInterval": 500 }, "launcher": { "actionPrefix": ">", @@ -704,6 +728,22 @@ default, you must create it manually.
+### Advanced configuration + +> [!WARNING] +> Do NOT change any of these options if you do not know what you are doing. These options control the +> tokens used internally within the shell, and can cause visual issues if changed. The existence of +> the options are also not guaranteed across versions, and may change or be removed without notice. + +A separate `~/.config/caelestia/shell-tokens.json` file allows editing the internal tokens without +touching the source code of the shell. These tokens affect, for example, individual rounding, +spacing, padding, font size, animation duration and easing curves tokens, and the sizes of certain +components. The appearance scale values in `shell.json` are multiplied against these base +token values to produce the final computed values. + +Per-monitor token overrides are also available at +`~/.config/caelestia/monitors//shell-tokens.json`. + ### Home Manager Module For NixOS users, a home manager module is also available. From 9be9ec10a61984dfbc48dca43b47c522a693104d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 23:37:42 +1000 Subject: [PATCH 298/409] fix: per monitor configs Use c++ singleton pattern for monitor manager and bind appearance tokens properly Also use scoped properties on ContentWindow --- components/containers/StyledWindow.qml | 1 + modules/drawers/ContentWindow.qml | 10 ++-- plugin/src/Caelestia/Config/config.cpp | 3 + .../Caelestia/Config/monitorconfigmanager.cpp | 56 ++++++------------- .../Caelestia/Config/monitorconfigmanager.hpp | 3 - 5 files changed, 27 insertions(+), 46 deletions(-) diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml index 57f319c09..c93465c78 100644 --- a/components/containers/StyledWindow.qml +++ b/components/containers/StyledWindow.qml @@ -8,6 +8,7 @@ PanelWindow { id: root required property string name + readonly property alias configScope: scope default property alias contentData: scope.data WlrLayershell.namespace: `caelestia-${name}` diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 2842a33f0..4ff12c166 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -30,9 +30,9 @@ StyledWindow { } return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; } - property real borderThickness: hasFullscreen ? 0 : Config.border.thickness - readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness - property real borderRounding: hasFullscreen ? 0 : Config.border.rounding + property real borderThickness: hasFullscreen ? 0 : configScope.Config.border.thickness + readonly property real borderLayoutThickness: hasFullscreen ? 0 : configScope.Config.border.thickness + property real borderRounding: hasFullscreen ? 0 : configScope.Config.border.rounding property real shadowOpacity: hasFullscreen ? 0 : 0.7 readonly property int dragMaskPadding: { @@ -44,8 +44,8 @@ StyledWindow { const thresholds = []; for (const panel of ["dashboard", "launcher", "session", "sidebar"]) - if (Config[panel].enabled) - thresholds.push(Config[panel].dragThreshold); + if (configScope.Config[panel].enabled) + thresholds.push(configScope.Config[panel].dragThreshold); return Math.max(...thresholds); } diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 410677350..9ed2b4af6 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -61,6 +61,9 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObj setupFileBackend(filePath); if (fallback) syncFromGlobal(fallback); + + // Bind appearance computed properties to token base values + bindAppearanceTokens(); } GlobalConfig* GlobalConfig::instance() { diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp index d8b0561c7..473b6567f 100644 --- a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp @@ -6,8 +6,6 @@ namespace caelestia::config { -MonitorConfigManager* MonitorConfigManager::s_instance = nullptr; - namespace { QString monitorConfigDir(const QString& screen) { @@ -18,52 +16,34 @@ QString monitorConfigDir(const QString& screen) { } // namespace MonitorConfigManager::MonitorConfigManager(QObject* parent) - : QObject(parent) { - s_instance = this; -} - -MonitorConfigManager::~MonitorConfigManager() { - s_instance = nullptr; -} + : QObject(parent) {} MonitorConfigManager* MonitorConfigManager::instance() { - return s_instance; + static MonitorConfigManager instance; + return &instance; } -MonitorConfigManager* MonitorConfigManager::create(QQmlEngine* engine, QJSEngine*) { - return new MonitorConfigManager(engine); +MonitorConfigManager* MonitorConfigManager::create(QQmlEngine*, QJSEngine*) { + QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); + return instance(); } GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) { - auto it = m_overlays.find(screen); - if (it != m_overlays.end() && it->config) - return it->config; - - auto* global = GlobalConfig::instance(); - if (!global) - return nullptr; - - auto dir = monitorConfigDir(screen); - auto* overlay = new GlobalConfig(global, dir + QStringLiteral("shell.json"), this); - - m_overlays[screen].config = overlay; - return overlay; + auto& overlay = m_overlays[screen]; + if (!overlay.config) { + auto dir = monitorConfigDir(screen); + overlay.config = new GlobalConfig(GlobalConfig::instance(), dir + QStringLiteral("shell.json"), this); + } + return overlay.config; } TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) { - auto it = m_overlays.find(screen); - if (it != m_overlays.end() && it->tokens) - return it->tokens; - - auto* global = TokenConfig::instance(); - if (!global) - return nullptr; - - auto dir = monitorConfigDir(screen); - auto* overlay = new TokenConfig(global, dir + QStringLiteral("shell-tokens.json"), this); - - m_overlays[screen].tokens = overlay; - return overlay; + auto& overlay = m_overlays[screen]; + if (!overlay.tokens) { + auto dir = monitorConfigDir(screen); + overlay.tokens = new TokenConfig(TokenConfig::instance(), dir + QStringLiteral("shell-tokens.json"), this); + } + return overlay.tokens; } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.hpp b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp index 2741b7327..70bbdc0d3 100644 --- a/plugin/src/Caelestia/Config/monitorconfigmanager.hpp +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp @@ -21,8 +21,6 @@ class MonitorConfigManager : public QObject { [[nodiscard]] Q_INVOKABLE GlobalConfig* configForScreen(const QString& screen); [[nodiscard]] Q_INVOKABLE TokenConfig* tokensForScreen(const QString& screen); - ~MonitorConfigManager() override; - private: explicit MonitorConfigManager(QObject* parent = nullptr); @@ -32,7 +30,6 @@ class MonitorConfigManager : public QObject { }; QHash m_overlays; - static MonitorConfigManager* s_instance; }; } // namespace caelestia::config From 31d7a77929abb304f0bd53df35b837111cd038d7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 23:49:40 +1000 Subject: [PATCH 299/409] fix: popouts anims, should not be per-monitor Anims are not per-monitor --- modules/bar/popouts/Wrapper.qml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 48fe05f87..9a18f9117 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -32,13 +32,16 @@ Item { property string detachedMode property string queuedMode - property int animLength: Tokens.anim.durations.expressiveDefaultSpatial - property easingCurve animCurve: Tokens.anim.expressiveDefaultSpatial + // Dummy object so Tokens attached prop resolves to global config + // Anim configs are not per-monitor + readonly property QtObject dummy: QtObject {} + property int animLength: dummy.Tokens.anim.durations.expressiveDefaultSpatial + property easingCurve animCurve: dummy.Tokens.anim.expressiveDefaultSpatial function setAnims(detach: bool): void { const type = `expressive${detach ? "Slow" : "Default"}Spatial`; - animLength = Tokens.anim.durations[type]; - animCurve = Tokens.anim[type]; + animLength = dummy.Tokens.anim.durations[type]; + animCurve = dummy.Tokens.anim[type]; } function detach(mode: string): void { From 93b5fb5f4f0d811460a0a86546e3a9da02a7817a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 11 Apr 2026 23:53:36 +1000 Subject: [PATCH 300/409] feat: make all configs sparse --- plugin/src/Caelestia/Config/config.cpp | 1 - plugin/src/Caelestia/Config/configobject.cpp | 54 +------------------- plugin/src/Caelestia/Config/configobject.hpp | 6 --- plugin/src/Caelestia/Config/tokens.cpp | 1 - 4 files changed, 2 insertions(+), 60 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 9ed2b4af6..7ae638d99 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -56,7 +56,6 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObj , m_sidebar(new SidebarConfig(this)) , m_services(new ServiceConfig(this)) , m_paths(new UserPaths(this)) { - setSparse(true); if (!filePath.isEmpty()) setupFileBackend(filePath); if (fallback) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 8103b8969..fda9fba27 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -70,56 +70,6 @@ QJsonObject ConfigObject::toJsonObject() const { QJsonObject obj; const auto* meta = metaObject(); - for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { - const auto prop = meta->property(i); - - if (!prop.isReadable()) - continue; - - const auto key = QString::fromUtf8(prop.name()); - const auto value = prop.read(this); - - // Recurse into sub-objects - if (value.canView()) { - auto* const subObj = value.value(); - if (subObj) - obj.insert(key, subObj->toJsonObject()); - else - qCWarning(lcConfig, "Unable to get sub-object when serializing config object"); - continue; - } - - // Skip read-only properties (computed values) - if (!prop.isWritable()) - continue; - - // Handle QStringList explicitly - if (prop.metaType().id() == QMetaType::QStringList) { - QJsonArray arr; - const auto strList = value.toStringList(); - for (const auto& s : strList) - arr.append(s); - obj.insert(key, arr); - continue; - } - - // Handle QVariantList explicitly - if (prop.metaType().id() == QMetaType::QVariantList) { - obj.insert(key, QJsonArray::fromVariantList(value.toList())); - continue; - } - - // Default case - obj.insert(key, QJsonValue::fromVariant(value)); - } - - return obj; -} - -QJsonObject ConfigObject::toSparseJsonObject() const { - QJsonObject obj; - const auto* meta = metaObject(); - for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { const auto prop = meta->property(i); @@ -133,7 +83,7 @@ QJsonObject ConfigObject::toSparseJsonObject() const { if (value.canView()) { auto* const subObj = value.value(); if (subObj) { - auto subJson = subObj->toSparseJsonObject(); + auto subJson = subObj->toJsonObject(); if (!subJson.isEmpty()) obj.insert(key, subJson); } @@ -304,7 +254,7 @@ void ConfigObject::setupFileBackend(const QString& path) { return; } - auto json = m_sparse ? toSparseJsonObject() : toJsonObject(); + auto json = toJsonObject(); file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); if (auto* root = qobject_cast(this)) emit root->fileSaved(); diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index f04204414..a70cdd74d 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -53,7 +53,6 @@ class ConfigObject : public QObject { void loadFromJson(const QJsonObject& obj); [[nodiscard]] QJsonObject toJsonObject() const; - [[nodiscard]] QJsonObject toSparseJsonObject() const; // File-backed config support. Call setupFileBackend() to enable // automatic file watching, debounced saving, and reload. @@ -69,10 +68,6 @@ class ConfigObject : public QObject { [[nodiscard]] bool isPropertyLoaded(const QString& name) const { return m_loadedKeys.contains(name); } - [[nodiscard]] bool isSparse() const { return m_sparse; } - - void setSparse(bool sparse) { m_sparse = sparse; } - [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } template static bool updateMember(T& member, const T& value) { @@ -100,7 +95,6 @@ class ConfigObject : public QObject { QString m_filePath; bool m_recentlySaved = false; - bool m_sparse = false; // File backend (heap-allocated only when setupFileBackend is called) QFileSystemWatcher* m_watcher = nullptr; diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 902dbfc49..74cd827b2 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -26,7 +26,6 @@ TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject : RootConfig(parent) , m_appearance(new AppearanceTokens(this)) , m_sizes(new SizeTokens(this)) { - setSparse(true); if (!filePath.isEmpty()) setupFileBackend(filePath); if (fallback) From 746d1af8262ed303a3b0934dde1a8b7e27108b83 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:00:56 +1000 Subject: [PATCH 301/409] feat: allow resetting options Also make options changed programmatically (from QML, etc) mark themselves as loaded --- plugin/src/Caelestia/Config/configobject.cpp | 18 ++++++++++++++++++ plugin/src/Caelestia/Config/configobject.hpp | 4 ++++ 2 files changed, 22 insertions(+) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index fda9fba27..7a36f7b5d 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -168,6 +168,24 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) { } } +void ConfigObject::markPropertyLoaded(const QString& name) { + m_loadedKeys.insert(name); +} + +void ConfigObject::resetOption(const QString& name) { + m_loadedKeys.remove(name); + + // If synced from global, re-copy the global value + if (m_global) { + int idx = metaObject()->indexOfProperty(name.toUtf8().constData()); + if (idx >= 0) { + auto prop = metaObject()->property(idx); + if (prop.isWritable()) + prop.write(this, prop.read(m_global)); + } + } +} + void ConfigObject::resyncFromGlobal() { if (!m_global) return; diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index a70cdd74d..8ffe074c6 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -20,6 +20,7 @@ public: } \ void set_##name(const Type& val) { \ if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ + markPropertyLoaded(QStringLiteral(#name)); \ Q_EMIT name##Changed(); \ notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ } \ @@ -68,6 +69,8 @@ class ConfigObject : public QObject { [[nodiscard]] bool isPropertyLoaded(const QString& name) const { return m_loadedKeys.contains(name); } + Q_INVOKABLE void resetOption(const QString& name); + [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } template static bool updateMember(T& member, const T& value) { @@ -86,6 +89,7 @@ class ConfigObject : public QObject { void propertiesChanged(const QMap& changed); protected: + void markPropertyLoaded(const QString& name); void notifyPropertyChanged(const QString& name, const QVariant& value); private: From 9b1b33a3883ea362ae46a7cfe697d18f574492cf Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:19:48 +1000 Subject: [PATCH 302/409] feat: remove file prefix on signals --- plugin/src/Caelestia/Config/configobject.cpp | 11 ++++++----- plugin/src/Caelestia/Config/configobject.hpp | 8 ++++---- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 7a36f7b5d..007699417 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -268,14 +268,15 @@ void ConfigObject::setupFileBackend(const QString& path) { if (!file.open(QIODevice::WriteOnly)) { qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath)); if (auto* root = qobject_cast(this)) - emit root->fileSaveFailed(QStringLiteral("Failed to open file for writing")); + emit root->saveFailed(QStringLiteral("Failed to open file for writing")); return; } auto json = toJsonObject(); file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); + file.close(); if (auto* root = qobject_cast(this)) - emit root->fileSaved(); + emit root->saved(); }); m_cooldownTimer->setSingleShot(true); @@ -355,9 +356,9 @@ void ConfigObject::onFileChanged() { bool ok = reloadFromFile(); if (auto* root = qobject_cast(this)) { if (ok) - emit root->fileLoaded(); + emit root->loaded(); else - emit root->fileLoadFailed(QStringLiteral("Failed to load config file")); + emit root->loadFailed(QStringLiteral("Failed to load config file")); } } } @@ -373,7 +374,7 @@ void RootConfig::save() { void RootConfig::reload() { if (reloadFromFile()) - emit fileLoaded(); + emit loaded(); } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 8ffe074c6..66df4ac3e 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -126,10 +126,10 @@ class RootConfig : public ConfigObject { Q_INVOKABLE void reload(); signals: - void fileLoaded(); - void fileLoadFailed(const QString& error); - void fileSaved(); - void fileSaveFailed(const QString& error); + void loaded(); + void loadFailed(const QString& error); + void saved(); + void saveFailed(const QString& error); }; } // namespace caelestia::config From 697784abcef53d75507fc738d2d9f3121794b04e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 01:20:40 +1000 Subject: [PATCH 303/409] fix: remove key from loaded keys on sync A sync goes through the property setter, which sets the key as loaded. But a sync is not considered an explicit set, so unset it after --- plugin/src/Caelestia/Config/configobject.cpp | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 007699417..4e198a411 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -161,6 +161,7 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) { if (!m_loadedKeys.contains(key)) { auto val = prop.read(global); prop.write(this, val); + m_loadedKeys.remove(key); // setter added it — remove since this is a synced value qCDebug(lcConfig) << " Synced" << key << "=" << val << "from global"; } else { qCDebug(lcConfig) << " Keeping loaded" << key << "=" << prop.read(this); @@ -206,8 +207,10 @@ void ConfigObject::resyncFromGlobal() { if (!prop.isWritable()) continue; - if (!m_loadedKeys.contains(key)) + if (!m_loadedKeys.contains(key)) { prop.write(this, prop.read(m_global)); + m_loadedKeys.remove(key); // setter added it — remove since this is a synced value + } } } @@ -219,6 +222,9 @@ void ConfigObject::onGlobalPropertiesChanged(const QMap& chan int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); if (idx >= 0) { metaObject()->property(idx).write(this, it.value()); + // Remove the key that was added by markPropertyLoaded in the setter — + // this is a synced value, not an explicit override + m_loadedKeys.remove(it.key()); qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value() << "from global change"; } From 72e0a218fd700bef06e073512e9013dd3e486daf Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:06:13 +1000 Subject: [PATCH 304/409] refactor: move file logic to RootConfig + to own file --- plugin/src/Caelestia/Config/CMakeLists.txt | 1 + plugin/src/Caelestia/Config/config.hpp | 1 + plugin/src/Caelestia/Config/configobject.cpp | 176 +++---------------- plugin/src/Caelestia/Config/configobject.hpp | 39 ---- plugin/src/Caelestia/Config/rootconfig.cpp | 127 +++++++++++++ plugin/src/Caelestia/Config/rootconfig.hpp | 46 +++++ plugin/src/Caelestia/Config/tokens.hpp | 2 +- 7 files changed, 197 insertions(+), 195 deletions(-) create mode 100644 plugin/src/Caelestia/Config/rootconfig.cpp create mode 100644 plugin/src/Caelestia/Config/rootconfig.hpp diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 92ee2da9f..a77a1fe96 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -3,6 +3,7 @@ qml_module(caelestia-config SOURCES config.cpp configobject.cpp + rootconfig.cpp configscope.cpp appearanceconfig.cpp tokens.cpp diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index d683df11b..1a9891434 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -13,6 +13,7 @@ #include "lockconfig.hpp" #include "notifsconfig.hpp" #include "osdconfig.hpp" +#include "rootconfig.hpp" #include "serviceconfig.hpp" #include "sessionconfig.hpp" #include "sidebarconfig.hpp" diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 4e198a411..cc38d7e5f 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -1,10 +1,6 @@ #include "configobject.hpp" -#include -#include -#include #include -#include #include #include #include @@ -15,6 +11,8 @@ namespace caelestia::config { Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg) +// ConfigObject + ConfigObject::ConfigObject(QObject* parent) : QObject(parent) {} @@ -169,24 +167,6 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) { } } -void ConfigObject::markPropertyLoaded(const QString& name) { - m_loadedKeys.insert(name); -} - -void ConfigObject::resetOption(const QString& name) { - m_loadedKeys.remove(name); - - // If synced from global, re-copy the global value - if (m_global) { - int idx = metaObject()->indexOfProperty(name.toUtf8().constData()); - if (idx >= 0) { - auto prop = metaObject()->property(idx); - if (prop.isWritable()) - prop.write(this, prop.read(m_global)); - } - } -} - void ConfigObject::resyncFromGlobal() { if (!m_global) return; @@ -214,6 +194,24 @@ void ConfigObject::resyncFromGlobal() { } } +void ConfigObject::markPropertyLoaded(const QString& name) { + m_loadedKeys.insert(name); +} + +void ConfigObject::resetOption(const QString& name) { + m_loadedKeys.remove(name); + + // If synced from global, re-copy the global value + if (m_global) { + int idx = metaObject()->indexOfProperty(name.toUtf8().constData()); + if (idx >= 0) { + auto prop = metaObject()->property(idx); + if (prop.isWritable()) + prop.write(this, prop.read(m_global)); + } + } +} + void ConfigObject::onGlobalPropertiesChanged(const QMap& changed) { for (auto it = changed.begin(); it != changed.end(); ++it) { if (m_loadedKeys.contains(it.key())) @@ -222,9 +220,7 @@ void ConfigObject::onGlobalPropertiesChanged(const QMap& chan int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); if (idx >= 0) { metaObject()->property(idx).write(this, it.value()); - // Remove the key that was added by markPropertyLoaded in the setter — - // this is a synced value, not an explicit override - m_loadedKeys.remove(it.key()); + m_loadedKeys.remove(it.key()); // setter added it — remove since this is a synced value qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value() << "from global change"; } @@ -253,134 +249,4 @@ void ConfigObject::emitBatchedChanges() { emit propertiesChanged(changes); } -void ConfigObject::setupFileBackend(const QString& path) { - m_filePath = path; - - m_watcher = new QFileSystemWatcher(this); - m_saveTimer = new QTimer(this); - m_cooldownTimer = new QTimer(this); - m_retryTimer = new QTimer(this); - - m_retryTimer->setSingleShot(true); - m_retryTimer->setInterval(50); - connect(m_retryTimer, &QTimer::timeout, this, &ConfigObject::reloadFromFile); - - m_saveTimer->setSingleShot(true); - m_saveTimer->setInterval(500); - connect(m_saveTimer, &QTimer::timeout, this, [this] { - QDir().mkpath(QFileInfo(m_filePath).absolutePath()); - - QFile file(m_filePath); - if (!file.open(QIODevice::WriteOnly)) { - qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath)); - if (auto* root = qobject_cast(this)) - emit root->saveFailed(QStringLiteral("Failed to open file for writing")); - return; - } - - auto json = toJsonObject(); - file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); - file.close(); - if (auto* root = qobject_cast(this)) - emit root->saved(); - }); - - m_cooldownTimer->setSingleShot(true); - m_cooldownTimer->setInterval(2000); - connect(m_cooldownTimer, &QTimer::timeout, this, [this] { - m_recentlySaved = false; - }); - - connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &ConfigObject::onFileChanged); - - qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; - - reloadFromFile(); - - if (QFile::exists(m_filePath)) - m_watcher->addPath(m_filePath); -} - -void ConfigObject::saveToFile() { - if (!m_saveTimer) - return; - m_saveTimer->start(); - m_recentlySaved = true; - m_cooldownTimer->start(); -} - -bool ConfigObject::reloadFromFile() { - QFile file(m_filePath); - - if (!file.open(QIODevice::ReadOnly)) { - qCDebug(lcConfig, "Failed to open %s", qUtf8Printable(m_filePath)); - return false; - } - - QJsonParseError error{}; - auto doc = QJsonDocument::fromJson(file.readAll(), &error); - - if (error.error != QJsonParseError::NoError) { - if (m_retryTimer && m_parseRetries < 3) { - m_parseRetries++; - qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath), - qUtf8Printable(error.errorString()), m_parseRetries); - m_retryTimer->start(); - } else { - qCWarning( - lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); - m_parseRetries = 0; - } - return false; - } - - m_parseRetries = 0; - - qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; - - clearLoadedKeys(); - loadFromJson(doc.object()); - - // Re-sync non-loaded properties from global after reload - if (m_global) { - qCDebug(lcConfig) << "Re-syncing" << metaObject()->className() << "from global after reload"; - resyncFromGlobal(); - } - - return true; -} - -void ConfigObject::onFileChanged() { - if (!m_watcher->files().contains(m_filePath)) - m_watcher->addPath(m_filePath); - - if (!m_recentlySaved) { - m_parseRetries = 0; - if (m_retryTimer) - m_retryTimer->stop(); - - bool ok = reloadFromFile(); - if (auto* root = qobject_cast(this)) { - if (ok) - emit root->loaded(); - else - emit root->loadFailed(QStringLiteral("Failed to load config file")); - } - } -} - -// RootConfig - -RootConfig::RootConfig(QObject* parent) - : ConfigObject(parent) {} - -void RootConfig::save() { - saveToFile(); -} - -void RootConfig::reload() { - if (reloadFromFile()) - emit loaded(); -} - } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 66df4ac3e..4e0cae926 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -1,6 +1,5 @@ #pragma once -#include #include #include #include @@ -55,14 +54,7 @@ class ConfigObject : public QObject { void loadFromJson(const QJsonObject& obj); [[nodiscard]] QJsonObject toJsonObject() const; - // File-backed config support. Call setupFileBackend() to enable - // automatic file watching, debounced saving, and reload. - void setupFileBackend(const QString& path); - void saveToFile(); - bool reloadFromFile(); - // Per-monitor overlay support (Qt Resolve Mask pattern). - // Eagerly syncs non-overridden properties from a global ConfigObject. void syncFromGlobal(ConfigObject* global); void resyncFromGlobal(); void clearLoadedKeys(); @@ -71,8 +63,6 @@ class ConfigObject : public QObject { Q_INVOKABLE void resetOption(const QString& name); - [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } - template static bool updateMember(T& member, const T& value) { if constexpr (std::is_floating_point_v) { if (qFuzzyCompare(member + 1.0, value + 1.0)) @@ -93,20 +83,9 @@ class ConfigObject : public QObject { void notifyPropertyChanged(const QString& name, const QVariant& value); private: - void onFileChanged(); void onGlobalPropertiesChanged(const QMap& changed); void emitBatchedChanges(); - QString m_filePath; - bool m_recentlySaved = false; - - // File backend (heap-allocated only when setupFileBackend is called) - QFileSystemWatcher* m_watcher = nullptr; - QTimer* m_saveTimer = nullptr; - QTimer* m_cooldownTimer = nullptr; - QTimer* m_retryTimer = nullptr; - int m_parseRetries = 0; - // Per-monitor overlay state ConfigObject* m_global = nullptr; QSet m_loadedKeys; @@ -114,22 +93,4 @@ class ConfigObject : public QObject { QTimer* m_batchTimer = nullptr; }; -// Intermediate base for singleton config roots (GlobalConfig, TokenConfig). -// Provides save/reload with file lifecycle signals. -class RootConfig : public ConfigObject { - Q_OBJECT - -public: - explicit RootConfig(QObject* parent = nullptr); - - Q_INVOKABLE void save(); - Q_INVOKABLE void reload(); - -signals: - void loaded(); - void loadFailed(const QString& error); - void saved(); - void saveFailed(const QString& error); -}; - } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp new file mode 100644 index 000000000..841d01f42 --- /dev/null +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -0,0 +1,127 @@ +#include "rootconfig.hpp" + +#include +#include +#include +#include + +namespace caelestia::config { + +RootConfig::RootConfig(QObject* parent) + : ConfigObject(parent) {} + +void RootConfig::setupFileBackend(const QString& path) { + m_filePath = path; + + m_watcher = new QFileSystemWatcher(this); + m_saveTimer = new QTimer(this); + m_cooldownTimer = new QTimer(this); + m_retryTimer = new QTimer(this); + + m_retryTimer->setSingleShot(true); + m_retryTimer->setInterval(50); + connect(m_retryTimer, &QTimer::timeout, this, &RootConfig::reloadFromFile); + + m_saveTimer->setSingleShot(true); + m_saveTimer->setInterval(500); + connect(m_saveTimer, &QTimer::timeout, this, [this] { + QDir().mkpath(QFileInfo(m_filePath).absolutePath()); + + QFile file(m_filePath); + if (!file.open(QIODevice::WriteOnly)) { + qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath)); + emit saveFailed(QStringLiteral("Failed to open file for writing")); + return; + } + + auto json = toJsonObject(); + file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); + file.close(); + emit saved(); + }); + + m_cooldownTimer->setSingleShot(true); + m_cooldownTimer->setInterval(2000); + connect(m_cooldownTimer, &QTimer::timeout, this, [this] { + m_recentlySaved = false; + }); + + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onFileChanged); + + qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; + + reloadFromFile(); + + if (QFile::exists(m_filePath)) + m_watcher->addPath(m_filePath); +} + +void RootConfig::saveToFile() { + if (!m_saveTimer) + return; + m_saveTimer->start(); + m_recentlySaved = true; + m_cooldownTimer->start(); +} + +bool RootConfig::reloadFromFile() { + QFile file(m_filePath); + + if (!file.open(QIODevice::ReadOnly)) { + qCDebug(lcConfig, "Failed to open %s", qUtf8Printable(m_filePath)); + return false; + } + + QJsonParseError error{}; + auto doc = QJsonDocument::fromJson(file.readAll(), &error); + + if (error.error != QJsonParseError::NoError) { + if (m_retryTimer && m_parseRetries < 3) { + m_parseRetries++; + qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath), + qUtf8Printable(error.errorString()), m_parseRetries); + m_retryTimer->start(); + } else { + qCWarning( + lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); + m_parseRetries = 0; + } + return false; + } + + m_parseRetries = 0; + + qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; + + clearLoadedKeys(); + loadFromJson(doc.object()); + + return true; +} + +void RootConfig::onFileChanged() { + if (!m_watcher->files().contains(m_filePath)) + m_watcher->addPath(m_filePath); + + if (!m_recentlySaved) { + m_parseRetries = 0; + if (m_retryTimer) + m_retryTimer->stop(); + + if (reloadFromFile()) + emit loaded(); + else + emit loadFailed(QStringLiteral("Failed to load config file")); + } +} + +void RootConfig::save() { + saveToFile(); +} + +void RootConfig::reload() { + if (reloadFromFile()) + emit loaded(); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp new file mode 100644 index 000000000..bb644acde --- /dev/null +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -0,0 +1,46 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include + +namespace caelestia::config { + +// Intermediate base for singleton config roots (GlobalConfig, TokenConfig). +// Provides file-backed persistence, save/reload, and lifecycle signals. +class RootConfig : public ConfigObject { + Q_OBJECT + +public: + explicit RootConfig(QObject* parent = nullptr); + + void setupFileBackend(const QString& path); + void saveToFile(); + bool reloadFromFile(); + + [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } + + Q_INVOKABLE void save(); + Q_INVOKABLE void reload(); + +signals: + void loaded(); + void loadFailed(const QString& error); + void saved(); + void saveFailed(const QString& error); + +private: + void onFileChanged(); + + QString m_filePath; + bool m_recentlySaved = false; + + QFileSystemWatcher* m_watcher = nullptr; + QTimer* m_saveTimer = nullptr; + QTimer* m_cooldownTimer = nullptr; + QTimer* m_retryTimer = nullptr; + int m_parseRetries = 0; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 69740aab0..231cbb52a 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -2,7 +2,7 @@ #include "anim.hpp" #include "appearanceconfig.hpp" -#include "configobject.hpp" +#include "rootconfig.hpp" #include #include From 24367dfb3ad9d3f99f3f18a23dd044197866503e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:22:06 +1000 Subject: [PATCH 305/409] feat: add config load/fail toasts --- modules/ConfigToasts.qml | 31 +++++++++++++++++++++++++++++++ shell.qml | 1 + 2 files changed, 32 insertions(+) create mode 100644 modules/ConfigToasts.qml diff --git a/modules/ConfigToasts.qml b/modules/ConfigToasts.qml new file mode 100644 index 000000000..7c43e522f --- /dev/null +++ b/modules/ConfigToasts.qml @@ -0,0 +1,31 @@ +import QtQuick +import Quickshell +import Caelestia +import Caelestia.Config + +Scope { + Connections { + function onLoaded(): void { + if (GlobalConfig.utilities.toasts.configLoaded) + Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded successfully!"), "rule_settings"); + } + + function onLoadFailed(error: string): void { + Toaster.toast(qsTr("Failed to read config file"), error, "settings_alert", Toast.Warning); + } + + function onSaveFailed(error: string): void { + Toaster.toast(qsTr("Failed to save config"), error, "settings_alert", Toast.Error); + } + + target: GlobalConfig + } + + Connections { + function onLoadFailed(error: string): void { + Toaster.toast(qsTr("Failed to read token config file"), error, "settings_alert", Toast.Warning); + } + + target: TokenConfig + } +} diff --git a/shell.qml b/shell.qml index 7ef1014d7..5ccf61d3d 100644 --- a/shell.qml +++ b/shell.qml @@ -19,6 +19,7 @@ ShellRoot { id: lock } + ConfigToasts {} Shortcuts {} BatteryMonitor {} IdleMonitors { From 1ad6f657aeb41eb8cd2ad0c3a7585a4f0a96e984 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:30:24 +1000 Subject: [PATCH 306/409] fix: don't emit until final retry Also more descriptive error messages --- plugin/src/Caelestia/Config/rootconfig.cpp | 42 ++++++++++++---------- plugin/src/Caelestia/Config/rootconfig.hpp | 4 ++- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index 841d01f42..193ac4729 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -20,7 +20,7 @@ void RootConfig::setupFileBackend(const QString& path) { m_retryTimer->setSingleShot(true); m_retryTimer->setInterval(50); - connect(m_retryTimer, &QTimer::timeout, this, &RootConfig::reloadFromFile); + connect(m_retryTimer, &QTimer::timeout, this, &RootConfig::reload); m_saveTimer->setSingleShot(true); m_saveTimer->setInterval(500); @@ -29,8 +29,9 @@ void RootConfig::setupFileBackend(const QString& path) { QFile file(m_filePath); if (!file.open(QIODevice::WriteOnly)) { - qCWarning(lcConfig, "Failed to write %s", qUtf8Printable(m_filePath)); - emit saveFailed(QStringLiteral("Failed to open file for writing")); + auto err = QStringLiteral("Failed to write %1: %2").arg(m_filePath, file.errorString()); + qCWarning(lcConfig, "%s", qUtf8Printable(err)); + emit saveFailed(err); return; } @@ -50,7 +51,7 @@ void RootConfig::setupFileBackend(const QString& path) { qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; - reloadFromFile(); + reload(); if (QFile::exists(m_filePath)) m_watcher->addPath(m_filePath); @@ -64,12 +65,13 @@ void RootConfig::saveToFile() { m_cooldownTimer->start(); } -bool RootConfig::reloadFromFile() { +std::optional RootConfig::reloadFromFile() { QFile file(m_filePath); if (!file.open(QIODevice::ReadOnly)) { - qCDebug(lcConfig, "Failed to open %s", qUtf8Printable(m_filePath)); - return false; + auto err = QStringLiteral("Failed to open %1: %2").arg(m_filePath, file.errorString()); + qCDebug(lcConfig, "%s", qUtf8Printable(err)); + return err; } QJsonParseError error{}; @@ -81,12 +83,12 @@ bool RootConfig::reloadFromFile() { qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString()), m_parseRetries); m_retryTimer->start(); - } else { - qCWarning( - lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); - m_parseRetries = 0; + return std::nullopt; // pending retry — no signal } - return false; + + qCWarning(lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); + m_parseRetries = 0; + return error.errorString(); } m_parseRetries = 0; @@ -96,7 +98,7 @@ bool RootConfig::reloadFromFile() { clearLoadedKeys(); loadFromJson(doc.object()); - return true; + return QString(); // success } void RootConfig::onFileChanged() { @@ -108,10 +110,7 @@ void RootConfig::onFileChanged() { if (m_retryTimer) m_retryTimer->stop(); - if (reloadFromFile()) - emit loaded(); - else - emit loadFailed(QStringLiteral("Failed to load config file")); + reload(); } } @@ -120,8 +119,13 @@ void RootConfig::save() { } void RootConfig::reload() { - if (reloadFromFile()) - emit loaded(); + auto result = reloadFromFile(); + if (result.has_value()) { + if (result->isEmpty()) + emit loaded(); + else + emit loadFailed(*result); + } } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index bb644acde..cfb1f82ba 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -2,6 +2,7 @@ #include "configobject.hpp" +#include #include #include @@ -17,7 +18,8 @@ class RootConfig : public ConfigObject { void setupFileBackend(const QString& path); void saveToFile(); - bool reloadFromFile(); + // Returns nullopt if retrying, empty string on success, error message on failure. + [[nodiscard]] std::optional reloadFromFile(); [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } From b987a39d795d61ad69d450ce7bc5b6d739ab4e4d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:33:29 +1000 Subject: [PATCH 307/409] feat: debounce reloads --- plugin/src/Caelestia/Config/rootconfig.cpp | 14 +++++++------- plugin/src/Caelestia/Config/rootconfig.hpp | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index 193ac4729..bab8e4b85 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -47,6 +47,11 @@ void RootConfig::setupFileBackend(const QString& path) { m_recentlySaved = false; }); + m_reloadDebounce = new QTimer(this); + m_reloadDebounce->setSingleShot(true); + m_reloadDebounce->setInterval(50); + connect(m_reloadDebounce, &QTimer::timeout, this, &RootConfig::reload); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onFileChanged); qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; @@ -105,13 +110,8 @@ void RootConfig::onFileChanged() { if (!m_watcher->files().contains(m_filePath)) m_watcher->addPath(m_filePath); - if (!m_recentlySaved) { - m_parseRetries = 0; - if (m_retryTimer) - m_retryTimer->stop(); - - reload(); - } + if (!m_recentlySaved) + m_reloadDebounce->start(); } void RootConfig::save() { diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index cfb1f82ba..fc9eabae0 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -42,6 +42,7 @@ class RootConfig : public ConfigObject { QTimer* m_saveTimer = nullptr; QTimer* m_cooldownTimer = nullptr; QTimer* m_retryTimer = nullptr; + QTimer* m_reloadDebounce = nullptr; int m_parseRetries = 0; }; From 3e1b235a75872059d1ff84a87c795e24fe42db6a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:41:42 +1000 Subject: [PATCH 308/409] feat: watch parent dirs recursively for changes --- plugin/src/Caelestia/Config/rootconfig.cpp | 64 ++++++++++++++++++---- plugin/src/Caelestia/Config/rootconfig.hpp | 4 +- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index bab8e4b85..79eaf37e7 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -4,9 +4,18 @@ #include #include #include +#include namespace caelestia::config { +namespace { + +QString watchRoot() { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); +} + +} // namespace + RootConfig::RootConfig(QObject* parent) : ConfigObject(parent) {} @@ -38,6 +47,10 @@ void RootConfig::setupFileBackend(const QString& path) { auto json = toJsonObject(); file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); file.close(); + + // Update watches — save may have created directories + updateWatch(); + emit saved(); }); @@ -52,14 +65,51 @@ void RootConfig::setupFileBackend(const QString& path) { m_reloadDebounce->setInterval(50); connect(m_reloadDebounce, &QTimer::timeout, this, &RootConfig::reload); - connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onFileChanged); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RootConfig::onWatcherEvent); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onWatcherEvent); qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; + updateWatch(); reload(); +} - if (QFile::exists(m_filePath)) - m_watcher->addPath(m_filePath); +void RootConfig::updateWatch() { + auto targetDir = QFileInfo(m_filePath).absolutePath(); + + // Find the nearest existing directory, walking up toward the watch root + auto dir = targetDir; + while (!QFile::exists(dir) && dir != watchRoot() && !dir.isEmpty()) { + auto parent = QFileInfo(dir).absolutePath(); + if (parent == dir) + break; // reached filesystem root + dir = parent; + } + + // Update directory watch if it changed + if (dir != m_watchedDir) { + if (!m_watchedDir.isEmpty()) + m_watcher->removePath(m_watchedDir); + + m_watchedDir = dir; + + if (QFile::exists(dir)) + m_watcher->addPath(dir); + } + + // Watch the file itself if it exists (for in-place modifications) + if (QFile::exists(m_filePath)) { + if (!m_watcher->files().contains(m_filePath)) + m_watcher->addPath(m_filePath); + } +} + +void RootConfig::onWatcherEvent() { + // Re-evaluate what to watch — directories may have been created or deleted + updateWatch(); + + if (!m_recentlySaved) + m_reloadDebounce->start(); } void RootConfig::saveToFile() { @@ -106,14 +156,6 @@ std::optional RootConfig::reloadFromFile() { return QString(); // success } -void RootConfig::onFileChanged() { - if (!m_watcher->files().contains(m_filePath)) - m_watcher->addPath(m_filePath); - - if (!m_recentlySaved) - m_reloadDebounce->start(); -} - void RootConfig::save() { saveToFile(); } diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index fc9eabae0..184895479 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -33,9 +33,11 @@ class RootConfig : public ConfigObject { void saveFailed(const QString& error); private: - void onFileChanged(); + void updateWatch(); + void onWatcherEvent(); QString m_filePath; + QString m_watchedDir; bool m_recentlySaved = false; QFileSystemWatcher* m_watcher = nullptr; From ef00097a51af86ef32015ad7571ac016559b91b4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 02:46:42 +1000 Subject: [PATCH 309/409] feat: improve error toasts --- modules/ConfigToasts.qml | 4 ++-- plugin/src/Caelestia/Config/rootconfig.cpp | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/modules/ConfigToasts.qml b/modules/ConfigToasts.qml index 7c43e522f..2e0f5fecb 100644 --- a/modules/ConfigToasts.qml +++ b/modules/ConfigToasts.qml @@ -11,7 +11,7 @@ Scope { } function onLoadFailed(error: string): void { - Toaster.toast(qsTr("Failed to read config file"), error, "settings_alert", Toast.Warning); + Toaster.toast(qsTr("Failed to parse config"), error, "settings_alert", Toast.Warning); } function onSaveFailed(error: string): void { @@ -23,7 +23,7 @@ Scope { Connections { function onLoadFailed(error: string): void { - Toaster.toast(qsTr("Failed to read token config file"), error, "settings_alert", Toast.Warning); + Toaster.toast(qsTr("Failed to parse token config"), error, "settings_alert", Toast.Warning); } target: TokenConfig diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index 79eaf37e7..bc3d1cdc4 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -123,6 +123,11 @@ void RootConfig::saveToFile() { std::optional RootConfig::reloadFromFile() { QFile file(m_filePath); + if (!file.exists()) { + qCDebug(lcConfig) << "File does not exist:" << m_filePath; + return std::nullopt; + } + if (!file.open(QIODevice::ReadOnly)) { auto err = QStringLiteral("Failed to open %1: %2").arg(m_filePath, file.errorString()); qCDebug(lcConfig, "%s", qUtf8Printable(err)); @@ -143,7 +148,7 @@ std::optional RootConfig::reloadFromFile() { qCWarning(lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); m_parseRetries = 0; - return error.errorString(); + return QStringLiteral("JSON parse error: %1").arg(error.errorString()); } m_parseRetries = 0; From ee9dce02682cf0203a1e402cd9cf04a575597169 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:01:58 +1000 Subject: [PATCH 310/409] fix: propagate per monitor overlay signals to main --- modules/ConfigToasts.qml | 12 ++++++------ plugin/src/Caelestia/Config/config.cpp | 6 +++--- plugin/src/Caelestia/Config/config.hpp | 3 ++- .../Caelestia/Config/monitorconfigmanager.cpp | 17 +++++++++++++++-- plugin/src/Caelestia/Config/rootconfig.cpp | 11 ++++++----- plugin/src/Caelestia/Config/rootconfig.hpp | 11 ++++++----- plugin/src/Caelestia/Config/tokens.cpp | 6 +++--- plugin/src/Caelestia/Config/tokens.hpp | 3 ++- 8 files changed, 43 insertions(+), 26 deletions(-) diff --git a/modules/ConfigToasts.qml b/modules/ConfigToasts.qml index 2e0f5fecb..bcba7a7d6 100644 --- a/modules/ConfigToasts.qml +++ b/modules/ConfigToasts.qml @@ -10,20 +10,20 @@ Scope { Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded successfully!"), "rule_settings"); } - function onLoadFailed(error: string): void { - Toaster.toast(qsTr("Failed to parse config"), error, "settings_alert", Toast.Warning); + function onLoadFailed(error: string, screen: string): void { + Toaster.toast(qsTr("Failed to parse config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Warning); } - function onSaveFailed(error: string): void { - Toaster.toast(qsTr("Failed to save config"), error, "settings_alert", Toast.Error); + function onSaveFailed(error: string, screen: string): void { + Toaster.toast(qsTr("Failed to save config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Error); } target: GlobalConfig } Connections { - function onLoadFailed(error: string): void { - Toaster.toast(qsTr("Failed to parse token config"), error, "settings_alert", Toast.Warning); + function onLoadFailed(error: string, screen: string): void { + Toaster.toast(qsTr("Failed to parse token config%1").arg(screen ? "for " + screen : ""), error, "settings_alert", Toast.Warning); } target: TokenConfig diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 7ae638d99..8a7a5c027 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -37,7 +37,7 @@ GlobalConfig::GlobalConfig(QObject* parent) setupFileBackend(configDir() + QStringLiteral("shell.json")); } -GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent) +GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) : RootConfig(parent) , m_appearance(new AppearanceConfig(this)) , m_general(new GeneralConfig(this)) @@ -57,7 +57,7 @@ GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObj , m_services(new ServiceConfig(this)) , m_paths(new UserPaths(this)) { if (!filePath.isEmpty()) - setupFileBackend(filePath); + setupFileBackend(filePath, screen); if (fallback) syncFromGlobal(fallback); @@ -73,7 +73,7 @@ GlobalConfig* GlobalConfig::instance() { GlobalConfig* GlobalConfig::defaults() { if (!m_defaults) - m_defaults = new GlobalConfig(nullptr, QString(), this); // Non-singleton constructor + m_defaults = new GlobalConfig(nullptr, QString(), QString(), this); return m_defaults; } diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 1a9891434..06f5e3e41 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -56,7 +56,8 @@ class GlobalConfig : public RootConfig { private: friend class MonitorConfigManager; explicit GlobalConfig(QObject* parent = nullptr); - explicit GlobalConfig(GlobalConfig* fallback, const QString& filePath, QObject* parent = nullptr); + explicit GlobalConfig( + GlobalConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); GlobalConfig* m_defaults = nullptr; bool m_tokensBound = false; diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp index 473b6567f..3746097f6 100644 --- a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp @@ -32,7 +32,13 @@ GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) { auto& overlay = m_overlays[screen]; if (!overlay.config) { auto dir = monitorConfigDir(screen); - overlay.config = new GlobalConfig(GlobalConfig::instance(), dir + QStringLiteral("shell.json"), this); + overlay.config = new GlobalConfig(GlobalConfig::instance(), dir + QStringLiteral("shell.json"), screen, this); + + auto* const global = GlobalConfig::instance(); + connect(overlay.config, &GlobalConfig::loaded, global, &GlobalConfig::loaded); + connect(overlay.config, &GlobalConfig::saved, global, &GlobalConfig::saved); + connect(overlay.config, &GlobalConfig::loadFailed, global, &GlobalConfig::loadFailed); + connect(overlay.config, &GlobalConfig::saveFailed, global, &GlobalConfig::saveFailed); } return overlay.config; } @@ -41,7 +47,14 @@ TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) { auto& overlay = m_overlays[screen]; if (!overlay.tokens) { auto dir = monitorConfigDir(screen); - overlay.tokens = new TokenConfig(TokenConfig::instance(), dir + QStringLiteral("shell-tokens.json"), this); + overlay.tokens = + new TokenConfig(TokenConfig::instance(), dir + QStringLiteral("shell-tokens.json"), screen, this); + + auto* const global = TokenConfig::instance(); + connect(overlay.tokens, &TokenConfig::loaded, global, &TokenConfig::loaded); + connect(overlay.tokens, &TokenConfig::saved, global, &TokenConfig::saved); + connect(overlay.tokens, &TokenConfig::loadFailed, global, &TokenConfig::loadFailed); + connect(overlay.tokens, &TokenConfig::saveFailed, global, &TokenConfig::saveFailed); } return overlay.tokens; } diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index bc3d1cdc4..fd90122a9 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -19,8 +19,9 @@ QString watchRoot() { RootConfig::RootConfig(QObject* parent) : ConfigObject(parent) {} -void RootConfig::setupFileBackend(const QString& path) { +void RootConfig::setupFileBackend(const QString& path, const QString& screen) { m_filePath = path; + m_screen = screen; m_watcher = new QFileSystemWatcher(this); m_saveTimer = new QTimer(this); @@ -40,7 +41,7 @@ void RootConfig::setupFileBackend(const QString& path) { if (!file.open(QIODevice::WriteOnly)) { auto err = QStringLiteral("Failed to write %1: %2").arg(m_filePath, file.errorString()); qCWarning(lcConfig, "%s", qUtf8Printable(err)); - emit saveFailed(err); + emit saveFailed(err, m_screen); return; } @@ -51,7 +52,7 @@ void RootConfig::setupFileBackend(const QString& path) { // Update watches — save may have created directories updateWatch(); - emit saved(); + emit saved(m_screen); }); m_cooldownTimer->setSingleShot(true); @@ -169,9 +170,9 @@ void RootConfig::reload() { auto result = reloadFromFile(); if (result.has_value()) { if (result->isEmpty()) - emit loaded(); + emit loaded(m_screen); else - emit loadFailed(*result); + emit loadFailed(*result, m_screen); } } diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index 184895479..a7a0c72b0 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -16,7 +16,7 @@ class RootConfig : public ConfigObject { public: explicit RootConfig(QObject* parent = nullptr); - void setupFileBackend(const QString& path); + void setupFileBackend(const QString& path, const QString& screen = {}); void saveToFile(); // Returns nullopt if retrying, empty string on success, error message on failure. [[nodiscard]] std::optional reloadFromFile(); @@ -27,16 +27,17 @@ class RootConfig : public ConfigObject { Q_INVOKABLE void reload(); signals: - void loaded(); - void loadFailed(const QString& error); - void saved(); - void saveFailed(const QString& error); + void loaded(const QString& screen); + void loadFailed(const QString& error, const QString& screen); + void saved(const QString& screen); + void saveFailed(const QString& error, const QString& screen); private: void updateWatch(); void onWatcherEvent(); QString m_filePath; + QString m_screen; QString m_watchedDir; bool m_recentlySaved = false; diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 74cd827b2..3f8000716 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -22,12 +22,12 @@ TokenConfig::TokenConfig(QObject* parent) setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); } -TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent) +TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) : RootConfig(parent) , m_appearance(new AppearanceTokens(this)) , m_sizes(new SizeTokens(this)) { if (!filePath.isEmpty()) - setupFileBackend(filePath); + setupFileBackend(filePath, screen); if (fallback) syncFromGlobal(fallback); } @@ -39,7 +39,7 @@ TokenConfig* TokenConfig::instance() { TokenConfig* TokenConfig::defaults() { if (!m_defaults) - m_defaults = new TokenConfig(nullptr, QString(), this); // Non-singleton constructor + m_defaults = new TokenConfig(nullptr, QString(), QString(), this); return m_defaults; } diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 231cbb52a..462d3f626 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -335,7 +335,8 @@ class TokenConfig : public RootConfig { private: friend class MonitorConfigManager; explicit TokenConfig(QObject* parent = nullptr); - explicit TokenConfig(TokenConfig* fallback, const QString& filePath, QObject* parent = nullptr); + explicit TokenConfig( + TokenConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); TokenConfig* m_defaults = nullptr; }; From 5a13cc22f618c65719e5bc53fcc97d0079a285b7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:12:13 +1000 Subject: [PATCH 311/409] feat: add unknown option toasts --- modules/ConfigToasts.qml | 8 ++++ .../Caelestia/Config/monitorconfigmanager.cpp | 2 + plugin/src/Caelestia/Config/rootconfig.cpp | 38 ++++++++++++++++++- plugin/src/Caelestia/Config/rootconfig.hpp | 2 + 4 files changed, 49 insertions(+), 1 deletion(-) diff --git a/modules/ConfigToasts.qml b/modules/ConfigToasts.qml index bcba7a7d6..81463b8d8 100644 --- a/modules/ConfigToasts.qml +++ b/modules/ConfigToasts.qml @@ -18,6 +18,10 @@ Scope { Toaster.toast(qsTr("Failed to save config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Error); } + function onUnknownOption(key: string, screen: string): void { + Toaster.toast(qsTr("Unknown option in%1 config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); + } + target: GlobalConfig } @@ -26,6 +30,10 @@ Scope { Toaster.toast(qsTr("Failed to parse token config%1").arg(screen ? "for " + screen : ""), error, "settings_alert", Toast.Warning); } + function onUnknownOption(key: string, screen: string): void { + Toaster.toast(qsTr("Unknown option in%1 token config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); + } + target: TokenConfig } } diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp index 3746097f6..fb1745ec6 100644 --- a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp @@ -39,6 +39,7 @@ GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) { connect(overlay.config, &GlobalConfig::saved, global, &GlobalConfig::saved); connect(overlay.config, &GlobalConfig::loadFailed, global, &GlobalConfig::loadFailed); connect(overlay.config, &GlobalConfig::saveFailed, global, &GlobalConfig::saveFailed); + connect(overlay.config, &GlobalConfig::unknownOption, global, &GlobalConfig::unknownOption); } return overlay.config; } @@ -55,6 +56,7 @@ TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) { connect(overlay.tokens, &TokenConfig::saved, global, &TokenConfig::saved); connect(overlay.tokens, &TokenConfig::loadFailed, global, &TokenConfig::loadFailed); connect(overlay.tokens, &TokenConfig::saveFailed, global, &TokenConfig::saveFailed); + connect(overlay.tokens, &TokenConfig::unknownOption, global, &TokenConfig::unknownOption); } return overlay.tokens; } diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index fd90122a9..8a10c9503 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace caelestia::config { @@ -19,6 +20,35 @@ QString watchRoot() { RootConfig::RootConfig(QObject* parent) : ConfigObject(parent) {} +QStringList RootConfig::collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json) { + QStringList unknown; + const auto* meta = obj->metaObject(); + + QSet known; + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) + known.insert(QString::fromUtf8(meta->property(i).name())); + + for (auto it = json.begin(); it != json.end(); ++it) { + if (!known.contains(it.key())) { + unknown.append(it.key()); + } else if (it.value().isObject()) { + int idx = meta->indexOfProperty(it.key().toUtf8().constData()); + if (idx >= 0) { + auto prop = meta->property(idx); + auto value = prop.read(obj); + auto* subObj = value.value(); + if (subObj) { + const auto subUnknown = collectUnknownKeys(subObj, it.value().toObject()); + for (const auto& subKey : subUnknown) + unknown.append(it.key() + QStringLiteral(".") + subKey); + } + } + } + } + + return unknown; +} + void RootConfig::setupFileBackend(const QString& path, const QString& screen) { m_filePath = path; m_screen = screen; @@ -157,7 +187,13 @@ std::optional RootConfig::reloadFromFile() { qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; clearLoadedKeys(); - loadFromJson(doc.object()); + + auto jsonObj = doc.object(); + const auto unknownKeys = collectUnknownKeys(this, jsonObj); + for (const auto& key : unknownKeys) + emit unknownOption(key, m_screen); + + loadFromJson(jsonObj); return QString(); // success } diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index a7a0c72b0..25b690029 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -31,8 +31,10 @@ class RootConfig : public ConfigObject { void loadFailed(const QString& error, const QString& screen); void saved(const QString& screen); void saveFailed(const QString& error, const QString& screen); + void unknownOption(const QString& key, const QString& screen); private: + static QStringList collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json); void updateWatch(); void onWatcherEvent(); From bea72b4a640786c84495705bf7cd9af09111c1b8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:20:25 +1000 Subject: [PATCH 312/409] feat: emit signals on startup Except loaded signal --- plugin/src/Caelestia/Config/rootconfig.cpp | 37 +++++++++++++++------- plugin/src/Caelestia/Config/rootconfig.hpp | 2 ++ 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index 8a10c9503..611abd575 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -102,7 +102,13 @@ void RootConfig::setupFileBackend(const QString& path, const QString& screen) { qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; updateWatch(); - reload(); + + // Load immediately so values are available during construction. + // Defer signal emissions to next event loop tick so QML has time to connect. + auto result = reloadFromFile(); + QTimer::singleShot(0, this, [this, result] { + emitLoadSignals(result, false); + }); } void RootConfig::updateWatch() { @@ -189,12 +195,11 @@ std::optional RootConfig::reloadFromFile() { clearLoadedKeys(); auto jsonObj = doc.object(); - const auto unknownKeys = collectUnknownKeys(this, jsonObj); - for (const auto& key : unknownKeys) - emit unknownOption(key, m_screen); - loadFromJson(jsonObj); + // Collect unknown keys — caller is responsible for emitting signals + m_lastUnknownKeys = collectUnknownKeys(this, jsonObj); + return QString(); // success } @@ -202,14 +207,24 @@ void RootConfig::save() { saveToFile(); } -void RootConfig::reload() { - auto result = reloadFromFile(); - if (result.has_value()) { - if (result->isEmpty()) +void RootConfig::emitLoadSignals(const std::optional& result, bool emitLoaded) { + if (!result.has_value()) + return; + + for (const auto& key : std::as_const(m_lastUnknownKeys)) + emit unknownOption(key, m_screen); + m_lastUnknownKeys.clear(); + + if (result->isEmpty()) { + if (emitLoaded) emit loaded(m_screen); - else - emit loadFailed(*result, m_screen); + } else { + emit loadFailed(*result, m_screen); } } +void RootConfig::reload() { + emitLoadSignals(reloadFromFile()); +} + } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index 25b690029..fb6a68b52 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -35,6 +35,7 @@ class RootConfig : public ConfigObject { private: static QStringList collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json); + void emitLoadSignals(const std::optional& result, bool emitLoaded = true); void updateWatch(); void onWatcherEvent(); @@ -49,6 +50,7 @@ class RootConfig : public ConfigObject { QTimer* m_retryTimer = nullptr; QTimer* m_reloadDebounce = nullptr; int m_parseRetries = 0; + QStringList m_lastUnknownKeys; }; } // namespace caelestia::config From 41ba4f62714fda70c1ce60985ac0c0612ad397a2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:39:58 +1000 Subject: [PATCH 313/409] feat: add per monitor enabled prop Enabled prop should be used in favour of general.excludedScreens. The latter has been removed --- README.md | 1 + plugin/src/Caelestia/Config/config.cpp | 9 +++++++++ plugin/src/Caelestia/Config/config.hpp | 4 ++++ plugin/src/Caelestia/Config/generalconfig.hpp | 1 - plugin/src/Caelestia/Config/tokens.cpp | 9 +++++++++ plugin/src/Caelestia/Config/tokens.hpp | 3 +++ services/Screens.qml | 9 ++------- 7 files changed, 28 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 8c6763bd8..d7e4c7c65 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,7 @@ For example, to disable the bar on DP-1: ```json { + "enabled": true, "appearance": { "anim": { "durations": { diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 8a7a5c027..b68f42471 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -1,5 +1,6 @@ #include "config.hpp" #include "configscope.hpp" +#include "monitorconfigmanager.hpp" #include "tokens.hpp" #include @@ -91,6 +92,10 @@ void GlobalConfig::bindAppearanceTokens() { m_tokensBound = true; } +GlobalConfig* GlobalConfig::forScreen(const QString& screen) { + return MonitorConfigManager::instance()->configForScreen(screen); +} + GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); return instance(); @@ -139,6 +144,10 @@ CONFIG_ATTACHED_GETTER(UserPaths, paths) #undef CONFIG_ATTACHED_GETTER +GlobalConfig* Config::forScreen(const QString& screen) { + return GlobalConfig::forScreen(screen); +} + Config* Config::qmlAttachedProperties(QObject* object) { return new Config(ConfigScope::find(object), object); } diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 06f5e3e41..1c34f0399 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -28,6 +28,7 @@ class GlobalConfig : public RootConfig { QML_ELEMENT QML_SINGLETON + CONFIG_PROPERTY(bool, enabled, true) CONFIG_SUBOBJECT(AppearanceConfig, appearance) CONFIG_SUBOBJECT(GeneralConfig, general) CONFIG_SUBOBJECT(BackgroundConfig, background) @@ -49,6 +50,7 @@ class GlobalConfig : public RootConfig { public: static GlobalConfig* instance(); [[nodiscard]] Q_INVOKABLE GlobalConfig* defaults(); + [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); static GlobalConfig* create(QQmlEngine*, QJSEngine*); void bindAppearanceTokens(); @@ -114,6 +116,8 @@ class Config : public QObject { [[nodiscard]] const ServiceConfig* services() const; [[nodiscard]] const UserPaths* paths() const; + [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); + static Config* qmlAttachedProperties(QObject* object); signals: diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp index 24982e61b..32b7df1ec 100644 --- a/plugin/src/Caelestia/Config/generalconfig.hpp +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -52,7 +52,6 @@ class GeneralConfig : public ConfigObject { QML_ANONYMOUS CONFIG_PROPERTY(QString, logo) - CONFIG_PROPERTY(QStringList, excludedScreens) CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) CONFIG_SUBOBJECT(GeneralApps, apps) diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 3f8000716..84d3cef73 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -1,6 +1,7 @@ #include "tokens.hpp" #include "config.hpp" #include "configscope.hpp" +#include "monitorconfigmanager.hpp" #include #include @@ -43,6 +44,10 @@ TokenConfig* TokenConfig::defaults() { return m_defaults; } +TokenConfig* TokenConfig::forScreen(const QString& screen) { + return MonitorConfigManager::instance()->tokensForScreen(screen); +} + TokenConfig* TokenConfig::create(QQmlEngine*, QJSEngine*) { QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); return instance(); @@ -119,6 +124,10 @@ const SizeTokens* Tokens::sizes() const { return global ? global->sizes() : nullptr; } +TokenConfig* Tokens::forScreen(const QString& screen) { + return TokenConfig::forScreen(screen); +} + Tokens* Tokens::qmlAttachedProperties(QObject* object) { return new Tokens(ConfigScope::find(object), object); } diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 462d3f626..2448b737a 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -330,6 +330,7 @@ class TokenConfig : public RootConfig { public: static TokenConfig* instance(); [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); + [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); static TokenConfig* create(QQmlEngine*, QJSEngine*); private: @@ -369,6 +370,8 @@ class Tokens : public QObject { [[nodiscard]] const AnimTokens* anim() const { return m_anim; } + [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); + static Tokens* qmlAttachedProperties(QObject* object); signals: diff --git a/services/Screens.qml b/services/Screens.qml index 7e1b85df6..f902a38d3 100644 --- a/services/Screens.qml +++ b/services/Screens.qml @@ -7,14 +7,9 @@ import qs.utils Singleton { id: root - readonly property list screens: { - const excluded = Config.general.excludedScreens; - if (excluded.length === 0) - return Quickshell.screens; - return Quickshell.screens.filter(s => !Strings.testRegexList(excluded, s.name)); - } + readonly property list screens: Quickshell.screens.filter(s => GlobalConfig.forScreen(s.name).enabled) function isExcluded(screen: ShellScreen): bool { - return Strings.testRegexList(Config.general.excludedScreens, screen.name); + return !GlobalConfig.forScreen(screen.name).enabled; } } From 3d012e86b325156ab7d16bcd069db572c01efaf3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:45:17 +1000 Subject: [PATCH 314/409] chore: remove old Config.save() --- modules/controlcenter/appearance/AppearancePane.qml | 2 -- modules/controlcenter/dashboard/DashboardPane.qml | 1 - modules/controlcenter/launcher/LauncherPane.qml | 3 --- modules/controlcenter/launcher/Settings.qml | 9 --------- modules/controlcenter/network/NetworkSettings.qml | 1 - modules/controlcenter/network/VpnDetails.qml | 3 --- modules/controlcenter/network/VpnList.qml | 7 ------- modules/controlcenter/network/VpnSettings.qml | 6 ------ .../controlcenter/notifications/NotificationsPane.qml | 2 -- modules/controlcenter/taskbar/TaskbarPane.qml | 1 - services/LyricsService.qml | 2 -- 11 files changed, 37 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 1c787f36e..7111d83f3 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -91,8 +91,6 @@ Item { Config.border.rounding = root.borderRounding; Config.border.thickness = root.borderThickness; - - Config.save(); } anchors.fill: parent diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 83bc1e169..eed077c95 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -57,7 +57,6 @@ Item { Config.dashboard.performance.showStorage = root.showStorage; Config.dashboard.performance.showNetwork = root.showNetwork; // Note: sizes properties are readonly and cannot be modified - Config.save(); } anchors.fill: parent diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 83c2232b4..c29243c23 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -62,7 +62,6 @@ Item { } Config.launcher.hiddenApps = hiddenApps; - Config.save(); } function filterApps(search: string): list { @@ -626,7 +625,6 @@ Item { } } Config.launcher.favouriteApps = favouriteApps; - Config.save(); } } } @@ -659,7 +657,6 @@ Item { } } Config.launcher.hiddenApps = hiddenApps; - Config.save(); } } } diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 61bab0c15..ce570737e 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -34,7 +34,6 @@ ColumnLayout { checked: Config.launcher.enabled toggle.onToggled: { Config.launcher.enabled = checked; - Config.save(); } } @@ -43,7 +42,6 @@ ColumnLayout { checked: Config.launcher.showOnHover toggle.onToggled: { Config.launcher.showOnHover = checked; - Config.save(); } } @@ -52,7 +50,6 @@ ColumnLayout { checked: Config.launcher.vimKeybinds toggle.onToggled: { Config.launcher.vimKeybinds = checked; - Config.save(); } } @@ -61,7 +58,6 @@ ColumnLayout { checked: Config.launcher.enableDangerousActions toggle.onToggled: { Config.launcher.enableDangerousActions = checked; - Config.save(); } } } @@ -126,7 +122,6 @@ ColumnLayout { checked: Config.launcher.useFuzzy.apps toggle.onToggled: { Config.launcher.useFuzzy.apps = checked; - Config.save(); } } @@ -135,7 +130,6 @@ ColumnLayout { checked: Config.launcher.useFuzzy.actions toggle.onToggled: { Config.launcher.useFuzzy.actions = checked; - Config.save(); } } @@ -144,7 +138,6 @@ ColumnLayout { checked: Config.launcher.useFuzzy.schemes toggle.onToggled: { Config.launcher.useFuzzy.schemes = checked; - Config.save(); } } @@ -153,7 +146,6 @@ ColumnLayout { checked: Config.launcher.useFuzzy.variants toggle.onToggled: { Config.launcher.useFuzzy.variants = checked; - Config.save(); } } @@ -162,7 +154,6 @@ ColumnLayout { checked: Config.launcher.useFuzzy.wallpapers toggle.onToggled: { Config.launcher.useFuzzy.wallpapers = checked; - Config.save(); } } } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 22d85188b..3e720ea4e 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -76,7 +76,6 @@ ColumnLayout { checked: Config.utilities.vpn.enabled toggle.onToggled: { Config.utilities.vpn.enabled = checked; - Config.save(); } } diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 3ea85d32a..fcdd9a8af 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -86,7 +86,6 @@ DeviceDetails { } Config.utilities.vpn.provider = providers; - Config.save(); } } @@ -141,7 +140,6 @@ DeviceDetails { } } Config.utilities.vpn.provider = providers; - Config.save(); root.session.vpn.active = null; } } @@ -540,7 +538,6 @@ DeviceDetails { } Config.utilities.vpn.provider = providers; - Config.save(); editVpnDialog.closeWithAnimation(); } } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index e0376fc63..957ce87ee 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -48,7 +48,6 @@ ColumnLayout { } } Config.utilities.vpn.provider = providers; - Config.save(); Qt.callLater(function () { VPN.toggle(); @@ -244,7 +243,6 @@ ColumnLayout { } } Config.utilities.vpn.provider = providers; - Config.save(); Qt.callLater(function () { VPN.toggle(); @@ -294,7 +292,6 @@ ColumnLayout { } } Config.utilities.vpn.provider = providers; - Config.save(); } } @@ -496,7 +493,6 @@ ColumnLayout { interface: "wt0" }); Config.utilities.vpn.provider = providers; - Config.save(); vpnDialog.closeWithAnimation(); } } @@ -517,7 +513,6 @@ ColumnLayout { interface: "tailscale0" }); Config.utilities.vpn.provider = providers; - Config.save(); vpnDialog.closeWithAnimation(); } } @@ -538,7 +533,6 @@ ColumnLayout { interface: "CloudflareWARP" }); Config.utilities.vpn.provider = providers; - Config.save(); vpnDialog.closeWithAnimation(); } } @@ -806,7 +800,6 @@ ColumnLayout { } Config.utilities.vpn.provider = providers; - Config.save(); vpnDialog.closeWithAnimation(); } } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index e0f0e7f50..30b5403b0 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -37,7 +37,6 @@ ColumnLayout { checked: Config.utilities.vpn.enabled toggle.onToggled: { Config.utilities.vpn.enabled = checked; - Config.save(); } } } @@ -149,7 +148,6 @@ ColumnLayout { } Config.utilities.vpn.provider = providers; - Config.save(); } } @@ -176,7 +174,6 @@ ColumnLayout { } } Config.utilities.vpn.provider = providers; - Config.save(); } } } @@ -220,7 +217,6 @@ ColumnLayout { interface: "wt0" }); Config.utilities.vpn.provider = providers; - Config.save(); } } @@ -238,7 +234,6 @@ ColumnLayout { interface: "tailscale0" }); Config.utilities.vpn.provider = providers; - Config.save(); } } @@ -256,7 +251,6 @@ ColumnLayout { interface: "CloudflareWARP" }); Config.utilities.vpn.provider = providers; - Config.save(); } } } diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml index 919a2549d..a522dca42 100644 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -56,8 +56,6 @@ Item { Config.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; Config.utilities.toasts.vpnChanged = root.vpnChanged; Config.utilities.toasts.nowPlaying = root.nowPlaying; - - Config.save(); } anchors.fill: parent diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 5715b8e27..4c3ff8af1 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -100,7 +100,6 @@ Item { }); } Config.bar.entries = entries; - Config.save(); } anchors.fill: parent diff --git a/services/LyricsService.qml b/services/LyricsService.qml index 7fd63670f..dc5f600f9 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -69,7 +69,6 @@ Singleton { function toggleVisibility() { Config.services.showLyrics = !Config.services.showLyrics; - Config.save(); } function loadLyrics() { @@ -274,7 +273,6 @@ Singleton { onPreferredBackendChanged: { if (Config.services.lyricsBackend !== preferredBackend) { Config.services.lyricsBackend = preferredBackend; - Config.save(); } } From 42b9e57eae90e07f6400ec3f5b812d532911561b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:46:37 +1000 Subject: [PATCH 315/409] fix: use singleton for conf writes --- .../appearance/AppearancePane.qml | 72 +++++++++---------- .../controlcenter/dashboard/DashboardPane.qml | 30 ++++---- .../controlcenter/launcher/LauncherPane.qml | 6 +- modules/controlcenter/launcher/Settings.qml | 18 ++--- .../controlcenter/network/NetworkSettings.qml | 2 +- modules/controlcenter/network/VpnDetails.qml | 6 +- modules/controlcenter/network/VpnList.qml | 14 ++-- modules/controlcenter/network/VpnSettings.qml | 12 ++-- .../notifications/NotificationsPane.qml | 36 +++++----- modules/controlcenter/taskbar/TaskbarPane.qml | 66 ++++++++--------- services/LyricsService.qml | 4 +- 11 files changed, 133 insertions(+), 133 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 7111d83f3..6ede9bb37 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -55,42 +55,42 @@ Item { property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 function saveConfig() { - Config.appearance.anim.durations.scale = root.animDurationsScale; - - Config.appearance.font.family.material = root.fontFamilyMaterial; - Config.appearance.font.family.mono = root.fontFamilyMono; - Config.appearance.font.family.sans = root.fontFamilySans; - Config.appearance.font.size.scale = root.fontSizeScale; - - Config.appearance.padding.scale = root.paddingScale; - Config.appearance.rounding.scale = root.roundingScale; - Config.appearance.spacing.scale = root.spacingScale; - - Config.appearance.transparency.enabled = root.transparencyEnabled; - Config.appearance.transparency.base = root.transparencyBase; - Config.appearance.transparency.layers = root.transparencyLayers; - - Config.background.desktopClock.enabled = root.desktopClockEnabled; - Config.background.enabled = root.backgroundEnabled; - Config.background.desktopClock.scale = root.desktopClockScale; - Config.background.desktopClock.position = root.desktopClockPosition; - Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; - Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; - Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; - Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; - Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; - Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; - Config.background.desktopClock.invertColors = root.desktopClockInvertColors; - - Config.background.wallpaperEnabled = root.wallpaperEnabled; - - Config.background.visualiser.enabled = root.visualiserEnabled; - Config.background.visualiser.autoHide = root.visualiserAutoHide; - Config.background.visualiser.rounding = root.visualiserRounding; - Config.background.visualiser.spacing = root.visualiserSpacing; - - Config.border.rounding = root.borderRounding; - Config.border.thickness = root.borderThickness; + GlobalConfig.appearance.anim.durations.scale = root.animDurationsScale; + + GlobalConfig.appearance.font.family.material = root.fontFamilyMaterial; + GlobalConfig.appearance.font.family.mono = root.fontFamilyMono; + GlobalConfig.appearance.font.family.sans = root.fontFamilySans; + GlobalConfig.appearance.font.size.scale = root.fontSizeScale; + + GlobalConfig.appearance.padding.scale = root.paddingScale; + GlobalConfig.appearance.rounding.scale = root.roundingScale; + GlobalConfig.appearance.spacing.scale = root.spacingScale; + + GlobalConfig.appearance.transparency.enabled = root.transparencyEnabled; + GlobalConfig.appearance.transparency.base = root.transparencyBase; + GlobalConfig.appearance.transparency.layers = root.transparencyLayers; + + GlobalConfig.background.desktopClock.enabled = root.desktopClockEnabled; + GlobalConfig.background.enabled = root.backgroundEnabled; + GlobalConfig.background.desktopClock.scale = root.desktopClockScale; + GlobalConfig.background.desktopClock.position = root.desktopClockPosition; + GlobalConfig.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; + GlobalConfig.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; + GlobalConfig.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; + GlobalConfig.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; + GlobalConfig.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; + GlobalConfig.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; + GlobalConfig.background.desktopClock.invertColors = root.desktopClockInvertColors; + + GlobalConfig.background.wallpaperEnabled = root.wallpaperEnabled; + + GlobalConfig.background.visualiser.enabled = root.visualiserEnabled; + GlobalConfig.background.visualiser.autoHide = root.visualiserAutoHide; + GlobalConfig.background.visualiser.rounding = root.visualiserRounding; + GlobalConfig.background.visualiser.spacing = root.visualiserSpacing; + + GlobalConfig.border.rounding = root.borderRounding; + GlobalConfig.border.thickness = root.borderThickness; } anchors.fill: parent diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index eed077c95..46e33ebcf 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -41,21 +41,21 @@ Item { property bool showNetwork: Config.dashboard.performance.showNetwork ?? true function saveConfig() { - Config.dashboard.enabled = root.enabled; - Config.dashboard.showOnHover = root.showOnHover; - Config.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; - Config.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; - Config.dashboard.dragThreshold = root.dragThreshold; - Config.dashboard.showDashboard = root.showDashboard; - Config.dashboard.showMedia = root.showMedia; - Config.dashboard.showPerformance = root.showPerformance; - Config.dashboard.showWeather = root.showWeather; - Config.dashboard.performance.showBattery = root.showBattery; - Config.dashboard.performance.showGpu = root.showGpu; - Config.dashboard.performance.showCpu = root.showCpu; - Config.dashboard.performance.showMemory = root.showMemory; - Config.dashboard.performance.showStorage = root.showStorage; - Config.dashboard.performance.showNetwork = root.showNetwork; + GlobalConfig.dashboard.enabled = root.enabled; + GlobalConfig.dashboard.showOnHover = root.showOnHover; + GlobalConfig.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; + GlobalConfig.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; + GlobalConfig.dashboard.dragThreshold = root.dragThreshold; + GlobalConfig.dashboard.showDashboard = root.showDashboard; + GlobalConfig.dashboard.showMedia = root.showMedia; + GlobalConfig.dashboard.showPerformance = root.showPerformance; + GlobalConfig.dashboard.showWeather = root.showWeather; + GlobalConfig.dashboard.performance.showBattery = root.showBattery; + GlobalConfig.dashboard.performance.showGpu = root.showGpu; + GlobalConfig.dashboard.performance.showCpu = root.showCpu; + GlobalConfig.dashboard.performance.showMemory = root.showMemory; + GlobalConfig.dashboard.performance.showStorage = root.showStorage; + GlobalConfig.dashboard.performance.showNetwork = root.showNetwork; // Note: sizes properties are readonly and cannot be modified } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index c29243c23..e5353fbab 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -61,7 +61,7 @@ Item { } } - Config.launcher.hiddenApps = hiddenApps; + GlobalConfig.launcher.hiddenApps = hiddenApps; } function filterApps(search: string): list { @@ -624,7 +624,7 @@ Item { favouriteApps.splice(index, 1); } } - Config.launcher.favouriteApps = favouriteApps; + GlobalConfig.launcher.favouriteApps = favouriteApps; } } } @@ -656,7 +656,7 @@ Item { hiddenApps.splice(index, 1); } } - Config.launcher.hiddenApps = hiddenApps; + GlobalConfig.launcher.hiddenApps = hiddenApps; } } } diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index ce570737e..df25a827f 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -33,7 +33,7 @@ ColumnLayout { label: qsTr("Enabled") checked: Config.launcher.enabled toggle.onToggled: { - Config.launcher.enabled = checked; + GlobalConfig.launcher.enabled = checked; } } @@ -41,7 +41,7 @@ ColumnLayout { label: qsTr("Show on hover") checked: Config.launcher.showOnHover toggle.onToggled: { - Config.launcher.showOnHover = checked; + GlobalConfig.launcher.showOnHover = checked; } } @@ -49,7 +49,7 @@ ColumnLayout { label: qsTr("Vim keybinds") checked: Config.launcher.vimKeybinds toggle.onToggled: { - Config.launcher.vimKeybinds = checked; + GlobalConfig.launcher.vimKeybinds = checked; } } @@ -57,7 +57,7 @@ ColumnLayout { label: qsTr("Enable dangerous actions") checked: Config.launcher.enableDangerousActions toggle.onToggled: { - Config.launcher.enableDangerousActions = checked; + GlobalConfig.launcher.enableDangerousActions = checked; } } } @@ -121,7 +121,7 @@ ColumnLayout { label: qsTr("Apps") checked: Config.launcher.useFuzzy.apps toggle.onToggled: { - Config.launcher.useFuzzy.apps = checked; + GlobalConfig.launcher.useFuzzy.apps = checked; } } @@ -129,7 +129,7 @@ ColumnLayout { label: qsTr("Actions") checked: Config.launcher.useFuzzy.actions toggle.onToggled: { - Config.launcher.useFuzzy.actions = checked; + GlobalConfig.launcher.useFuzzy.actions = checked; } } @@ -137,7 +137,7 @@ ColumnLayout { label: qsTr("Schemes") checked: Config.launcher.useFuzzy.schemes toggle.onToggled: { - Config.launcher.useFuzzy.schemes = checked; + GlobalConfig.launcher.useFuzzy.schemes = checked; } } @@ -145,7 +145,7 @@ ColumnLayout { label: qsTr("Variants") checked: Config.launcher.useFuzzy.variants toggle.onToggled: { - Config.launcher.useFuzzy.variants = checked; + GlobalConfig.launcher.useFuzzy.variants = checked; } } @@ -153,7 +153,7 @@ ColumnLayout { label: qsTr("Wallpapers") checked: Config.launcher.useFuzzy.wallpapers toggle.onToggled: { - Config.launcher.useFuzzy.wallpapers = checked; + GlobalConfig.launcher.useFuzzy.wallpapers = checked; } } } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 3e720ea4e..5ed755fe8 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -75,7 +75,7 @@ ColumnLayout { label: qsTr("VPN enabled") checked: Config.utilities.vpn.enabled toggle.onToggled: { - Config.utilities.vpn.enabled = checked; + GlobalConfig.utilities.vpn.enabled = checked; } } diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index fcdd9a8af..f0197f9c9 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -85,7 +85,7 @@ DeviceDetails { } } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; } } @@ -139,7 +139,7 @@ DeviceDetails { providers.push(Config.utilities.vpn.provider[i]); } } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; root.session.vpn.active = null; } } @@ -537,7 +537,7 @@ DeviceDetails { } } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; editVpnDialog.closeWithAnimation(); } } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 957ce87ee..dd63300d1 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -47,7 +47,7 @@ ColumnLayout { providers.push(p); } } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; Qt.callLater(function () { VPN.toggle(); @@ -242,7 +242,7 @@ ColumnLayout { providers.push(p); } } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; Qt.callLater(function () { VPN.toggle(); @@ -291,7 +291,7 @@ ColumnLayout { providers.push(reconstructed); } } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; } } @@ -492,7 +492,7 @@ ColumnLayout { displayName: "NetBird", interface: "wt0" }); - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } @@ -512,7 +512,7 @@ ColumnLayout { displayName: "Tailscale", interface: "tailscale0" }); - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } @@ -532,7 +532,7 @@ ColumnLayout { displayName: "Cloudflare WARP", interface: "CloudflareWARP" }); - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } @@ -799,7 +799,7 @@ ColumnLayout { providers.push(newProvider); } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 30b5403b0..27c1cc8e9 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -36,7 +36,7 @@ ColumnLayout { label: qsTr("VPN enabled") checked: Config.utilities.vpn.enabled toggle.onToggled: { - Config.utilities.vpn.enabled = checked; + GlobalConfig.utilities.vpn.enabled = checked; } } } @@ -147,7 +147,7 @@ ColumnLayout { providers.unshift(provider); } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; } } @@ -173,7 +173,7 @@ ColumnLayout { providers.push(reconstructed); } } - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; } } } @@ -216,7 +216,7 @@ ColumnLayout { displayName: "NetBird", interface: "wt0" }); - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; } } @@ -233,7 +233,7 @@ ColumnLayout { displayName: "Tailscale", interface: "tailscale0" }); - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; } } @@ -250,7 +250,7 @@ ColumnLayout { displayName: "Cloudflare WARP", interface: "CloudflareWARP" }); - Config.utilities.vpn.provider = providers; + GlobalConfig.utilities.vpn.provider = providers; } } } diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml index a522dca42..ea75c2a67 100644 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -38,24 +38,24 @@ Item { property bool nowPlaying: Config.utilities.toasts.nowPlaying ?? false function saveConfig(): void { - Config.notifs.expire = root.notificationsExpire; - Config.notifs.fullscreen = root.notificationsFullscreen; - Config.notifs.openExpanded = root.notificationsOpenExpanded; - Config.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; - Config.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; - - Config.utilities.maxToasts = root.maxToasts; - Config.utilities.toasts.fullscreen = root.toastsFullscreen; - Config.utilities.toasts.chargingChanged = root.chargingChanged; - Config.utilities.toasts.gameModeChanged = root.gameModeChanged; - Config.utilities.toasts.dndChanged = root.dndChanged; - Config.utilities.toasts.audioOutputChanged = root.audioOutputChanged; - Config.utilities.toasts.audioInputChanged = root.audioInputChanged; - Config.utilities.toasts.capsLockChanged = root.capsLockChanged; - Config.utilities.toasts.numLockChanged = root.numLockChanged; - Config.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; - Config.utilities.toasts.vpnChanged = root.vpnChanged; - Config.utilities.toasts.nowPlaying = root.nowPlaying; + GlobalConfig.notifs.expire = root.notificationsExpire; + GlobalConfig.notifs.fullscreen = root.notificationsFullscreen; + GlobalConfig.notifs.openExpanded = root.notificationsOpenExpanded; + GlobalConfig.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; + GlobalConfig.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; + + GlobalConfig.utilities.maxToasts = root.maxToasts; + GlobalConfig.utilities.toasts.fullscreen = root.toastsFullscreen; + GlobalConfig.utilities.toasts.chargingChanged = root.chargingChanged; + GlobalConfig.utilities.toasts.gameModeChanged = root.gameModeChanged; + GlobalConfig.utilities.toasts.dndChanged = root.dndChanged; + GlobalConfig.utilities.toasts.audioOutputChanged = root.audioOutputChanged; + GlobalConfig.utilities.toasts.audioInputChanged = root.audioInputChanged; + GlobalConfig.utilities.toasts.capsLockChanged = root.capsLockChanged; + GlobalConfig.utilities.toasts.numLockChanged = root.numLockChanged; + GlobalConfig.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; + GlobalConfig.utilities.toasts.vpnChanged = root.vpnChanged; + GlobalConfig.utilities.toasts.nowPlaying = root.nowPlaying; } anchors.fill: parent diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 4c3ff8af1..75d70bffd 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -54,38 +54,38 @@ Item { property list excludedScreens: Config.bar.excludedScreens ?? [] function saveConfig(entryIndex, entryEnabled) { - Config.bar.activeWindow.compact = root.activeWindowCompact; - Config.bar.activeWindow.inverted = root.activeWindowInverted; - Config.bar.clock.background = root.clockBackground; - Config.bar.clock.showDate = root.clockShowDate; - Config.bar.clock.showIcon = root.clockShowIcon; - Config.bar.persistent = root.persistent; - Config.bar.showOnHover = root.showOnHover; - Config.bar.dragThreshold = root.dragThreshold; - Config.bar.status.showAudio = root.showAudio; - Config.bar.status.showMicrophone = root.showMicrophone; - Config.bar.status.showKbLayout = root.showKbLayout; - Config.bar.status.showNetwork = root.showNetwork; - Config.bar.status.showWifi = root.showWifi; - Config.bar.status.showBluetooth = root.showBluetooth; - Config.bar.status.showBattery = root.showBattery; - Config.bar.status.showLockStatus = root.showLockStatus; - Config.bar.tray.background = root.trayBackground; - Config.bar.tray.compact = root.trayCompact; - Config.bar.tray.recolour = root.trayRecolour; - Config.bar.workspaces.shown = root.workspacesShown; - Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; - Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; - Config.bar.workspaces.showWindows = root.workspacesShowWindows; - Config.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; - Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; - Config.bar.scrollActions.workspaces = root.scrollWorkspaces; - Config.bar.scrollActions.volume = root.scrollVolume; - Config.bar.scrollActions.brightness = root.scrollBrightness; - Config.bar.popouts.activeWindow = root.popoutActiveWindow; - Config.bar.popouts.tray = root.popoutTray; - Config.bar.popouts.statusIcons = root.popoutStatusIcons; - Config.bar.excludedScreens = root.excludedScreens; + GlobalConfig.bar.activeWindow.compact = root.activeWindowCompact; + GlobalConfig.bar.activeWindow.inverted = root.activeWindowInverted; + GlobalConfig.bar.clock.background = root.clockBackground; + GlobalConfig.bar.clock.showDate = root.clockShowDate; + GlobalConfig.bar.clock.showIcon = root.clockShowIcon; + GlobalConfig.bar.persistent = root.persistent; + GlobalConfig.bar.showOnHover = root.showOnHover; + GlobalConfig.bar.dragThreshold = root.dragThreshold; + GlobalConfig.bar.status.showAudio = root.showAudio; + GlobalConfig.bar.status.showMicrophone = root.showMicrophone; + GlobalConfig.bar.status.showKbLayout = root.showKbLayout; + GlobalConfig.bar.status.showNetwork = root.showNetwork; + GlobalConfig.bar.status.showWifi = root.showWifi; + GlobalConfig.bar.status.showBluetooth = root.showBluetooth; + GlobalConfig.bar.status.showBattery = root.showBattery; + GlobalConfig.bar.status.showLockStatus = root.showLockStatus; + GlobalConfig.bar.tray.background = root.trayBackground; + GlobalConfig.bar.tray.compact = root.trayCompact; + GlobalConfig.bar.tray.recolour = root.trayRecolour; + GlobalConfig.bar.workspaces.shown = root.workspacesShown; + GlobalConfig.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; + GlobalConfig.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; + GlobalConfig.bar.workspaces.showWindows = root.workspacesShowWindows; + GlobalConfig.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; + GlobalConfig.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; + GlobalConfig.bar.scrollActions.workspaces = root.scrollWorkspaces; + GlobalConfig.bar.scrollActions.volume = root.scrollVolume; + GlobalConfig.bar.scrollActions.brightness = root.scrollBrightness; + GlobalConfig.bar.popouts.activeWindow = root.popoutActiveWindow; + GlobalConfig.bar.popouts.tray = root.popoutTray; + GlobalConfig.bar.popouts.statusIcons = root.popoutStatusIcons; + GlobalConfig.bar.excludedScreens = root.excludedScreens; const entries = []; for (let i = 0; i < entriesModel.count; i++) { @@ -99,7 +99,7 @@ Item { enabled: enabled }); } - Config.bar.entries = entries; + GlobalConfig.bar.entries = entries; } anchors.fill: parent diff --git a/services/LyricsService.qml b/services/LyricsService.qml index dc5f600f9..04fc64fbf 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -68,7 +68,7 @@ Singleton { } function toggleVisibility() { - Config.services.showLyrics = !Config.services.showLyrics; + GlobalConfig.services.showLyrics = !Config.services.showLyrics; } function loadLyrics() { @@ -272,7 +272,7 @@ Singleton { onPreferredBackendChanged: { if (Config.services.lyricsBackend !== preferredBackend) { - Config.services.lyricsBackend = preferredBackend; + GlobalConfig.services.lyricsBackend = preferredBackend; } } From fc1b3ca2d62b71281458d7d6f6e518e2ce42a9d6 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:47:42 +1000 Subject: [PATCH 316/409] chore: remove unused import --- services/Screens.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/services/Screens.qml b/services/Screens.qml index f902a38d3..ac26d27d8 100644 --- a/services/Screens.qml +++ b/services/Screens.qml @@ -2,7 +2,6 @@ pragma Singleton import Quickshell import Caelestia.Config -import qs.utils Singleton { id: root From 644e70679b4e1112fe56311ed2fd45e552e1eb2e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:52:51 +1000 Subject: [PATCH 317/409] refactor: move attached types to own files --- plugin/src/Caelestia/Config/CMakeLists.txt | 2 + plugin/src/Caelestia/Config/config.cpp | 52 ------------ plugin/src/Caelestia/Config/config.hpp | 64 --------------- .../src/Caelestia/Config/configattached.cpp | 56 +++++++++++++ .../src/Caelestia/Config/configattached.hpp | 69 ++++++++++++++++ plugin/src/Caelestia/Config/tokens.cpp | 80 ------------------ plugin/src/Caelestia/Config/tokens.hpp | 43 ---------- .../src/Caelestia/Config/tokensattached.cpp | 82 +++++++++++++++++++ .../src/Caelestia/Config/tokensattached.hpp | 52 ++++++++++++ 9 files changed, 261 insertions(+), 239 deletions(-) create mode 100644 plugin/src/Caelestia/Config/configattached.cpp create mode 100644 plugin/src/Caelestia/Config/configattached.hpp create mode 100644 plugin/src/Caelestia/Config/tokensattached.cpp create mode 100644 plugin/src/Caelestia/Config/tokensattached.hpp diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index a77a1fe96..9322d76ca 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -2,11 +2,13 @@ qml_module(caelestia-config URI Caelestia.Config SOURCES config.cpp + configattached.cpp configobject.cpp rootconfig.cpp configscope.cpp appearanceconfig.cpp tokens.cpp + tokensattached.cpp anim.cpp monitorconfigmanager.cpp backgroundconfig.hpp diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index b68f42471..9cdcc9826 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -1,5 +1,4 @@ #include "config.hpp" -#include "configscope.hpp" #include "monitorconfigmanager.hpp" #include "tokens.hpp" @@ -101,55 +100,4 @@ GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { return instance(); } -// Config (attached type) - -Config::Config(ConfigScope* scope, QObject* parent) - : QObject(parent) - , m_scope(scope) { - connectScope(); -} - -void Config::connectScope() { - if (!m_scope) - return; - connect(m_scope, &ConfigScope::configChanged, this, &Config::sourceChanged); - connect(m_scope, &ConfigScope::tokensChanged, this, &Config::sourceChanged); -} - -// Helper: return per-monitor sub-object if scope exists, otherwise global -#define CONFIG_ATTACHED_GETTER(Type, name) \ - const Type* Config::name() const { \ - if (m_scope && m_scope->config()) \ - return m_scope->config()->name(); \ - return GlobalConfig::instance()->name(); \ - } - -CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) -CONFIG_ATTACHED_GETTER(GeneralConfig, general) -CONFIG_ATTACHED_GETTER(BackgroundConfig, background) -CONFIG_ATTACHED_GETTER(BarConfig, bar) -CONFIG_ATTACHED_GETTER(BorderConfig, border) -CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard) -CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter) -CONFIG_ATTACHED_GETTER(LauncherConfig, launcher) -CONFIG_ATTACHED_GETTER(NotifsConfig, notifs) -CONFIG_ATTACHED_GETTER(OsdConfig, osd) -CONFIG_ATTACHED_GETTER(SessionConfig, session) -CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) -CONFIG_ATTACHED_GETTER(LockConfig, lock) -CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) -CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) -CONFIG_ATTACHED_GETTER(ServiceConfig, services) -CONFIG_ATTACHED_GETTER(UserPaths, paths) - -#undef CONFIG_ATTACHED_GETTER - -GlobalConfig* Config::forScreen(const QString& screen) { - return GlobalConfig::forScreen(screen); -} - -Config* Config::qmlAttachedProperties(QObject* object) { - return new Config(ConfigScope::find(object), object); -} - } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 1c34f0399..3a66c0b00 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -65,68 +65,4 @@ class GlobalConfig : public RootConfig { bool m_tokensBound = false; }; -class ConfigScope; - -class Config : public QObject { - Q_OBJECT - Q_MOC_INCLUDE("configscope.hpp") - QML_ELEMENT - QML_UNCREATABLE("") - QML_ATTACHED(Config) - - Q_PROPERTY(caelestia::config::ConfigScope* scope READ scope NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::BarConfig* bar READ bar NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::BorderConfig* border READ border NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) - -public: - explicit Config(ConfigScope* scope, QObject* parent = nullptr); - - [[nodiscard]] ConfigScope* scope() const { return m_scope; } - - [[nodiscard]] const AppearanceConfig* appearance() const; - [[nodiscard]] const GeneralConfig* general() const; - [[nodiscard]] const BackgroundConfig* background() const; - [[nodiscard]] const BarConfig* bar() const; - [[nodiscard]] const BorderConfig* border() const; - [[nodiscard]] const DashboardConfig* dashboard() const; - [[nodiscard]] const ControlCenterConfig* controlCenter() const; - [[nodiscard]] const LauncherConfig* launcher() const; - [[nodiscard]] const NotifsConfig* notifs() const; - [[nodiscard]] const OsdConfig* osd() const; - [[nodiscard]] const SessionConfig* session() const; - [[nodiscard]] const WInfoConfig* winfo() const; - [[nodiscard]] const LockConfig* lock() const; - [[nodiscard]] const UtilitiesConfig* utilities() const; - [[nodiscard]] const SidebarConfig* sidebar() const; - [[nodiscard]] const ServiceConfig* services() const; - [[nodiscard]] const UserPaths* paths() const; - - [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); - - static Config* qmlAttachedProperties(QObject* object); - -signals: - void sourceChanged(); - -private: - void connectScope(); - - ConfigScope* m_scope; -}; - } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp new file mode 100644 index 000000000..02574910b --- /dev/null +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -0,0 +1,56 @@ +#include "configattached.hpp" +#include "config.hpp" +#include "configscope.hpp" +#include "monitorconfigmanager.hpp" + +namespace caelestia::config { + +Config::Config(ConfigScope* scope, QObject* parent) + : QObject(parent) + , m_scope(scope) { + connectScope(); +} + +void Config::connectScope() { + if (!m_scope) + return; + connect(m_scope, &ConfigScope::configChanged, this, &Config::sourceChanged); + connect(m_scope, &ConfigScope::tokensChanged, this, &Config::sourceChanged); +} + +#define CONFIG_ATTACHED_GETTER(Type, name) \ + const Type* Config::name() const { \ + if (m_scope && m_scope->config()) \ + return m_scope->config()->name(); \ + return GlobalConfig::instance()->name(); \ + } + +CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) +CONFIG_ATTACHED_GETTER(GeneralConfig, general) +CONFIG_ATTACHED_GETTER(BackgroundConfig, background) +CONFIG_ATTACHED_GETTER(BarConfig, bar) +CONFIG_ATTACHED_GETTER(BorderConfig, border) +CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard) +CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter) +CONFIG_ATTACHED_GETTER(LauncherConfig, launcher) +CONFIG_ATTACHED_GETTER(NotifsConfig, notifs) +CONFIG_ATTACHED_GETTER(OsdConfig, osd) +CONFIG_ATTACHED_GETTER(SessionConfig, session) +CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) +CONFIG_ATTACHED_GETTER(LockConfig, lock) +CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) +CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) +CONFIG_ATTACHED_GETTER(ServiceConfig, services) +CONFIG_ATTACHED_GETTER(UserPaths, paths) + +#undef CONFIG_ATTACHED_GETTER + +GlobalConfig* Config::forScreen(const QString& screen) { + return GlobalConfig::forScreen(screen); +} + +Config* Config::qmlAttachedProperties(QObject* object) { + return new Config(ConfigScope::find(object), object); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp new file mode 100644 index 000000000..6aeb7c190 --- /dev/null +++ b/plugin/src/Caelestia/Config/configattached.hpp @@ -0,0 +1,69 @@ +#pragma once + +#include "config.hpp" +#include "configscope.hpp" + +namespace caelestia::config { + +class Config : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + QML_ATTACHED(Config) + + Q_PROPERTY(caelestia::config::ConfigScope* scope READ scope NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BarConfig* bar READ bar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BorderConfig* border READ border NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) + +public: + explicit Config(ConfigScope* scope, QObject* parent = nullptr); + + [[nodiscard]] ConfigScope* scope() const { return m_scope; } + + [[nodiscard]] const AppearanceConfig* appearance() const; + [[nodiscard]] const GeneralConfig* general() const; + [[nodiscard]] const BackgroundConfig* background() const; + [[nodiscard]] const BarConfig* bar() const; + [[nodiscard]] const BorderConfig* border() const; + [[nodiscard]] const DashboardConfig* dashboard() const; + [[nodiscard]] const ControlCenterConfig* controlCenter() const; + [[nodiscard]] const LauncherConfig* launcher() const; + [[nodiscard]] const NotifsConfig* notifs() const; + [[nodiscard]] const OsdConfig* osd() const; + [[nodiscard]] const SessionConfig* session() const; + [[nodiscard]] const WInfoConfig* winfo() const; + [[nodiscard]] const LockConfig* lock() const; + [[nodiscard]] const UtilitiesConfig* utilities() const; + [[nodiscard]] const SidebarConfig* sidebar() const; + [[nodiscard]] const ServiceConfig* services() const; + [[nodiscard]] const UserPaths* paths() const; + + [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); + + static Config* qmlAttachedProperties(QObject* object); + +signals: + void sourceChanged(); + +private: + void connectScope(); + + ConfigScope* m_scope; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 84d3cef73..3d3f48c94 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -1,6 +1,5 @@ #include "tokens.hpp" #include "config.hpp" -#include "configscope.hpp" #include "monitorconfigmanager.hpp" #include @@ -53,83 +52,4 @@ TokenConfig* TokenConfig::create(QQmlEngine*, QJSEngine*) { return instance(); } -// Tokens (attached type) - -// Resolve appearance from per-monitor GlobalConfig overlay or global GlobalConfig -static const AppearanceConfig* resolveAppearance(ConfigScope* scope) { - if (scope && scope->config()) - return scope->config()->appearance(); - auto* global = GlobalConfig::instance(); - return global ? global->appearance() : nullptr; -} - -Tokens::Tokens(ConfigScope* scope, QObject* parent) - : QObject(parent) - , m_scope(scope) - , m_anim(new AnimTokens(this)) { - connectScope(); - bindAnim(); -} - -void Tokens::connectScope() { - if (!m_scope) - return; - connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged); - connect(m_scope, &ConfigScope::configChanged, this, &Tokens::bindAnim); -} - -void Tokens::bindAnim() { - auto* appearance = resolveAppearance(m_scope); - if (!appearance) - return; - - // Bind durations from resolved GlobalConfig appearance - m_anim->bindDurations(appearance->anim()->durations()); - - // Bind curves from TokenConfig - auto* tokens = TokenConfig::instance(); - if (tokens) - m_anim->bindCurves(tokens->appearance()->curves()); -} - -const AppearanceRounding* Tokens::rounding() const { - auto* a = resolveAppearance(m_scope); - return a ? a->rounding() : nullptr; -} - -const AppearanceSpacing* Tokens::spacing() const { - auto* a = resolveAppearance(m_scope); - return a ? a->spacing() : nullptr; -} - -const AppearancePadding* Tokens::padding() const { - auto* a = resolveAppearance(m_scope); - return a ? a->padding() : nullptr; -} - -const AppearanceFont* Tokens::font() const { - auto* a = resolveAppearance(m_scope); - return a ? a->font() : nullptr; -} - -const AppearanceTransparency* Tokens::transparency() const { - auto* a = resolveAppearance(m_scope); - return a ? a->transparency() : nullptr; -} - -const SizeTokens* Tokens::sizes() const { - if (m_scope && m_scope->tokens()) - return m_scope->tokens()->sizes(); - auto* global = TokenConfig::instance(); - return global ? global->sizes() : nullptr; -} - -TokenConfig* Tokens::forScreen(const QString& screen) { - return TokenConfig::forScreen(screen); -} - -Tokens* Tokens::qmlAttachedProperties(QObject* object) { - return new Tokens(ConfigScope::find(object), object); -} - } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 2448b737a..ca42ccbdc 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -342,47 +342,4 @@ class TokenConfig : public RootConfig { TokenConfig* m_defaults = nullptr; }; -class Tokens : public QObject { - Q_OBJECT - Q_MOC_INCLUDE("configscope.hpp") - QML_ELEMENT - QML_UNCREATABLE("") - QML_ATTACHED(Tokens) - - Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) - Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) - -public: - explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); - - [[nodiscard]] const AppearanceRounding* rounding() const; - [[nodiscard]] const AppearanceSpacing* spacing() const; - [[nodiscard]] const AppearancePadding* padding() const; - [[nodiscard]] const AppearanceFont* font() const; - [[nodiscard]] const AppearanceTransparency* transparency() const; - - [[nodiscard]] const SizeTokens* sizes() const; - - [[nodiscard]] const AnimTokens* anim() const { return m_anim; } - - [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); - - static Tokens* qmlAttachedProperties(QObject* object); - -signals: - void sourceChanged(); - -private: - void connectScope(); - void bindAnim(); - - ConfigScope* m_scope; - AnimTokens* m_anim = nullptr; -}; - } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp new file mode 100644 index 000000000..0f1a8bf30 --- /dev/null +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -0,0 +1,82 @@ +#include "tokensattached.hpp" +#include "anim.hpp" +#include "config.hpp" +#include "configscope.hpp" +#include "monitorconfigmanager.hpp" +#include "tokens.hpp" + +namespace caelestia::config { + +static const AppearanceConfig* resolveAppearance(ConfigScope* scope) { + if (scope && scope->config()) + return scope->config()->appearance(); + return GlobalConfig::instance()->appearance(); +} + +Tokens::Tokens(ConfigScope* scope, QObject* parent) + : QObject(parent) + , m_scope(scope) + , m_anim(new AnimTokens(this)) { + connectScope(); + bindAnim(); +} + +void Tokens::connectScope() { + if (!m_scope) + return; + connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged); + connect(m_scope, &ConfigScope::configChanged, this, &Tokens::bindAnim); +} + +void Tokens::bindAnim() { + auto* appearance = resolveAppearance(m_scope); + if (!appearance) + return; + + m_anim->bindDurations(appearance->anim()->durations()); + + auto* tokens = TokenConfig::instance(); + if (tokens) + m_anim->bindCurves(tokens->appearance()->curves()); +} + +const AppearanceRounding* Tokens::rounding() const { + auto* a = resolveAppearance(m_scope); + return a ? a->rounding() : nullptr; +} + +const AppearanceSpacing* Tokens::spacing() const { + auto* a = resolveAppearance(m_scope); + return a ? a->spacing() : nullptr; +} + +const AppearancePadding* Tokens::padding() const { + auto* a = resolveAppearance(m_scope); + return a ? a->padding() : nullptr; +} + +const AppearanceFont* Tokens::font() const { + auto* a = resolveAppearance(m_scope); + return a ? a->font() : nullptr; +} + +const AppearanceTransparency* Tokens::transparency() const { + auto* a = resolveAppearance(m_scope); + return a ? a->transparency() : nullptr; +} + +const SizeTokens* Tokens::sizes() const { + if (m_scope && m_scope->tokens()) + return m_scope->tokens()->sizes(); + return TokenConfig::instance()->sizes(); +} + +TokenConfig* Tokens::forScreen(const QString& screen) { + return TokenConfig::forScreen(screen); +} + +Tokens* Tokens::qmlAttachedProperties(QObject* object) { + return new Tokens(ConfigScope::find(object), object); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp new file mode 100644 index 000000000..df4eeece9 --- /dev/null +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -0,0 +1,52 @@ +#pragma once + +#include "anim.hpp" +#include "appearanceconfig.hpp" +#include "configscope.hpp" +#include "tokens.hpp" + +namespace caelestia::config { + +class Tokens : public QObject { + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + QML_ATTACHED(Tokens) + + Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) + +public: + explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); + + [[nodiscard]] const AppearanceRounding* rounding() const; + [[nodiscard]] const AppearanceSpacing* spacing() const; + [[nodiscard]] const AppearancePadding* padding() const; + [[nodiscard]] const AppearanceFont* font() const; + [[nodiscard]] const AppearanceTransparency* transparency() const; + + [[nodiscard]] const SizeTokens* sizes() const; + + [[nodiscard]] const AnimTokens* anim() const { return m_anim; } + + [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); + + static Tokens* qmlAttachedProperties(QObject* object); + +signals: + void sourceChanged(); + +private: + void connectScope(); + void bindAnim(); + + ConfigScope* m_scope; + AnimTokens* m_anim = nullptr; +}; + +} // namespace caelestia::config From dd15b1d80ea93ad526649f7c29046006fcd02711 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:54:01 +1000 Subject: [PATCH 318/409] refactor: static -> anonymous namespace --- plugin/src/Caelestia/Config/tokensattached.cpp | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 0f1a8bf30..0db819919 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -7,12 +7,16 @@ namespace caelestia::config { -static const AppearanceConfig* resolveAppearance(ConfigScope* scope) { +namespace { + +const AppearanceConfig* resolveAppearance(ConfigScope* scope) { if (scope && scope->config()) return scope->config()->appearance(); return GlobalConfig::instance()->appearance(); } +} // namespace + Tokens::Tokens(ConfigScope* scope, QObject* parent) : QObject(parent) , m_scope(scope) From f1fb68c7210783b0200ecbf2fcb4588219646587 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 03:58:35 +1000 Subject: [PATCH 319/409] feat: add auto save on option changes --- plugin/src/Caelestia/Config/rootconfig.cpp | 24 ++++++++++++++++++++++ plugin/src/Caelestia/Config/rootconfig.hpp | 3 +++ 2 files changed, 27 insertions(+) diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index 611abd575..d82d910bc 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -96,6 +96,9 @@ void RootConfig::setupFileBackend(const QString& path, const QString& screen) { m_reloadDebounce->setInterval(50); connect(m_reloadDebounce, &QTimer::timeout, this, &RootConfig::reload); + // Auto-save when any property changes (debounced by the save timer) + connectAutoSave(this); + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RootConfig::onWatcherEvent); connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onWatcherEvent); @@ -111,6 +114,23 @@ void RootConfig::setupFileBackend(const QString& path, const QString& screen) { }); } +void RootConfig::connectAutoSave(ConfigObject* obj) { + connect(obj, &ConfigObject::propertiesChanged, this, [this] { + if (!m_loading) + saveToFile(); + }); + + // Recurse into sub-objects + const auto* meta = obj->metaObject(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + auto value = prop.read(obj); + auto* subObj = value.value(); + if (subObj) + connectAutoSave(subObj); + } +} + void RootConfig::updateWatch() { auto targetDir = QFileInfo(m_filePath).absolutePath(); @@ -192,11 +212,15 @@ std::optional RootConfig::reloadFromFile() { qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; + m_loading = true; + clearLoadedKeys(); auto jsonObj = doc.object(); loadFromJson(jsonObj); + m_loading = false; + // Collect unknown keys — caller is responsible for emitting signals m_lastUnknownKeys = collectUnknownKeys(this, jsonObj); diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index fb6a68b52..01ce2e54e 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -39,10 +39,13 @@ class RootConfig : public ConfigObject { void updateWatch(); void onWatcherEvent(); + void connectAutoSave(ConfigObject* obj); + QString m_filePath; QString m_screen; QString m_watchedDir; bool m_recentlySaved = false; + bool m_loading = false; QFileSystemWatcher* m_watcher = nullptr; QTimer* m_saveTimer = nullptr; From 7b13772f26f31e20e094734fdbbe1229215fad4e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 21:07:00 +1000 Subject: [PATCH 320/409] chore: add not per-mon opts to readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/README.md b/README.md index d7e4c7c65..bd3303953 100644 --- a/README.md +++ b/README.md @@ -226,6 +226,18 @@ For example, to disable the bar on DP-1: } ``` +> [!NOTE] +> Not all options are respect per-monitor overrides. Most notably, the following options will only read +> from the global config, and ignore the respective option in per-monitor config files. +> +> - All animation configs are not per-monitor +> - `general` (logo, terminal app, idle settings) +> - `services` (weather, audio, GPU, player settings, etc.) +> - `paths` (wallpaper dir, lyrics dir, etc.) +> - `notifs` (notification behavior) +> - `utilities` (individual toast enabled status, vpn configs) +> - `launcher` (actions, favourite apps, hidden apps) + ### Example configuration > [!NOTE] From 1385ca15debbb4ae964cfa2d42fb70bddaea6305 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:16:32 +1000 Subject: [PATCH 321/409] feat: add warning on global options using per-mon confs --- plugin/src/Caelestia/Config/configobject.cpp | 8 +++++ plugin/src/Caelestia/Config/configobject.hpp | 32 +++++++++++++++++++ plugin/src/Caelestia/Config/generalconfig.hpp | 20 ++++++------ .../src/Caelestia/Config/launcherconfig.hpp | 24 +++++++------- plugin/src/Caelestia/Config/notifsconfig.hpp | 8 ++--- plugin/src/Caelestia/Config/serviceconfig.hpp | 28 ++++++++-------- plugin/src/Caelestia/Config/userpaths.hpp | 4 +-- .../src/Caelestia/Config/utilitiesconfig.hpp | 28 ++++++++-------- 8 files changed, 96 insertions(+), 56 deletions(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index cc38d7e5f..d4a1a77f3 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -29,6 +29,10 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) { if (!obj.contains(key)) continue; + if (m_global && m_globalOnlyKeys.contains(key)) + qCWarning( + lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", qUtf8Printable(key)); + const auto jsonVal = obj.value(key); // Recurse into sub-objects @@ -227,6 +231,10 @@ void ConfigObject::onGlobalPropertiesChanged(const QMap& chan } } +void ConfigObject::markGlobalOnly(const QString& name) { + m_globalOnlyKeys.insert(name); +} + void ConfigObject::notifyPropertyChanged(const QString& name, const QVariant& value) { m_pendingChanges.insert(name, value); diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 4e0cae926..8e3969a45 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -41,6 +41,34 @@ public: private: \ Type* m_##name = nullptr; +// Like CONFIG_PROPERTY but warns on read/write when accessed on a per-monitor overlay. +#define CONFIG_GLOBAL_PROPERTY(Type, name, ...) \ + Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ + \ +public: \ + [[nodiscard]] Type name() const { \ + if (isOverlay()) \ + qCWarning(caelestia::config::lcConfig, "Reading global-only option '%s' on per-monitor overlay", #name); \ + return m_##name; \ + } \ + void set_##name(const Type& val) { \ + if (isOverlay()) \ + qCWarning(caelestia::config::lcConfig, "Writing global-only option '%s' on per-monitor overlay", #name); \ + if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ + markPropertyLoaded(QStringLiteral(#name)); \ + Q_EMIT name##Changed(); \ + notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ + } \ + } \ + Q_SIGNAL void name##Changed(); \ + \ +private: \ + Type m_##name __VA_OPT__(= __VA_ARGS__); \ + const bool m_##name##_go = [this] { \ + markGlobalOnly(QStringLiteral(#name)); \ + return true; \ + }(); + namespace caelestia::config { Q_DECLARE_LOGGING_CATEGORY(lcConfig) @@ -61,6 +89,8 @@ class ConfigObject : public QObject { [[nodiscard]] bool isPropertyLoaded(const QString& name) const { return m_loadedKeys.contains(name); } + [[nodiscard]] bool isOverlay() const { return m_global != nullptr; } + Q_INVOKABLE void resetOption(const QString& name); template static bool updateMember(T& member, const T& value) { @@ -80,6 +110,7 @@ class ConfigObject : public QObject { protected: void markPropertyLoaded(const QString& name); + void markGlobalOnly(const QString& name); void notifyPropertyChanged(const QString& name, const QVariant& value); private: @@ -89,6 +120,7 @@ class ConfigObject : public QObject { // Per-monitor overlay state ConfigObject* m_global = nullptr; QSet m_loadedKeys; + QSet m_globalOnlyKeys; QMap m_pendingChanges; QTimer* m_batchTimer = nullptr; }; diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp index 32b7df1ec..f84cfa34d 100644 --- a/plugin/src/Caelestia/Config/generalconfig.hpp +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -12,10 +12,10 @@ class GeneralApps : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QStringList, terminal, { QStringLiteral("foot") }) - CONFIG_PROPERTY(QStringList, audio, { QStringLiteral("pavucontrol") }) - CONFIG_PROPERTY(QStringList, playback, { QStringLiteral("mpv") }) - CONFIG_PROPERTY(QStringList, explorer, { QStringLiteral("thunar") }) + CONFIG_GLOBAL_PROPERTY(QStringList, terminal, { QStringLiteral("foot") }) + CONFIG_GLOBAL_PROPERTY(QStringList, audio, { QStringLiteral("pavucontrol") }) + CONFIG_GLOBAL_PROPERTY(QStringList, playback, { QStringLiteral("mpv") }) + CONFIG_GLOBAL_PROPERTY(QStringList, explorer, { QStringLiteral("thunar") }) public: explicit GeneralApps(QObject* parent = nullptr) @@ -26,9 +26,9 @@ class GeneralIdle : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, lockBeforeSleep, true) - CONFIG_PROPERTY(bool, inhibitWhenAudio, true) - CONFIG_PROPERTY(QVariantList, timeouts) + CONFIG_GLOBAL_PROPERTY(bool, lockBeforeSleep, true) + CONFIG_GLOBAL_PROPERTY(bool, inhibitWhenAudio, true) + CONFIG_GLOBAL_PROPERTY(QVariantList, timeouts) public: explicit GeneralIdle(QObject* parent = nullptr) @@ -39,8 +39,8 @@ class GeneralBattery : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QVariantList, warnLevels) - CONFIG_PROPERTY(int, criticalLevel, 3) + CONFIG_GLOBAL_PROPERTY(QVariantList, warnLevels) + CONFIG_GLOBAL_PROPERTY(int, criticalLevel, 3) public: explicit GeneralBattery(QObject* parent = nullptr) @@ -51,7 +51,7 @@ class GeneralConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QString, logo) + CONFIG_GLOBAL_PROPERTY(QString, logo) CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) CONFIG_SUBOBJECT(GeneralApps, apps) diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp index 23f71734d..b0c8d06bb 100644 --- a/plugin/src/Caelestia/Config/launcherconfig.hpp +++ b/plugin/src/Caelestia/Config/launcherconfig.hpp @@ -12,11 +12,11 @@ class LauncherUseFuzzy : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, apps, false) - CONFIG_PROPERTY(bool, actions, false) - CONFIG_PROPERTY(bool, schemes, false) - CONFIG_PROPERTY(bool, variants, false) - CONFIG_PROPERTY(bool, wallpapers, false) + CONFIG_GLOBAL_PROPERTY(bool, apps, false) + CONFIG_GLOBAL_PROPERTY(bool, actions, false) + CONFIG_GLOBAL_PROPERTY(bool, schemes, false) + CONFIG_GLOBAL_PROPERTY(bool, variants, false) + CONFIG_GLOBAL_PROPERTY(bool, wallpapers, false) public: explicit LauncherUseFuzzy(QObject* parent = nullptr) @@ -31,15 +31,15 @@ class LauncherConfig : public ConfigObject { CONFIG_PROPERTY(bool, showOnHover, false) CONFIG_PROPERTY(int, maxShown, 7) CONFIG_PROPERTY(int, maxWallpapers, 9) - CONFIG_PROPERTY(QString, specialPrefix, QStringLiteral("@")) - CONFIG_PROPERTY(QString, actionPrefix, QStringLiteral(">")) - CONFIG_PROPERTY(bool, enableDangerousActions, false) + CONFIG_GLOBAL_PROPERTY(QString, specialPrefix, QStringLiteral("@")) + CONFIG_GLOBAL_PROPERTY(QString, actionPrefix, QStringLiteral(">")) + CONFIG_GLOBAL_PROPERTY(bool, enableDangerousActions, false) CONFIG_PROPERTY(int, dragThreshold, 50) - CONFIG_PROPERTY(bool, vimKeybinds, false) - CONFIG_PROPERTY(QStringList, favouriteApps) - CONFIG_PROPERTY(QStringList, hiddenApps) + CONFIG_GLOBAL_PROPERTY(bool, vimKeybinds, false) + CONFIG_GLOBAL_PROPERTY(QStringList, favouriteApps) + CONFIG_GLOBAL_PROPERTY(QStringList, hiddenApps) CONFIG_SUBOBJECT(LauncherUseFuzzy, useFuzzy) - CONFIG_PROPERTY(QVariantList, actions) + CONFIG_GLOBAL_PROPERTY(QVariantList, actions) public: explicit LauncherConfig(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/notifsconfig.hpp b/plugin/src/Caelestia/Config/notifsconfig.hpp index 745b057b0..eef360fe4 100644 --- a/plugin/src/Caelestia/Config/notifsconfig.hpp +++ b/plugin/src/Caelestia/Config/notifsconfig.hpp @@ -10,12 +10,12 @@ class NotifsConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, expire, true) - CONFIG_PROPERTY(QString, fullscreen, QStringLiteral("on")) - CONFIG_PROPERTY(int, defaultExpireTimeout, 5000) + CONFIG_GLOBAL_PROPERTY(bool, expire, true) + CONFIG_GLOBAL_PROPERTY(QString, fullscreen, QStringLiteral("on")) + CONFIG_GLOBAL_PROPERTY(int, defaultExpireTimeout, 5000) CONFIG_PROPERTY(qreal, clearThreshold, 0.3) CONFIG_PROPERTY(int, expandThreshold, 20) - CONFIG_PROPERTY(bool, actionOnClick, false) + CONFIG_GLOBAL_PROPERTY(bool, actionOnClick, false) CONFIG_PROPERTY(int, groupPreviewNum, 3) CONFIG_PROPERTY(bool, openExpanded, false) diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp index 062bf4513..439092af6 100644 --- a/plugin/src/Caelestia/Config/serviceconfig.hpp +++ b/plugin/src/Caelestia/Config/serviceconfig.hpp @@ -11,20 +11,20 @@ class ServiceConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QString, weatherLocation) - CONFIG_PROPERTY(bool, useFahrenheit, false) - CONFIG_PROPERTY(bool, useFahrenheitPerformance, false) - CONFIG_PROPERTY(bool, useTwelveHourClock, false) - CONFIG_PROPERTY(QString, gpuType) - CONFIG_PROPERTY(int, visualiserBars, 45) - CONFIG_PROPERTY(qreal, audioIncrement, 0.1) - CONFIG_PROPERTY(qreal, brightnessIncrement, 0.1) - CONFIG_PROPERTY(qreal, maxVolume, 1.0) - CONFIG_PROPERTY(bool, smartScheme, true) - CONFIG_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify")) - CONFIG_PROPERTY(QVariantList, playerAliases) - CONFIG_PROPERTY(bool, showLyrics, false) - CONFIG_PROPERTY(QString, lyricsBackend, QStringLiteral("Auto")) + CONFIG_GLOBAL_PROPERTY(QString, weatherLocation) + CONFIG_GLOBAL_PROPERTY(bool, useFahrenheit, false) + CONFIG_GLOBAL_PROPERTY(bool, useFahrenheitPerformance, false) + CONFIG_GLOBAL_PROPERTY(bool, useTwelveHourClock, false) + CONFIG_GLOBAL_PROPERTY(QString, gpuType) + CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 45) + CONFIG_GLOBAL_PROPERTY(qreal, audioIncrement, 0.1) + CONFIG_GLOBAL_PROPERTY(qreal, brightnessIncrement, 0.1) + CONFIG_GLOBAL_PROPERTY(qreal, maxVolume, 1.0) + CONFIG_GLOBAL_PROPERTY(bool, smartScheme, true) + CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify")) + CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases) + CONFIG_GLOBAL_PROPERTY(bool, showLyrics, false) + CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, QStringLiteral("Auto")) public: explicit ServiceConfig(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/userpaths.hpp b/plugin/src/Caelestia/Config/userpaths.hpp index 188121881..f0d0fb2e8 100644 --- a/plugin/src/Caelestia/Config/userpaths.hpp +++ b/plugin/src/Caelestia/Config/userpaths.hpp @@ -12,9 +12,9 @@ class UserPaths : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QString, wallpaperDir, + CONFIG_GLOBAL_PROPERTY(QString, wallpaperDir, QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QStringLiteral("/Wallpapers")) - CONFIG_PROPERTY(QString, lyricsDir, QDir::homePath() + QStringLiteral("/Music/lyrics/")) + CONFIG_GLOBAL_PROPERTY(QString, lyricsDir, QDir::homePath() + QStringLiteral("/Music/lyrics/")) CONFIG_PROPERTY(QString, sessionGif, QStringLiteral("root:/assets/kurukuru.gif")) CONFIG_PROPERTY(QString, mediaGif, QStringLiteral("root:/assets/bongocat.gif")) CONFIG_PROPERTY(QString, noNotifsPic, QStringLiteral("root:/assets/dino.png")) diff --git a/plugin/src/Caelestia/Config/utilitiesconfig.hpp b/plugin/src/Caelestia/Config/utilitiesconfig.hpp index 57b6fccd0..cabb877e4 100644 --- a/plugin/src/Caelestia/Config/utilitiesconfig.hpp +++ b/plugin/src/Caelestia/Config/utilitiesconfig.hpp @@ -11,19 +11,19 @@ class UtilitiesToasts : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, configLoaded, true) CONFIG_PROPERTY(QString, fullscreen, QStringLiteral("off")) - CONFIG_PROPERTY(bool, chargingChanged, true) - CONFIG_PROPERTY(bool, gameModeChanged, true) - CONFIG_PROPERTY(bool, dndChanged, true) - CONFIG_PROPERTY(bool, audioOutputChanged, true) - CONFIG_PROPERTY(bool, audioInputChanged, true) - CONFIG_PROPERTY(bool, capsLockChanged, true) - CONFIG_PROPERTY(bool, numLockChanged, true) - CONFIG_PROPERTY(bool, kbLayoutChanged, true) - CONFIG_PROPERTY(bool, kbLimit, true) - CONFIG_PROPERTY(bool, vpnChanged, true) - CONFIG_PROPERTY(bool, nowPlaying, false) + CONFIG_GLOBAL_PROPERTY(bool, configLoaded, true) + CONFIG_GLOBAL_PROPERTY(bool, chargingChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, gameModeChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, dndChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, audioOutputChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, audioInputChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, capsLockChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, numLockChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, kbLayoutChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, kbLimit, true) + CONFIG_GLOBAL_PROPERTY(bool, vpnChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, nowPlaying, false) public: explicit UtilitiesToasts(QObject* parent = nullptr) @@ -34,8 +34,8 @@ class UtilitiesVpn : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, enabled, false) - CONFIG_PROPERTY(QVariantList, provider) + CONFIG_GLOBAL_PROPERTY(bool, enabled, false) + CONFIG_GLOBAL_PROPERTY(QVariantList, provider) public: explicit UtilitiesVpn(QObject* parent = nullptr) From e16a70bee783d492ac36cbc9c6892f3cffab69be Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:26:30 +1000 Subject: [PATCH 322/409] fix: don't sync global props Prevents warning on syncs as well --- plugin/src/Caelestia/Config/configobject.cpp | 8 ++++---- plugin/src/Caelestia/Config/configobject.hpp | 2 ++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index d4a1a77f3..6a700cf4e 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -29,7 +29,7 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) { if (!obj.contains(key)) continue; - if (m_global && m_globalOnlyKeys.contains(key)) + if (m_global && isGlobalOnly(key)) qCWarning( lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", qUtf8Printable(key)); @@ -157,7 +157,7 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) { continue; } - if (!prop.isWritable()) + if (!prop.isWritable() || isGlobalOnly(key)) continue; if (!m_loadedKeys.contains(key)) { @@ -188,7 +188,7 @@ void ConfigObject::resyncFromGlobal() { continue; } - if (!prop.isWritable()) + if (!prop.isWritable() || isGlobalOnly(key)) continue; if (!m_loadedKeys.contains(key)) { @@ -218,7 +218,7 @@ void ConfigObject::resetOption(const QString& name) { void ConfigObject::onGlobalPropertiesChanged(const QMap& changed) { for (auto it = changed.begin(); it != changed.end(); ++it) { - if (m_loadedKeys.contains(it.key())) + if (m_loadedKeys.contains(it.key()) || isGlobalOnly(it.key())) continue; int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 8e3969a45..f3e10508c 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -91,6 +91,8 @@ class ConfigObject : public QObject { [[nodiscard]] bool isOverlay() const { return m_global != nullptr; } + [[nodiscard]] bool isGlobalOnly(const QString& name) const { return m_globalOnlyKeys.contains(name); } + Q_INVOKABLE void resetOption(const QString& name); template static bool updateMember(T& member, const T& value) { From 129b0633811dbe5e98ad5b48da50f179d4c90592 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:38:36 +1000 Subject: [PATCH 323/409] refactor: move inline getter impls to cpp file --- plugin/src/Caelestia/Config/anim.cpp | 40 +++++++++++++++++++ plugin/src/Caelestia/Config/anim.hpp | 29 +++++--------- .../src/Caelestia/Config/configattached.cpp | 4 ++ .../src/Caelestia/Config/configattached.hpp | 2 +- plugin/src/Caelestia/Config/configobject.cpp | 12 ++++++ plugin/src/Caelestia/Config/configobject.hpp | 8 ++-- plugin/src/Caelestia/Config/configscope.cpp | 12 ++++++ plugin/src/Caelestia/Config/configscope.hpp | 8 ++-- plugin/src/Caelestia/Config/rootconfig.cpp | 4 ++ plugin/src/Caelestia/Config/rootconfig.hpp | 2 +- .../src/Caelestia/Config/tokensattached.cpp | 4 ++ .../src/Caelestia/Config/tokensattached.hpp | 3 +- 12 files changed, 95 insertions(+), 33 deletions(-) diff --git a/plugin/src/Caelestia/Config/anim.cpp b/plugin/src/Caelestia/Config/anim.cpp index 9c2b91118..824523c2b 100644 --- a/plugin/src/Caelestia/Config/anim.cpp +++ b/plugin/src/Caelestia/Config/anim.cpp @@ -9,6 +9,46 @@ namespace caelestia::config { AnimTokens::AnimTokens(QObject* parent) : QObject(parent) {} +QEasingCurve AnimTokens::emphasized() const { + return m_emphasized; +} + +QEasingCurve AnimTokens::emphasizedAccel() const { + return m_emphasizedAccel; +} + +QEasingCurve AnimTokens::emphasizedDecel() const { + return m_emphasizedDecel; +} + +QEasingCurve AnimTokens::standard() const { + return m_standard; +} + +QEasingCurve AnimTokens::standardAccel() const { + return m_standardAccel; +} + +QEasingCurve AnimTokens::standardDecel() const { + return m_standardDecel; +} + +QEasingCurve AnimTokens::expressiveFastSpatial() const { + return m_expressiveFastSpatial; +} + +QEasingCurve AnimTokens::expressiveDefaultSpatial() const { + return m_expressiveDefaultSpatial; +} + +QEasingCurve AnimTokens::expressiveSlowSpatial() const { + return m_expressiveSlowSpatial; +} + +AnimDurations* AnimTokens::durations() const { + return m_durations; +} + QEasingCurve AnimTokens::buildCurve(const QList& points) { QEasingCurve curve(QEasingCurve::BezierSpline); diff --git a/plugin/src/Caelestia/Config/anim.hpp b/plugin/src/Caelestia/Config/anim.hpp index 464ee32a0..1d5909a78 100644 --- a/plugin/src/Caelestia/Config/anim.hpp +++ b/plugin/src/Caelestia/Config/anim.hpp @@ -33,25 +33,16 @@ class AnimTokens : public QObject { void bindCurves(AnimCurves* curves); void bindDurations(AnimDurations* durations); - [[nodiscard]] QEasingCurve emphasized() const { return m_emphasized; } - - [[nodiscard]] QEasingCurve emphasizedAccel() const { return m_emphasizedAccel; } - - [[nodiscard]] QEasingCurve emphasizedDecel() const { return m_emphasizedDecel; } - - [[nodiscard]] QEasingCurve standard() const { return m_standard; } - - [[nodiscard]] QEasingCurve standardAccel() const { return m_standardAccel; } - - [[nodiscard]] QEasingCurve standardDecel() const { return m_standardDecel; } - - [[nodiscard]] QEasingCurve expressiveFastSpatial() const { return m_expressiveFastSpatial; } - - [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const { return m_expressiveDefaultSpatial; } - - [[nodiscard]] QEasingCurve expressiveSlowSpatial() const { return m_expressiveSlowSpatial; } - - [[nodiscard]] AnimDurations* durations() const { return m_durations; } + [[nodiscard]] QEasingCurve emphasized() const; + [[nodiscard]] QEasingCurve emphasizedAccel() const; + [[nodiscard]] QEasingCurve emphasizedDecel() const; + [[nodiscard]] QEasingCurve standard() const; + [[nodiscard]] QEasingCurve standardAccel() const; + [[nodiscard]] QEasingCurve standardDecel() const; + [[nodiscard]] QEasingCurve expressiveFastSpatial() const; + [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const; + [[nodiscard]] QEasingCurve expressiveSlowSpatial() const; + [[nodiscard]] AnimDurations* durations() const; signals: void curvesChanged(); diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp index 02574910b..55694c324 100644 --- a/plugin/src/Caelestia/Config/configattached.cpp +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -11,6 +11,10 @@ Config::Config(ConfigScope* scope, QObject* parent) connectScope(); } +ConfigScope* Config::scope() const { + return m_scope; +} + void Config::connectScope() { if (!m_scope) return; diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp index 6aeb7c190..d63c330fe 100644 --- a/plugin/src/Caelestia/Config/configattached.hpp +++ b/plugin/src/Caelestia/Config/configattached.hpp @@ -33,7 +33,7 @@ class Config : public QObject { public: explicit Config(ConfigScope* scope, QObject* parent = nullptr); - [[nodiscard]] ConfigScope* scope() const { return m_scope; } + [[nodiscard]] ConfigScope* scope() const; [[nodiscard]] const AppearanceConfig* appearance() const; [[nodiscard]] const GeneralConfig* general() const; diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 6a700cf4e..b209b30a3 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -198,6 +198,18 @@ void ConfigObject::resyncFromGlobal() { } } +bool ConfigObject::isPropertyLoaded(const QString& name) const { + return m_loadedKeys.contains(name); +} + +bool ConfigObject::isOverlay() const { + return m_global != nullptr; +} + +bool ConfigObject::isGlobalOnly(const QString& name) const { + return m_globalOnlyKeys.contains(name); +} + void ConfigObject::markPropertyLoaded(const QString& name) { m_loadedKeys.insert(name); } diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index f3e10508c..6666b7043 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -87,11 +87,9 @@ class ConfigObject : public QObject { void resyncFromGlobal(); void clearLoadedKeys(); - [[nodiscard]] bool isPropertyLoaded(const QString& name) const { return m_loadedKeys.contains(name); } - - [[nodiscard]] bool isOverlay() const { return m_global != nullptr; } - - [[nodiscard]] bool isGlobalOnly(const QString& name) const { return m_globalOnlyKeys.contains(name); } + [[nodiscard]] bool isPropertyLoaded(const QString& name) const; + [[nodiscard]] bool isOverlay() const; + [[nodiscard]] bool isGlobalOnly(const QString& name) const; Q_INVOKABLE void resetOption(const QString& name); diff --git a/plugin/src/Caelestia/Config/configscope.cpp b/plugin/src/Caelestia/Config/configscope.cpp index 6f20f05d0..769f2b73f 100644 --- a/plugin/src/Caelestia/Config/configscope.cpp +++ b/plugin/src/Caelestia/Config/configscope.cpp @@ -6,6 +6,18 @@ namespace caelestia::config { ConfigScope::ConfigScope(QQuickItem* parent) : QQuickItem(parent) {} +QString ConfigScope::screen() const { + return m_screen; +} + +GlobalConfig* ConfigScope::config() const { + return m_config; +} + +TokenConfig* ConfigScope::tokens() const { + return m_tokens; +} + void ConfigScope::setScreen(const QString& screen) { if (m_screen == screen) return; diff --git a/plugin/src/Caelestia/Config/configscope.hpp b/plugin/src/Caelestia/Config/configscope.hpp index 202cf48a5..828698167 100644 --- a/plugin/src/Caelestia/Config/configscope.hpp +++ b/plugin/src/Caelestia/Config/configscope.hpp @@ -21,13 +21,11 @@ class ConfigScope : public QQuickItem { public: explicit ConfigScope(QQuickItem* parent = nullptr); - [[nodiscard]] QString screen() const { return m_screen; } - + [[nodiscard]] QString screen() const; void setScreen(const QString& screen); - [[nodiscard]] GlobalConfig* config() const { return m_config; } - - [[nodiscard]] TokenConfig* tokens() const { return m_tokens; } + [[nodiscard]] GlobalConfig* config() const; + [[nodiscard]] TokenConfig* tokens() const; static ConfigScope* find(QObject* object); diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp index d82d910bc..9e976ab2a 100644 --- a/plugin/src/Caelestia/Config/rootconfig.cpp +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -20,6 +20,10 @@ QString watchRoot() { RootConfig::RootConfig(QObject* parent) : ConfigObject(parent) {} +bool RootConfig::recentlySaved() const { + return m_recentlySaved; +} + QStringList RootConfig::collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json) { QStringList unknown; const auto* meta = obj->metaObject(); diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp index 01ce2e54e..bf9b8e2c3 100644 --- a/plugin/src/Caelestia/Config/rootconfig.hpp +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -21,7 +21,7 @@ class RootConfig : public ConfigObject { // Returns nullopt if retrying, empty string on success, error message on failure. [[nodiscard]] std::optional reloadFromFile(); - [[nodiscard]] bool recentlySaved() const { return m_recentlySaved; } + [[nodiscard]] bool recentlySaved() const; Q_INVOKABLE void save(); Q_INVOKABLE void reload(); diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 0db819919..23ee28414 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -75,6 +75,10 @@ const SizeTokens* Tokens::sizes() const { return TokenConfig::instance()->sizes(); } +const AnimTokens* Tokens::anim() const { + return m_anim; +} + TokenConfig* Tokens::forScreen(const QString& screen) { return TokenConfig::forScreen(screen); } diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp index df4eeece9..88cf01026 100644 --- a/plugin/src/Caelestia/Config/tokensattached.hpp +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -31,8 +31,7 @@ class Tokens : public QObject { [[nodiscard]] const AppearanceTransparency* transparency() const; [[nodiscard]] const SizeTokens* sizes() const; - - [[nodiscard]] const AnimTokens* anim() const { return m_anim; } + [[nodiscard]] const AnimTokens* anim() const; [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); From b8d17ad8efb797cae68cb7ad5679afb24efaf031 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:47:30 +1000 Subject: [PATCH 324/409] fix: use global config for global properties --- modules/background/DesktopClock.qml | 2 +- modules/bar/components/Clock.qml | 2 +- modules/controlcenter/launcher/Settings.qml | 4 ++-- modules/dashboard/Performance.qml | 2 +- modules/dashboard/WeatherTab.qml | 2 +- modules/dashboard/dash/DateTime.qml | 2 +- modules/launcher/AppList.qml | 2 +- modules/launcher/Content.qml | 6 +++--- modules/launcher/ContentList.qml | 2 +- modules/launcher/items/AppItem.qml | 2 +- modules/launcher/items/CalcItem.qml | 2 +- modules/launcher/services/Actions.qml | 4 ++-- modules/launcher/services/Apps.qml | 2 +- modules/launcher/services/M3Variants.qml | 2 +- modules/launcher/services/Schemes.qml | 2 +- modules/lock/Center.qml | 2 +- modules/lock/WeatherInfo.qml | 2 +- modules/osd/Content.qml | 4 ++-- modules/utilities/cards/IdleInhibit.qml | 2 +- services/Weather.qml | 8 ++++---- 20 files changed, 28 insertions(+), 28 deletions(-) diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index b7d22417d..be6dff610 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -106,7 +106,7 @@ Item { Layout.alignment: Qt.AlignTop Layout.topMargin: Tokens.padding.large * 1.4 * root.clockScale - active: Config.services.useTwelveHourClock + active: GlobalConfig.services.useTwelveHourClock visible: active sourceComponent: StyledText { diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index d6528c641..ffe599afd 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -62,7 +62,7 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter horizontalAlignment: StyledText.AlignHCenter - text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") + text: Time.format(GlobalConfig.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") font.pointSize: Tokens.font.size.smaller font.family: Tokens.font.family.mono color: root.colour diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index df25a827f..657c4a286 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -100,13 +100,13 @@ ColumnLayout { PropertyRow { label: qsTr("Special prefix") - value: Config.launcher.specialPrefix || qsTr("None") + value: GlobalConfig.launcher.specialPrefix || qsTr("None") } PropertyRow { showTopMargin: true label: qsTr("Action prefix") - value: Config.launcher.actionPrefix || qsTr("None") + value: GlobalConfig.launcher.actionPrefix || qsTr("None") } } diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 0c405fd3b..cfed92967 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -14,7 +14,7 @@ Item { readonly property int minWidth: 400 + 400 + Tokens.spacing.normal + 120 + Tokens.padding.large * 2 function displayTemp(temp: real): string { - return `${Math.ceil(Config.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheitPerformance ? "F" : "C"}`; + return `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}`; } implicitWidth: Math.max(minWidth, content.implicitWidth) diff --git a/modules/dashboard/WeatherTab.qml b/modules/dashboard/WeatherTab.qml index 312372b28..3cf8d3b71 100644 --- a/modules/dashboard/WeatherTab.qml +++ b/modules/dashboard/WeatherTab.qml @@ -193,7 +193,7 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter - text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" + text: GlobalConfig.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" font.weight: 600 color: Colours.palette.m3tertiary } diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index 248d41f3e..82bee151c 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -51,7 +51,7 @@ Item { asynchronous: true Layout.alignment: Qt.AlignHCenter - active: Config.services.useTwelveHourClock + active: GlobalConfig.services.useTwelveHourClock visible: active sourceComponent: StyledText { diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 52b9b1b59..ac5bea65b 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -50,7 +50,7 @@ StyledListView { state: { const text = search.text; - const prefix = Config.launcher.actionPrefix; + const prefix = GlobalConfig.launcher.actionPrefix; if (text.startsWith(prefix)) { for (const action of ["calc", "scheme", "variant"]) if (text.startsWith(`${prefix}${action} `)) diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 2ca417391..7e283c8f5 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -78,7 +78,7 @@ Item { topPadding: Tokens.padding.larger bottomPadding: Tokens.padding.larger - placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) + placeholderText: qsTr("Type \"%1\" for commands").arg(GlobalConfig.launcher.actionPrefix) onAccepted: { const currentItem = list.currentList?.currentItem; @@ -88,8 +88,8 @@ Item { Wallpapers.previewColourLock = true; Wallpapers.setWallpaper(currentItem.modelData.path); root.visibilities.launcher = false; - } else if (text.startsWith(Config.launcher.actionPrefix)) { - if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) + } else if (text.startsWith(GlobalConfig.launcher.actionPrefix)) { + if (text.startsWith(`${GlobalConfig.launcher.actionPrefix}calc `)) currentItem.onClicked(); else currentItem.modelData.onClicked(list.currentList); diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 4667a540d..482d6c0d2 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -18,7 +18,7 @@ Item { required property int padding required property int rounding - readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) + readonly property bool showWallpapers: search.text.startsWith(`${GlobalConfig.launcher.actionPrefix}wallpaper `) readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly anchors.horizontalCenter: parent.horizontalCenter diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 7a3995496..a78349cde 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -78,7 +78,7 @@ Item { asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - active: root.modelData && Strings.testRegexList(Config.launcher.favouriteApps, root.modelData.id) + active: root.modelData && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, root.modelData.id) sourceComponent: MaterialIcon { text: "favorite" diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 668829911..534b6c10c 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -10,7 +10,7 @@ Item { id: root required property var list - readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) + readonly property string math: list.search.text.slice(`${GlobalConfig.launcher.actionPrefix}calc `.length) function onClicked(): void { Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 30b8bb2c8..13ec03614 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -11,7 +11,7 @@ Searcher { id: root function transformSearch(search: string): string { - return search.slice(Config.launcher.actionPrefix.length); + return search.slice(GlobalConfig.launcher.actionPrefix.length); } list: variants.instances @@ -39,7 +39,7 @@ Searcher { return; if (command[0] === "autocomplete" && command.length > 1) { - list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; + list.search.text = `${GlobalConfig.launcher.actionPrefix}${command[1]} `; } else if (command[0] === "setMode" && command.length > 1) { list.visibilities.launcher = false; Colours.setMode(command[1]); diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index 57461ffb3..ff05456bc 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -24,7 +24,7 @@ Searcher { } function search(search: string): list { - const prefix = Config.launcher.specialPrefix; + const prefix = GlobalConfig.launcher.specialPrefix; if (search.startsWith(`${prefix}i `)) { keys = ["id", "name"]; diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml index d7d7bf564..aa5d1ca76 100644 --- a/modules/launcher/services/M3Variants.qml +++ b/modules/launcher/services/M3Variants.qml @@ -10,7 +10,7 @@ Searcher { id: root function transformSearch(search: string): string { - return search.slice(`${Config.launcher.actionPrefix}variant `.length); + return search.slice(`${GlobalConfig.launcher.actionPrefix}variant `.length); } list: [ diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml index 571cb1013..bd2a10f78 100644 --- a/modules/launcher/services/Schemes.qml +++ b/modules/launcher/services/Schemes.qml @@ -14,7 +14,7 @@ Searcher { property string currentVariant function transformSearch(search: string): string { - return search.slice(`${Config.launcher.actionPrefix}scheme `.length); + return search.slice(`${GlobalConfig.launcher.actionPrefix}scheme `.length); } function selector(item: var): string { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index be7bb22d5..712e67093 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -58,7 +58,7 @@ ColumnLayout { Layout.leftMargin: Tokens.spacing.small Layout.alignment: Qt.AlignVCenter - active: Config.services.useTwelveHourClock + active: GlobalConfig.services.useTwelveHourClock visible: active sourceComponent: StyledText { diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index e76be9e6a..d96a54dfb 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -159,7 +159,7 @@ ColumnLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` + text: GlobalConfig.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` color: Colours.palette.m3secondary font.pointSize: Tokens.font.size.larger } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 012c4fe56..ff3295b7b 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -46,7 +46,7 @@ Item { icon: Icons.getVolumeIcon(value, root.muted) value: root.volume - to: Config.services.maxVolume + to: GlobalConfig.services.maxVolume onMoved: Audio.setVolume(value) } } @@ -71,7 +71,7 @@ Item { icon: Icons.getMicVolumeIcon(value, root.sourceMuted) value: root.sourceVolume - to: Config.services.maxVolume + to: GlobalConfig.services.maxVolume onMoved: Audio.setSourceVolume(value) } } diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index bf80dee34..13d6f7a12 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -93,7 +93,7 @@ StyledRect { id: activeText anchors.centerIn: parent - text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) + text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, GlobalConfig.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) color: Colours.palette.m3onPrimary font.pointSize: Math.round(Tokens.font.size.small * 0.9) } diff --git a/services/Weather.qml b/services/Weather.qml index e5037c40f..504c0bbb0 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -17,12 +17,12 @@ Singleton { readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string description: cc?.weatherDesc ?? qsTr("No weather") - readonly property string temp: Config.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` - readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` + readonly property string temp: GlobalConfig.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` + readonly property string feelsLike: GlobalConfig.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` readonly property int humidity: cc?.humidity ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0 - readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" - readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" + readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" + readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property var cachedCities: new Map() From 8f579a1e65872a37dd7801487dda50a2faf7df73 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:52:18 +1000 Subject: [PATCH 325/409] fix: ignore most operations if global only --- plugin/src/Caelestia/Config/configobject.cpp | 20 ++++++++++++++++---- plugin/src/Caelestia/Config/configobject.hpp | 1 + 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index b209b30a3..8b53b6d18 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -29,7 +29,7 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) { if (!obj.contains(key)) continue; - if (m_global && isGlobalOnly(key)) + if (isGlobalOnly(key)) qCWarning( lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", qUtf8Printable(key)); @@ -79,6 +79,10 @@ QJsonObject ConfigObject::toJsonObject() const { continue; const auto key = QString::fromUtf8(prop.name()); + + if (isGlobalOnly(key)) + continue; + const auto value = prop.read(this); // Recurse into sub-objects — include only if they have loaded keys @@ -125,6 +129,8 @@ void ConfigObject::clearLoadedKeys() { const auto* meta = metaObject(); for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { auto prop = meta->property(i); + if (isGlobalOnly(QString::fromUtf8(prop.name()))) + continue; auto value = prop.read(this); auto* subObj = value.value(); if (subObj) @@ -146,6 +152,9 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) { auto prop = meta->property(i); const auto key = QString::fromUtf8(prop.name()); + if (isGlobalOnly(key)) + continue; + auto current = prop.read(this); auto* subObj = current.value(); @@ -157,7 +166,7 @@ void ConfigObject::syncFromGlobal(ConfigObject* global) { continue; } - if (!prop.isWritable() || isGlobalOnly(key)) + if (!prop.isWritable()) continue; if (!m_loadedKeys.contains(key)) { @@ -180,6 +189,9 @@ void ConfigObject::resyncFromGlobal() { auto prop = meta->property(i); const auto key = QString::fromUtf8(prop.name()); + if (isGlobalOnly(key)) + continue; + auto current = prop.read(this); auto* subObj = current.value(); @@ -188,7 +200,7 @@ void ConfigObject::resyncFromGlobal() { continue; } - if (!prop.isWritable() || isGlobalOnly(key)) + if (!prop.isWritable()) continue; if (!m_loadedKeys.contains(key)) { @@ -207,7 +219,7 @@ bool ConfigObject::isOverlay() const { } bool ConfigObject::isGlobalOnly(const QString& name) const { - return m_globalOnlyKeys.contains(name); + return isOverlay() && m_globalOnlyKeys.contains(name); } void ConfigObject::markPropertyLoaded(const QString& name) { diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index 6666b7043..d13e6119e 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -89,6 +89,7 @@ class ConfigObject : public QObject { [[nodiscard]] bool isPropertyLoaded(const QString& name) const; [[nodiscard]] bool isOverlay() const; + // Returns true only on overlays — global singleton always returns false. [[nodiscard]] bool isGlobalOnly(const QString& name) const; Q_INVOKABLE void resetOption(const QString& name); From 666d451f4b063bb86775bd9da1a3af1211efc072 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:02:00 +1000 Subject: [PATCH 326/409] fix: global config use for rest of global props --- modules/BatteryMonitor.qml | 4 +- .../bar/popouts/kblayout/KbLayoutModel.qml | 2 +- .../controlcenter/launcher/LauncherPane.qml | 24 ++++++------ modules/controlcenter/launcher/Settings.qml | 16 ++++---- .../controlcenter/network/NetworkSettings.qml | 8 ++-- modules/controlcenter/network/VpnDetails.qml | 18 ++++----- modules/controlcenter/network/VpnList.qml | 38 +++++++++---------- modules/controlcenter/network/VpnSettings.qml | 20 +++++----- .../notifications/NotificationsPane.qml | 26 ++++++------- modules/launcher/Content.qml | 2 +- modules/launcher/services/Actions.qml | 4 +- modules/launcher/services/Apps.qml | 6 +-- modules/launcher/services/M3Variants.qml | 2 +- modules/launcher/services/Schemes.qml | 2 +- modules/notifications/Notification.qml | 2 +- modules/utilities/cards/Toggles.qml | 2 +- services/Audio.qml | 4 +- services/GameMode.qml | 4 +- services/Hypr.qml | 6 +-- services/NotifData.qml | 6 +-- services/Notifs.qml | 4 +- services/Players.qml | 2 +- services/VPN.qml | 6 +-- services/Wallpapers.qml | 2 +- 24 files changed, 105 insertions(+), 105 deletions(-) diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index b64122830..d0327ba06 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -12,10 +12,10 @@ Scope { Connections { function onOnBatteryChanged(): void { if (UPower.onBattery) { - if (Config.utilities.toasts.chargingChanged) + if (GlobalConfig.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); } else { - if (Config.utilities.toasts.chargingChanged) + if (GlobalConfig.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); for (const level of root.warnLevels) level.warned = false; diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index b6535847a..f62d0b989 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -106,7 +106,7 @@ Item { arr = arr.filter(i => i.layoutIndex !== activeIndex); arr.forEach(i => _visibleModel.append(i)); - if (!Config.utilities.toasts.kbLimit) + if (!GlobalConfig.utilities.toasts.kbLimit) return; if (_layoutsModel.count > 4) { diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index e5353fbab..70649bcea 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -37,8 +37,8 @@ Item { const appId = root.selectedApp.id || root.selectedApp.entry?.id; - root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); - root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); + root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); + root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); } function saveHiddenApps(isHidden) { @@ -48,7 +48,7 @@ Item { const appId = root.selectedApp.id || root.selectedApp.entry?.id; - const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; if (isHidden) { if (!hiddenApps.includes(appId)) { @@ -128,7 +128,7 @@ Item { id: allAppsDb path: `${Paths.state}/apps.sqlite` - favouriteApps: Config.launcher.favouriteApps + favouriteApps: GlobalConfig.launcher.favouriteApps entries: DesktopEntries.applications.values } @@ -358,8 +358,8 @@ Item { } Loader { - readonly property bool isHidden: modelData ? Strings.testRegexList(Config.launcher.hiddenApps, modelData.id) : false - readonly property bool isFav: modelData ? Strings.testRegexList(Config.launcher.favouriteApps, modelData.id) : false + readonly property bool isHidden: modelData ? Strings.testRegexList(GlobalConfig.launcher.hiddenApps, modelData.id) : false + readonly property bool isFav: modelData ? Strings.testRegexList(GlobalConfig.launcher.favouriteApps, modelData.id) : false Layout.alignment: Qt.AlignVCenter asynchronous: true @@ -422,8 +422,8 @@ Item { onDisplayedAppChanged: { if (displayedApp) { const appId = displayedApp.id || displayedApp.entry?.id; - root.hideFromLauncherChecked = Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0 && Strings.testRegexList(Config.launcher.hiddenApps, appId); - root.favouriteChecked = Config.launcher.favouriteApps && Config.launcher.favouriteApps.length > 0 && Strings.testRegexList(Config.launcher.favouriteApps, appId); + root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); + root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); } else { root.hideFromLauncherChecked = false; root.favouriteChecked = false; @@ -606,14 +606,14 @@ Item { // * app isn't in favouriteApps array but marked as favourite anyway // ^^^ This means that this app is favourited because of a regex check // this button can not toggle regexed apps - enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (Config.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) + enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (GlobalConfig.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) opacity: enabled ? 1 : 0.6 onToggled: checked => { root.favouriteChecked = checked; const app = appDetailsLayout.displayedApp; if (app) { const appId = app.id || app.entry?.id; - const favouriteApps = Config.launcher.favouriteApps ? [...Config.launcher.favouriteApps] : []; + const favouriteApps = GlobalConfig.launcher.favouriteApps ? [...GlobalConfig.launcher.favouriteApps] : []; if (checked) { if (!favouriteApps.includes(appId)) { favouriteApps.push(appId); @@ -638,14 +638,14 @@ Item { // * app isn't in hiddenApps array but marked as hidden anyway // ^^^ This means that this app is hidden because of a regex check // this button can not toggle regexed apps - enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (Config.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) + enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (GlobalConfig.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) opacity: enabled ? 1 : 0.6 onToggled: checked => { root.hideFromLauncherChecked = checked; const app = appDetailsLayout.displayedApp; if (app) { const appId = app.id || app.entry?.id; - const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; if (checked) { if (!hiddenApps.includes(appId)) { hiddenApps.push(appId); diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 657c4a286..f01812308 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -47,7 +47,7 @@ ColumnLayout { ToggleRow { label: qsTr("Vim keybinds") - checked: Config.launcher.vimKeybinds + checked: GlobalConfig.launcher.vimKeybinds toggle.onToggled: { GlobalConfig.launcher.vimKeybinds = checked; } @@ -55,7 +55,7 @@ ColumnLayout { ToggleRow { label: qsTr("Enable dangerous actions") - checked: Config.launcher.enableDangerousActions + checked: GlobalConfig.launcher.enableDangerousActions toggle.onToggled: { GlobalConfig.launcher.enableDangerousActions = checked; } @@ -119,7 +119,7 @@ ColumnLayout { SectionContainer { ToggleRow { label: qsTr("Apps") - checked: Config.launcher.useFuzzy.apps + checked: GlobalConfig.launcher.useFuzzy.apps toggle.onToggled: { GlobalConfig.launcher.useFuzzy.apps = checked; } @@ -127,7 +127,7 @@ ColumnLayout { ToggleRow { label: qsTr("Actions") - checked: Config.launcher.useFuzzy.actions + checked: GlobalConfig.launcher.useFuzzy.actions toggle.onToggled: { GlobalConfig.launcher.useFuzzy.actions = checked; } @@ -135,7 +135,7 @@ ColumnLayout { ToggleRow { label: qsTr("Schemes") - checked: Config.launcher.useFuzzy.schemes + checked: GlobalConfig.launcher.useFuzzy.schemes toggle.onToggled: { GlobalConfig.launcher.useFuzzy.schemes = checked; } @@ -143,7 +143,7 @@ ColumnLayout { ToggleRow { label: qsTr("Variants") - checked: Config.launcher.useFuzzy.variants + checked: GlobalConfig.launcher.useFuzzy.variants toggle.onToggled: { GlobalConfig.launcher.useFuzzy.variants = checked; } @@ -151,7 +151,7 @@ ColumnLayout { ToggleRow { label: qsTr("Wallpapers") - checked: Config.launcher.useFuzzy.wallpapers + checked: GlobalConfig.launcher.useFuzzy.wallpapers toggle.onToggled: { GlobalConfig.launcher.useFuzzy.wallpapers = checked; } @@ -202,7 +202,7 @@ ColumnLayout { PropertyRow { label: qsTr("Total hidden") - value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) + value: qsTr("%1").arg(GlobalConfig.launcher.hiddenApps ? GlobalConfig.launcher.hiddenApps.length : 0) } } } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index 5ed755fe8..d8700ecd9 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -65,15 +65,15 @@ ColumnLayout { Layout.topMargin: Tokens.spacing.large title: qsTr("VPN") description: qsTr("VPN provider settings") - visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 + visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 } SectionContainer { - visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 + visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 ToggleRow { label: qsTr("VPN enabled") - checked: Config.utilities.vpn.enabled + checked: GlobalConfig.utilities.vpn.enabled toggle.onToggled: { GlobalConfig.utilities.vpn.enabled = checked; } @@ -82,7 +82,7 @@ ColumnLayout { PropertyRow { showTopMargin: true label: qsTr("Providers") - value: qsTr("%1").arg(Config.utilities.vpn.provider.length) + value: qsTr("%1").arg(GlobalConfig.utilities.vpn.provider.length) } TextButton { diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index f0197f9c9..c146554f2 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -21,7 +21,7 @@ DeviceDetails { readonly property bool providerEnabled: { if (!vpnProvider || vpnProvider.index === undefined) return false; - const provider = Config.utilities.vpn.provider[vpnProvider.index]; + const provider = GlobalConfig.utilities.vpn.provider[vpnProvider.index]; return provider && typeof provider === "object" && provider.enabled === true; } @@ -55,8 +55,8 @@ DeviceDetails { const index = root.vpnProvider.index; // Copy providers and update enabled state - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - const p = Config.utilities.vpn.provider[i]; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, @@ -115,7 +115,7 @@ DeviceDetails { inactiveOnColour: Colours.palette.m3onSecondaryContainer onClicked: { - const provider = Config.utilities.vpn.provider[root.vpnProvider.index]; + const provider = GlobalConfig.utilities.vpn.provider[root.vpnProvider.index]; editVpnDialog.editIndex = root.vpnProvider.index; editVpnDialog.providerName = root.vpnProvider.name; editVpnDialog.displayName = root.vpnProvider.displayName; @@ -134,9 +134,9 @@ DeviceDetails { onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i !== root.vpnProvider.index) { - providers.push(Config.utilities.vpn.provider[i]); + providers.push(GlobalConfig.utilities.vpn.provider[i]); } } GlobalConfig.utilities.vpn.provider = providers; @@ -500,10 +500,10 @@ DeviceDetails { onClicked: { const providers = []; - const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex]; + const oldProvider = GlobalConfig.utilities.vpn.provider[editVpnDialog.editIndex]; const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i === editVpnDialog.editIndex) { const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0; const newProvider = { @@ -520,7 +520,7 @@ DeviceDetails { providers.push(newProvider); } else { - const p = Config.utilities.vpn.provider[i]; + const p = GlobalConfig.utilities.vpn.provider[i]; const reconstructed = { displayName: p.displayName, enabled: p.enabled, diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index dd63300d1..23fdf5b13 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -27,8 +27,8 @@ ColumnLayout { root.pendingSwitchIndex = -1; const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - const p = Config.utilities.vpn.provider[i]; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, @@ -79,7 +79,7 @@ ColumnLayout { spacing: Tokens.spacing.smaller model: ScriptModel { - values: Config.utilities.vpn.provider.map((provider, index) => { + values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { const isObject = typeof provider === "object"; const name = isObject ? (provider.name || "custom") : String(provider); const displayName = isObject ? (provider.displayName || name) : name; @@ -222,8 +222,8 @@ ColumnLayout { VPN.toggle(); } else { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - const p = Config.utilities.vpn.provider[i]; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, @@ -273,9 +273,9 @@ ColumnLayout { StateLayer { function onClicked(): void { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i !== modelData.index) { - const p = Config.utilities.vpn.provider[i]; + const p = GlobalConfig.utilities.vpn.provider[i]; const reconstructed = { name: p.name, displayName: p.displayName, @@ -346,7 +346,7 @@ ColumnLayout { } function showEditForm(index: int): void { - const provider = Config.utilities.vpn.provider[index]; + const provider = GlobalConfig.utilities.vpn.provider[index]; const isObject = typeof provider === "object"; editIndex = index; @@ -484,8 +484,8 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push({ name: "netbird", @@ -504,8 +504,8 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push({ name: "tailscale", @@ -524,8 +524,8 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push({ name: "warp", @@ -780,21 +780,21 @@ ColumnLayout { } if (vpnDialog.editIndex >= 0) { - const oldProvider = Config.utilities.vpn.provider[vpnDialog.editIndex]; + const oldProvider = GlobalConfig.utilities.vpn.provider[vpnDialog.editIndex]; if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) { newProvider.enabled = oldProvider.enabled; } - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i === vpnDialog.editIndex) { providers.push(newProvider); } else { - providers.push(Config.utilities.vpn.provider[i]); + providers.push(GlobalConfig.utilities.vpn.provider[i]); } } } else { - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push(newProvider); } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 27c1cc8e9..064f32a12 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -34,7 +34,7 @@ ColumnLayout { SectionContainer { ToggleRow { label: qsTr("VPN enabled") - checked: Config.utilities.vpn.enabled + checked: GlobalConfig.utilities.vpn.enabled toggle.onToggled: { GlobalConfig.utilities.vpn.enabled = checked; } @@ -58,7 +58,7 @@ ColumnLayout { spacing: Tokens.spacing.smaller model: ScriptModel { - values: Config.utilities.vpn.provider.map((provider, index) => { + values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { const isObject = typeof provider === "object"; const name = isObject ? (provider.name || "custom") : String(provider); const displayName = isObject ? (provider.displayName || name) : name; @@ -116,11 +116,11 @@ ColumnLayout { IconButton { icon: modelData.isActive ? "arrow_downward" : "arrow_upward" - visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1 + visible: !modelData.isActive || GlobalConfig.utilities.vpn.provider.length > 1 onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - const p = Config.utilities.vpn.provider[i]; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; const reconstructed = { name: p.name, displayName: p.displayName, @@ -155,9 +155,9 @@ ColumnLayout { icon: "delete" onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i !== index) { - const p = Config.utilities.vpn.provider[i]; + const p = GlobalConfig.utilities.vpn.provider[i]; const reconstructed = { name: p.name, displayName: p.displayName, @@ -210,7 +210,7 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { - const providers = [...Config.utilities.vpn.provider]; + const providers = [...GlobalConfig.utilities.vpn.provider]; providers.push({ name: "netbird", displayName: "NetBird", @@ -227,7 +227,7 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { - const providers = [...Config.utilities.vpn.provider]; + const providers = [...GlobalConfig.utilities.vpn.provider]; providers.push({ name: "tailscale", displayName: "Tailscale", @@ -244,7 +244,7 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { - const providers = [...Config.utilities.vpn.provider]; + const providers = [...GlobalConfig.utilities.vpn.provider]; providers.push({ name: "warp", displayName: "Cloudflare WARP", diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml index ea75c2a67..eda4e213a 100644 --- a/modules/controlcenter/notifications/NotificationsPane.qml +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -18,24 +18,24 @@ Item { required property Session session - property bool notificationsExpire: Config.notifs.expire ?? true - property string notificationsFullscreen: Config.notifs.fullscreen ?? "on" + property bool notificationsExpire: GlobalConfig.notifs.expire ?? true + property string notificationsFullscreen: GlobalConfig.notifs.fullscreen ?? "on" property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false - property int notificationsDefaultExpireTimeout: Config.notifs.defaultExpireTimeout ?? 5000 + property int notificationsDefaultExpireTimeout: GlobalConfig.notifs.defaultExpireTimeout ?? 5000 property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3 property int maxToasts: Config.utilities.maxToasts ?? 4 property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off" - property bool chargingChanged: Config.utilities.toasts.chargingChanged ?? true - property bool gameModeChanged: Config.utilities.toasts.gameModeChanged ?? true - property bool dndChanged: Config.utilities.toasts.dndChanged ?? true - property bool audioOutputChanged: Config.utilities.toasts.audioOutputChanged ?? true - property bool audioInputChanged: Config.utilities.toasts.audioInputChanged ?? true - property bool capsLockChanged: Config.utilities.toasts.capsLockChanged ?? true - property bool numLockChanged: Config.utilities.toasts.numLockChanged ?? true - property bool kbLayoutChanged: Config.utilities.toasts.kbLayoutChanged ?? true - property bool vpnChanged: Config.utilities.toasts.vpnChanged ?? true - property bool nowPlaying: Config.utilities.toasts.nowPlaying ?? false + property bool chargingChanged: GlobalConfig.utilities.toasts.chargingChanged ?? true + property bool gameModeChanged: GlobalConfig.utilities.toasts.gameModeChanged ?? true + property bool dndChanged: GlobalConfig.utilities.toasts.dndChanged ?? true + property bool audioOutputChanged: GlobalConfig.utilities.toasts.audioOutputChanged ?? true + property bool audioInputChanged: GlobalConfig.utilities.toasts.audioInputChanged ?? true + property bool capsLockChanged: GlobalConfig.utilities.toasts.capsLockChanged ?? true + property bool numLockChanged: GlobalConfig.utilities.toasts.numLockChanged ?? true + property bool kbLayoutChanged: GlobalConfig.utilities.toasts.kbLayoutChanged ?? true + property bool vpnChanged: GlobalConfig.utilities.toasts.vpnChanged ?? true + property bool nowPlaying: GlobalConfig.utilities.toasts.nowPlaying ?? false function saveConfig(): void { GlobalConfig.notifs.expire = root.notificationsExpire; diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 7e283c8f5..48a3d4faf 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -106,7 +106,7 @@ Item { Keys.onEscapePressed: root.visibilities.launcher = false Keys.onPressed: event => { - if (!Config.launcher.vimKeybinds) + if (!GlobalConfig.launcher.vimKeybinds) return; if (event.modifiers & Qt.ControlModifier) { diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 13ec03614..4ec249da2 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -15,12 +15,12 @@ Searcher { } list: variants.instances - useFuzzy: Config.launcher.useFuzzy.actions + useFuzzy: GlobalConfig.launcher.useFuzzy.actions Variants { id: variants - model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false))) + model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (GlobalConfig.launcher.enableDangerousActions || !(a.dangerous ?? false))) Action {} } diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index ff05456bc..5c80513e9 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -66,13 +66,13 @@ Searcher { } list: appDb.apps - useFuzzy: Config.launcher.useFuzzy.apps + useFuzzy: GlobalConfig.launcher.useFuzzy.apps AppDb { id: appDb path: `${Paths.state}/apps.sqlite` - favouriteApps: Config.launcher.favouriteApps - entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(Config.launcher.hiddenApps, a.id)) + favouriteApps: GlobalConfig.launcher.favouriteApps + entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(GlobalConfig.launcher.hiddenApps, a.id)) } } diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml index aa5d1ca76..4517d02cb 100644 --- a/modules/launcher/services/M3Variants.qml +++ b/modules/launcher/services/M3Variants.qml @@ -69,7 +69,7 @@ Searcher { description: qsTr("All colours are grayscale, no chroma.") } ] - useFuzzy: Config.launcher.useFuzzy.variants + useFuzzy: GlobalConfig.launcher.useFuzzy.variants component Variant: QtObject { required property string variant diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml index bd2a10f78..cc555f294 100644 --- a/modules/launcher/services/Schemes.qml +++ b/modules/launcher/services/Schemes.qml @@ -26,7 +26,7 @@ Searcher { } list: schemes.instances - useFuzzy: Config.launcher.useFuzzy.schemes + useFuzzy: GlobalConfig.launcher.useFuzzy.schemes keys: ["name", "flavour"] weights: [0.9, 0.1] diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index ca7596dc8..24c0c958f 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -81,7 +81,7 @@ StyledRect { } } onClicked: event => { - if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) + if (!GlobalConfig.notifs.actionOnClick || event.button !== Qt.LeftButton) return; const actions = root.modelData.actions; diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 49147a624..b2e145670 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -27,7 +27,7 @@ StyledRect { } if (item.id === "vpn") { - return Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); + return GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); } seenIds.add(item.id); diff --git a/services/Audio.qml b/services/Audio.qml index faf86a932..88fe9652d 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -111,7 +111,7 @@ Singleton { const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); - if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) + if (previousSinkName && previousSinkName !== newSinkName && GlobalConfig.utilities.toasts.audioOutputChanged) Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); previousSinkName = newSinkName; @@ -123,7 +123,7 @@ Singleton { const newSourceName = source.description || source.name || qsTr("Unknown Device"); - if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) + if (previousSourceName && previousSourceName !== newSourceName && GlobalConfig.utilities.toasts.audioInputChanged) Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); previousSourceName = newSourceName; diff --git a/services/GameMode.qml b/services/GameMode.qml index 44b0fe4c8..6dfc791c5 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -28,11 +28,11 @@ Singleton { onEnabledChanged: { if (enabled) { setDynamicConfs(); - if (Config.utilities.toasts.gameModeChanged) + if (GlobalConfig.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad"); } else { Hypr.extras.message("reload"); - if (Config.utilities.toasts.gameModeChanged) + if (GlobalConfig.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad"); } } diff --git a/services/Hypr.qml b/services/Hypr.qml index 054b611c5..181967e78 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -93,7 +93,7 @@ Singleton { Component.onCompleted: reloadDynamicConfs() onCapsLockChanged: { - if (!Config.utilities.toasts.capsLockChanged) + if (!GlobalConfig.utilities.toasts.capsLockChanged) return; if (capsLock) @@ -103,7 +103,7 @@ Singleton { } onNumLockChanged: { - if (!Config.utilities.toasts.numLockChanged) + if (!GlobalConfig.utilities.toasts.numLockChanged) return; if (numLock) @@ -113,7 +113,7 @@ Singleton { } onKbLayoutFullChanged: { - if (hadKeyboard && Config.utilities.toasts.kbLayoutChanged) + if (hadKeyboard && GlobalConfig.utilities.toasts.kbLayoutChanged) Toaster.toast(qsTr("Keyboard layout changed"), qsTr("Layout changed to: %1").arg(kbLayoutFull), "keyboard"); hadKeyboard = !!keyboard; diff --git a/services/NotifData.qml b/services/NotifData.qml index 1cec15100..c92a3ad78 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -33,7 +33,7 @@ QtObject { property string appName property string image property var hints // Hints are not persisted across restarts - property real expireTimeout: Config.notifs.defaultExpireTimeout + property real expireTimeout: GlobalConfig.notifs.defaultExpireTimeout property int urgency: NotificationUrgency.Normal property bool resident property bool hasActionIcons @@ -41,9 +41,9 @@ QtObject { readonly property Timer timer: Timer { running: true - interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout + interval: notif.expireTimeout > 0 ? notif.expireTimeout : GlobalConfig.notifs.defaultExpireTimeout onTriggered: { - if (Config.notifs.expire) + if (GlobalConfig.notifs.expire) notif.popup = false; } } diff --git a/services/Notifs.qml b/services/Notifs.qml index 510ee0bc3..d539d0a28 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -32,13 +32,13 @@ Singleton { function shouldShowPopup(): bool { if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) return false; - if (Config.notifs.fullscreen === "off" && hasFullscreen()) + if (GlobalConfig.notifs.fullscreen === "off" && hasFullscreen()) return false; return true; } onDndChanged: { - if (!Config.utilities.toasts.dndChanged) + if (!GlobalConfig.utilities.toasts.dndChanged) return; if (dnd) diff --git a/services/Players.qml b/services/Players.qml index 0724afbc5..23c9067fb 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -37,7 +37,7 @@ Singleton { Connections { function onPostTrackChanged() { - if (!Config.utilities.toasts.nowPlaying) { + if (!GlobalConfig.utilities.toasts.nowPlaying) { return; } if (root.active.trackArtist != "" && root.active.trackTitle != "") { diff --git a/services/VPN.qml b/services/VPN.qml index ac82a394a..61e91d0b2 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -18,9 +18,9 @@ Singleton { }) readonly property bool connecting: connectProc.running || disconnectProc.running - readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) + readonly property bool enabled: GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) readonly property var providerInput: { - const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); + const enabledProvider = GlobalConfig.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); return enabledProvider || "wireguard"; } readonly property bool isCustomProvider: typeof providerInput === "object" @@ -279,7 +279,7 @@ Singleton { } function emitStatusToast(statusObj: var): void { - if (!Config.utilities.toasts.vpnChanged) + if (!GlobalConfig.utilities.toasts.vpnChanged) return; const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 4ef522bad..3478d90cd 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -41,7 +41,7 @@ Searcher { list: wallpapers.entries key: "relativePath" - useFuzzy: Config.launcher.useFuzzy.wallpapers + useFuzzy: GlobalConfig.launcher.useFuzzy.wallpapers extraOpts: useFuzzy ? ({}) : ({ forward: false }) From 03c1e5060af2153766da0f0e55da0d3ce4232b49 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:04:57 +1000 Subject: [PATCH 327/409] feat: print full option path in warning --- plugin/src/Caelestia/Config/configobject.cpp | 36 ++++++++++++++++++-- plugin/src/Caelestia/Config/configobject.hpp | 7 ++-- 2 files changed, 39 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp index 8b53b6d18..030ecabe8 100644 --- a/plugin/src/Caelestia/Config/configobject.cpp +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -30,8 +30,8 @@ void ConfigObject::loadFromJson(const QJsonObject& obj) { continue; if (isGlobalOnly(key)) - qCWarning( - lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", qUtf8Printable(key)); + qCWarning(lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", + qUtf8Printable(propertyPath(key))); const auto jsonVal = obj.value(key); @@ -210,6 +210,38 @@ void ConfigObject::resyncFromGlobal() { } } +QString ConfigObject::propertyPath(const QString& name) const { + QStringList parts; + parts.append(name); + + const QObject* obj = this; + while (auto* parentObj = obj->parent()) { + auto* parentConfig = qobject_cast(parentObj); + if (!parentConfig) + break; + + // Find which property name this child is on the parent + const auto* meta = parentConfig->metaObject(); + bool found = false; + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + auto val = prop.read(parentObj); + if (val.value() == obj) { + parts.prepend(QString::fromUtf8(prop.name())); + found = true; + break; + } + } + + if (!found) + break; + + obj = parentObj; + } + + return parts.join(QLatin1Char('.')); +} + bool ConfigObject::isPropertyLoaded(const QString& name) const { return m_loadedKeys.contains(name); } diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index d13e6119e..c35d812c1 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -48,12 +48,14 @@ private: public: \ [[nodiscard]] Type name() const { \ if (isOverlay()) \ - qCWarning(caelestia::config::lcConfig, "Reading global-only option '%s' on per-monitor overlay", #name); \ + qCWarning(caelestia::config::lcConfig, "Reading global-only option '%s' on per-monitor overlay", \ + qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ return m_##name; \ } \ void set_##name(const Type& val) { \ if (isOverlay()) \ - qCWarning(caelestia::config::lcConfig, "Writing global-only option '%s' on per-monitor overlay", #name); \ + qCWarning(caelestia::config::lcConfig, "Writing global-only option '%s' on per-monitor overlay", \ + qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ markPropertyLoaded(QStringLiteral(#name)); \ Q_EMIT name##Changed(); \ @@ -88,6 +90,7 @@ class ConfigObject : public QObject { void clearLoadedKeys(); [[nodiscard]] bool isPropertyLoaded(const QString& name) const; + [[nodiscard]] QString propertyPath(const QString& name) const; [[nodiscard]] bool isOverlay() const; // Returns true only on overlays — global singleton always returns false. [[nodiscard]] bool isGlobalOnly(const QString& name) const; From 0432c4708e6c797d4f3d9de4ccfe3b6a583493e3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:22:48 +1000 Subject: [PATCH 328/409] fix: anims should not be per monitor --- .../src/Caelestia/Config/appearanceconfig.hpp | 2 +- plugin/src/Caelestia/Config/tokens.hpp | 32 +++++++++---------- .../src/Caelestia/Config/tokensattached.cpp | 12 ++----- 3 files changed, 19 insertions(+), 27 deletions(-) diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index c58105a71..f366482dc 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -169,7 +169,7 @@ class AnimDurations : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(qreal, scale, 1) + CONFIG_GLOBAL_PROPERTY(qreal, scale, 1) Q_PROPERTY(int small READ small NOTIFY valuesChanged) Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index ca42ccbdc..1a0c7b8fb 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -15,15 +15,15 @@ class AnimCurves : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QList, emphasized) - CONFIG_PROPERTY(QList, emphasizedAccel) - CONFIG_PROPERTY(QList, emphasizedDecel) - CONFIG_PROPERTY(QList, standard) - CONFIG_PROPERTY(QList, standardAccel) - CONFIG_PROPERTY(QList, standardDecel) - CONFIG_PROPERTY(QList, expressiveFastSpatial) - CONFIG_PROPERTY(QList, expressiveDefaultSpatial) - CONFIG_PROPERTY(QList, expressiveSlowSpatial) + CONFIG_GLOBAL_PROPERTY(QList, emphasized) + CONFIG_GLOBAL_PROPERTY(QList, emphasizedAccel) + CONFIG_GLOBAL_PROPERTY(QList, emphasizedDecel) + CONFIG_GLOBAL_PROPERTY(QList, standard) + CONFIG_GLOBAL_PROPERTY(QList, standardAccel) + CONFIG_GLOBAL_PROPERTY(QList, standardDecel) + CONFIG_GLOBAL_PROPERTY(QList, expressiveFastSpatial) + CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultSpatial) + CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowSpatial) public: explicit AnimCurves(QObject* parent = nullptr) @@ -103,13 +103,13 @@ class AnimDurationTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(int, small, 200) - CONFIG_PROPERTY(int, normal, 400) - CONFIG_PROPERTY(int, large, 600) - CONFIG_PROPERTY(int, extraLarge, 1000) - CONFIG_PROPERTY(int, expressiveFastSpatial, 350) - CONFIG_PROPERTY(int, expressiveDefaultSpatial, 500) - CONFIG_PROPERTY(int, expressiveSlowSpatial, 650) + CONFIG_GLOBAL_PROPERTY(int, small, 200) + CONFIG_GLOBAL_PROPERTY(int, normal, 400) + CONFIG_GLOBAL_PROPERTY(int, large, 600) + CONFIG_GLOBAL_PROPERTY(int, extraLarge, 1000) + CONFIG_GLOBAL_PROPERTY(int, expressiveFastSpatial, 350) + CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultSpatial, 500) + CONFIG_GLOBAL_PROPERTY(int, expressiveSlowSpatial, 650) public: explicit AnimDurationTokens(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 23ee28414..1dd159d73 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -29,19 +29,11 @@ void Tokens::connectScope() { if (!m_scope) return; connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged); - connect(m_scope, &ConfigScope::configChanged, this, &Tokens::bindAnim); } void Tokens::bindAnim() { - auto* appearance = resolveAppearance(m_scope); - if (!appearance) - return; - - m_anim->bindDurations(appearance->anim()->durations()); - - auto* tokens = TokenConfig::instance(); - if (tokens) - m_anim->bindCurves(tokens->appearance()->curves()); + m_anim->bindDurations(GlobalConfig::instance()->appearance()->anim()->durations()); + m_anim->bindCurves(TokenConfig::instance()->appearance()->curves()); } const AppearanceRounding* Tokens::rounding() const { From 5cef79d5e046d8b27c998cf438bbe37bba180121 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 12 Apr 2026 23:50:46 +1000 Subject: [PATCH 329/409] feat: warn on per monitor confs being accessed without scope --- .../src/Caelestia/Config/configattached.cpp | 3 ++ .../src/Caelestia/Config/tokensattached.cpp | 41 ++++++++----------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp index 55694c324..cc6ae8f95 100644 --- a/plugin/src/Caelestia/Config/configattached.cpp +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -26,6 +26,9 @@ void Config::connectScope() { const Type* Config::name() const { \ if (m_scope && m_scope->config()) \ return m_scope->config()->name(); \ + if (parent()) \ + qCWarning(lcConfig, "Config.%s accessed without a ConfigScope ancestor on %s", #name, \ + parent()->metaObject()->className()); \ return GlobalConfig::instance()->name(); \ } diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 1dd159d73..cf4acdb42 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -9,9 +9,12 @@ namespace caelestia::config { namespace { -const AppearanceConfig* resolveAppearance(ConfigScope* scope) { +const AppearanceConfig* resolveAppearance(ConfigScope* scope, const char* prop, QObject* parent) { if (scope && scope->config()) return scope->config()->appearance(); + if (parent) + qCWarning(lcConfig, "Tokens.%s accessed without a ConfigScope ancestor on %s", prop, + parent->metaObject()->className()); return GlobalConfig::instance()->appearance(); } @@ -36,34 +39,26 @@ void Tokens::bindAnim() { m_anim->bindCurves(TokenConfig::instance()->appearance()->curves()); } -const AppearanceRounding* Tokens::rounding() const { - auto* a = resolveAppearance(m_scope); - return a ? a->rounding() : nullptr; -} - -const AppearanceSpacing* Tokens::spacing() const { - auto* a = resolveAppearance(m_scope); - return a ? a->spacing() : nullptr; -} +#define TOKENS_ATTACHED_GETTER(Type, name) \ + const Type* Tokens::name() const { \ + auto* a = resolveAppearance(m_scope, #name, parent()); \ + return a ? a->name() : nullptr; \ + } -const AppearancePadding* Tokens::padding() const { - auto* a = resolveAppearance(m_scope); - return a ? a->padding() : nullptr; -} +TOKENS_ATTACHED_GETTER(AppearanceRounding, rounding) +TOKENS_ATTACHED_GETTER(AppearanceSpacing, spacing) +TOKENS_ATTACHED_GETTER(AppearancePadding, padding) +TOKENS_ATTACHED_GETTER(AppearanceFont, font) +TOKENS_ATTACHED_GETTER(AppearanceTransparency, transparency) -const AppearanceFont* Tokens::font() const { - auto* a = resolveAppearance(m_scope); - return a ? a->font() : nullptr; -} - -const AppearanceTransparency* Tokens::transparency() const { - auto* a = resolveAppearance(m_scope); - return a ? a->transparency() : nullptr; -} +#undef TOKENS_ATTACHED_GETTER const SizeTokens* Tokens::sizes() const { if (m_scope && m_scope->tokens()) return m_scope->tokens()->sizes(); + if (parent()) + qCWarning(lcConfig, "Tokens.sizes accessed without a ConfigScope ancestor on %s", + parent()->metaObject()->className()); return TokenConfig::instance()->sizes(); } From 2f5fcfa2fbeede87b707f419be5174544fd8dac1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:06:21 +1000 Subject: [PATCH 330/409] fix: move more properties to global only --- plugin/src/Caelestia/Config/barconfig.hpp | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp index 3ec57b006..8def79216 100644 --- a/plugin/src/Caelestia/Config/barconfig.hpp +++ b/plugin/src/Caelestia/Config/barconfig.hpp @@ -45,13 +45,13 @@ class BarWorkspaces : public ConfigObject { CONFIG_PROPERTY(bool, showWindowsOnSpecialWorkspaces, true) CONFIG_PROPERTY(int, maxWindowIcons, 0) CONFIG_PROPERTY(bool, activeTrail, false) - CONFIG_PROPERTY(bool, perMonitorWorkspaces, true) + CONFIG_GLOBAL_PROPERTY(bool, perMonitorWorkspaces, true) CONFIG_PROPERTY(QString, label, QStringLiteral(" ")) CONFIG_PROPERTY(QString, occupiedLabel, QStringLiteral("\U000f06af")) CONFIG_PROPERTY(QString, activeLabel, QStringLiteral("\U000f06af")) CONFIG_PROPERTY(QString, capitalisation, QStringLiteral("preserve")) - CONFIG_PROPERTY(QVariantList, specialWorkspaceIcons) - CONFIG_PROPERTY(QVariantList, windowIcons) + CONFIG_GLOBAL_PROPERTY(QVariantList, specialWorkspaceIcons) + CONFIG_GLOBAL_PROPERTY(QVariantList, windowIcons) public: explicit BarWorkspaces(QObject* parent = nullptr) @@ -78,8 +78,8 @@ class BarTray : public ConfigObject { CONFIG_PROPERTY(bool, background, false) CONFIG_PROPERTY(bool, recolour, false) CONFIG_PROPERTY(bool, compact, false) - CONFIG_PROPERTY(QVariantList, iconSubs) - CONFIG_PROPERTY(QStringList, hiddenIcons) + CONFIG_GLOBAL_PROPERTY(QVariantList, iconSubs) + CONFIG_GLOBAL_PROPERTY(QStringList, hiddenIcons) public: explicit BarTray(QObject* parent = nullptr) From 7e4b14f6ba179dfc093fb3cbe15c75e4e6913325 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:06:48 +1000 Subject: [PATCH 331/409] fix: per monitor props not having ancestor scope --- modules/bar/Bar.qml | 8 +++--- modules/bar/components/ActiveWindow.qml | 8 +++--- modules/bar/components/Tray.qml | 2 +- .../components/workspaces/ActiveIndicator.qml | 12 +++++---- .../workspaces/SpecialWorkspaces.qml | 6 ++--- .../bar/components/workspaces/Workspace.qml | 2 +- .../bar/components/workspaces/Workspaces.qml | 4 +-- modules/bar/popouts/Content.qml | 2 +- modules/controlcenter/taskbar/TaskbarPane.qml | 2 +- modules/dashboard/Media.qml | 6 ++--- modules/drawers/Regions.qml | 25 +++++++++++-------- modules/osd/Content.qml | 4 +-- services/Audio.qml | 16 ++++++------ services/Brightness.qml | 4 +-- services/LyricsService.qml | 12 ++++----- services/Players.qml | 4 +-- services/SystemUsage.qml | 4 +-- services/Time.qml | 2 +- services/Wallpapers.qml | 2 +- services/Weather.qml | 4 +-- utils/Icons.qml | 6 ++--- utils/SysInfo.qml | 8 +++--- 22 files changed, 74 insertions(+), 69 deletions(-) diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 27483d9f2..a2ed060e7 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -80,11 +80,11 @@ ColumnLayout { const ch = childAt(width / 2, y) as WrappedLoader; if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) { // Workspace scroll - const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); + const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); const specialWs = mon?.lastIpcObject.specialWorkspace.name; if (specialWs?.length > 0) Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`); - else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) + else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`); } else if (y < screen.height / 2 && Config.bar.scrollActions.volume) { // Volume scroll on top half @@ -96,9 +96,9 @@ ColumnLayout { // Brightness scroll on bottom half const monitor = Brightness.getMonitorForScreen(screen); if (angleDelta.y > 0) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); else if (angleDelta.y < 0) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); } } diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 6bd5ef3fd..b4bd3bba8 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -86,8 +86,8 @@ Item { id: metrics text: root.windowTitle - font.pointSize: Tokens.font.size.smaller - font.family: Tokens.font.family.mono + font.pointSize: root.Tokens.font.size.smaller + font.family: root.Tokens.font.family.mono elide: Qt.ElideRight elideWidth: root.maxHeight - icon.height @@ -120,10 +120,10 @@ Item { transform: [ Translate { - x: Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0 + x: root.Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0 }, Rotation { - angle: Config.bar.activeWindow.inverted ? 270 : 90 + angle: root.Config.bar.activeWindow.inverted ? 270 : 90 origin.x: text.implicitHeight / 2 origin.y: text.implicitHeight / 2 } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index ceb5e180a..80d5d0091 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -68,7 +68,7 @@ StyledRect { id: items model: ScriptModel { - values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id)) + values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) } TrayItem {} diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index ec782e3bf..19d6c7834 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -1,3 +1,5 @@ +pragma ComponentBehavior: Bound + import QtQuick import Caelestia.Config import qs.components @@ -61,13 +63,13 @@ StyledRect { } Behavior on leading { - enabled: Config.bar.workspaces.activeTrail + enabled: root.Config.bar.workspaces.activeTrail EAnim {} } Behavior on trailing { - enabled: Config.bar.workspaces.activeTrail + enabled: root.Config.bar.workspaces.activeTrail EAnim { duration: Tokens.anim.durations.normal * 2 @@ -75,19 +77,19 @@ StyledRect { } Behavior on currentSize { - enabled: Config.bar.workspaces.activeTrail + enabled: root.Config.bar.workspaces.activeTrail EAnim {} } Behavior on offset { - enabled: !Config.bar.workspaces.activeTrail + enabled: !root.Config.bar.workspaces.activeTrail EAnim {} } Behavior on size { - enabled: !Config.bar.workspaces.activeTrail + enabled: !root.Config.bar.workspaces.activeTrail EAnim {} } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index 7f43e3a74..cafecb7bc 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -15,7 +15,7 @@ Item { required property ShellScreen screen readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) - readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" + readonly property string activeSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" layer.enabled: true layer.effect: OpacityMask { @@ -95,7 +95,7 @@ Item { onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial)) model: ScriptModel { - values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) + values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!GlobalConfig.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) } preferredHighlightBegin: 0 @@ -347,7 +347,7 @@ Item { model: ScriptModel { values: { const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); - const maxIcons = Config.bar.workspaces.maxWindowIcons; + const maxIcons = root.Config.bar.workspaces.maxWindowIcons; return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; } } diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index 39c55ec66..bd581aab1 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -94,7 +94,7 @@ ColumnLayout { values: { const ws = root.ws; const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); - const maxIcons = Config.bar.workspaces.maxWindowIcons; + const maxIcons = root.Config.bar.workspaces.maxWindowIcons; return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; } } diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index e7f617c64..698312b24 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -14,8 +14,8 @@ StyledClippingRect { required property ShellScreen screen required property bool fullscreen - readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" - readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId + readonly property bool onSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" + readonly property int activeWsId: GlobalConfig.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId readonly property var occupied: { const occ = {}; diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 8b5d0db53..f1c2456ba 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -128,7 +128,7 @@ Item { Repeater { model: ScriptModel { - values: SystemTray.items.values.filter(i => !Config.bar.tray.hiddenIcons.includes(i.id)) + values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) } Popout { diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 75d70bffd..601fc09fe 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -43,7 +43,7 @@ Item { property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 - property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true + property bool workspacesPerMonitor: GlobalConfig.bar.workspaces.perMonitorWorkspaces ?? true property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true property bool scrollVolume: Config.bar.scrollActions.volume ?? true property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index e46388430..0a6979962 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -122,7 +122,7 @@ Item { id: visualiserBars model: Array.from({ - length: Config.services.visualiserBars + length: GlobalConfig.services.visualiserBars }, (_, i) => i) ShapePath { @@ -131,13 +131,13 @@ Item { required property int modelData readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) - readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars + readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars readonly property real magnitude: value * Tokens.sizes.dashboard.mediaVisualiserSize readonly property real cos: Math.cos(angle) readonly property real sin: Math.sin(angle) capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - strokeWidth: 360 / Config.services.visualiserBars - Tokens.spacing.small / 4 + strokeWidth: 360 / GlobalConfig.services.visualiserBars - Tokens.spacing.small / 4 strokeColor: Colours.palette.m3primary startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml index 022d96615..26b7b4c67 100644 --- a/modules/drawers/Regions.qml +++ b/modules/drawers/Regions.qml @@ -12,22 +12,25 @@ Region { required property Panels panels required property var win + readonly property real borderThickness: win.configScope.Config.border.thickness + readonly property real clampedThickness: win.configScope.Config.border.clampedThickness + x: bar.clampedWidth + win.dragMaskPadding - y: Config.border.clampedThickness + win.dragMaskPadding - width: win.width - bar.clampedWidth - Config.border.clampedThickness - win.dragMaskPadding * 2 - height: win.height - Config.border.clampedThickness * 2 - win.dragMaskPadding * 2 + y: clampedThickness + win.dragMaskPadding + width: win.width - bar.clampedWidth - clampedThickness - win.dragMaskPadding * 2 + height: win.height - clampedThickness * 2 - win.dragMaskPadding * 2 intersection: Intersection.Xor R { panel: root.panels.dashboard y: 0 - height: panel.height * (1 - root.panels.dashboard.offsetScale) + Config.border.thickness + height: panel.height * (1 - root.panels.dashboard.offsetScale) + root.borderThickness } R { panel: root.panels.launcher y: root.win.height - height - height: panel.height * (1 - root.panels.launcher.offsetScale) + Config.border.thickness + height: panel.height * (1 - root.panels.launcher.offsetScale) + root.borderThickness } R { @@ -35,7 +38,7 @@ Region { panel: root.panels.sessionWrapper x: root.win.width - width - width: panel.width * (1 - root.panels.session.offsetScale) + Config.border.thickness + sidebarRegion.width + width: panel.width * (1 - root.panels.session.offsetScale) + root.borderThickness + sidebarRegion.width } R { @@ -43,25 +46,25 @@ Region { panel: root.panels.sidebar x: root.win.width - width - width: panel.width * (1 - root.panels.sidebar.offsetScale) + Config.border.thickness + width: panel.width * (1 - root.panels.sidebar.offsetScale) + root.borderThickness } R { panel: root.panels.osdWrapper x: root.win.width - width - width: panel.width * (1 - root.panels.osd.offsetScale) + Config.border.thickness + sessionRegion.width + width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness + sessionRegion.width } R { panel: root.panels.notifications y: 0 - height: panel.height + Config.border.thickness + height: panel.height + root.borderThickness } R { panel: root.panels.utilities y: root.win.height - height - height: panel.height * (1 - root.panels.utilities.offsetScale) + Config.border.thickness + height: panel.height * (1 - root.panels.utilities.offsetScale) + root.borderThickness } R { @@ -73,7 +76,7 @@ Region { required property Item panel x: panel.x + root.bar.implicitWidth - y: panel.y + Config.border.thickness + y: panel.y + root.borderThickness width: panel.width height: panel.height intersection: Intersection.Subtract diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index ff3295b7b..7ea58ab73 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -87,9 +87,9 @@ Item { if (!monitor) return; if (event.angleDelta.y > 0) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); else if (event.angleDelta.y < 0) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); } implicitWidth: Tokens.sizes.osd.sliderWidth diff --git a/services/Audio.qml b/services/Audio.qml index 88fe9652d..6268b92ed 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -33,31 +33,31 @@ Singleton { function setVolume(newVolume: real): void { if (sink?.ready && sink?.audio) { sink.audio.muted = false; - sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + sink.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); } } function incrementVolume(amount: real): void { - setVolume(volume + (amount || Config.services.audioIncrement)); + setVolume(volume + (amount || GlobalConfig.services.audioIncrement)); } function decrementVolume(amount: real): void { - setVolume(volume - (amount || Config.services.audioIncrement)); + setVolume(volume - (amount || GlobalConfig.services.audioIncrement)); } function setSourceVolume(newVolume: real): void { if (source?.ready && source?.audio) { source.audio.muted = false; - source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + source.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); } } function incrementSourceVolume(amount: real): void { - setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); + setSourceVolume(sourceVolume + (amount || GlobalConfig.services.audioIncrement)); } function decrementSourceVolume(amount: real): void { - setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); + setSourceVolume(sourceVolume - (amount || GlobalConfig.services.audioIncrement)); } function setAudioSink(newSink: PwNode): void { @@ -80,7 +80,7 @@ Singleton { function setStreamVolume(stream: PwNode, newVolume: real): void { if (stream?.ready && stream?.audio) { stream.audio.muted = false; - stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + stream.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); } } @@ -166,7 +166,7 @@ Singleton { CavaProvider { id: cava - bars: Config.services.visualiserBars + bars: GlobalConfig.services.visualiserBars } BeatTracker { diff --git a/services/Brightness.qml b/services/Brightness.qml index 2cd628ec0..ac34cbc1c 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -50,13 +50,13 @@ Singleton { function increaseBrightness(): void { const monitor = getMonitor("active"); if (monitor) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); } function decreaseBrightness(): void { const monitor = getMonitor("active"); if (monitor) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); } onMonitorsChanged: { diff --git a/services/LyricsService.qml b/services/LyricsService.qml index 04fc64fbf..26a75e9b5 100644 --- a/services/LyricsService.qml +++ b/services/LyricsService.qml @@ -15,17 +15,17 @@ Singleton { property int currentIndex: -1 property bool loading: false property bool isManualSeeking: false - property bool lyricsVisible: Config.services.showLyrics + property bool lyricsVisible: GlobalConfig.services.showLyrics property string backend: "Local" - property string preferredBackend: Config.services.lyricsBackend + property string preferredBackend: GlobalConfig.services.lyricsBackend property real currentSongId: 0 property string loadedLocalFile: "" property real offset property int currentRequestId: 0 property var lyricsMap: ({}) - readonly property string lyricsDir: Paths.absolutePath(Config.paths.lyricsDir) - readonly property string lyricsMapFile: Paths.absolutePath(Config.paths.lyricsDir) + "/lyrics_map.json" + readonly property string lyricsDir: Paths.absolutePath(GlobalConfig.paths.lyricsDir) + readonly property string lyricsMapFile: lyricsDir + "/lyrics_map.json" readonly property alias model: lyricsModel readonly property alias candidatesModel: fetchedCandidatesModel readonly property var _netEaseHeaders: ({ @@ -68,7 +68,7 @@ Singleton { } function toggleVisibility() { - GlobalConfig.services.showLyrics = !Config.services.showLyrics; + GlobalConfig.services.showLyrics = !GlobalConfig.services.showLyrics; } function loadLyrics() { @@ -271,7 +271,7 @@ Singleton { } onPreferredBackendChanged: { - if (Config.services.lyricsBackend !== preferredBackend) { + if (GlobalConfig.services.lyricsBackend !== preferredBackend) { GlobalConfig.services.lyricsBackend = preferredBackend; } } diff --git a/services/Players.qml b/services/Players.qml index 23c9067fb..41507839d 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -12,11 +12,11 @@ Singleton { id: root readonly property list list: Mpris.players.values - readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null + readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === GlobalConfig.services.defaultPlayer) ?? list[0] ?? null property alias manualActive: props.manualActive function getIdentity(player: MprisPlayer): string { - const alias = Config.services.playerAliases.find(a => a.from === player.identity); + const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity); return alias?.to ?? player.identity; } diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index e1dc4947d..d2b190751 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -14,7 +14,7 @@ Singleton { property real cpuTemp // GPU properties - readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType + readonly property string gpuType: GlobalConfig.services.gpuType.toUpperCase() || autoGpuType property string autoGpuType: "NONE" property string gpuName: "" property real gpuPerc @@ -251,7 +251,7 @@ Singleton { Process { id: gpuTypeCheck - running: !Config.services.gpuType + running: !GlobalConfig.services.gpuType command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] stdout: StdioCollector { onStreamFinished: root.autoGpuType = text.trim() diff --git a/services/Time.qml b/services/Time.qml index f92c69a14..0db520d09 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -11,7 +11,7 @@ Singleton { readonly property int minutes: clock.minutes readonly property int seconds: clock.seconds - readonly property string timeStr: format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") + readonly property string timeStr: format(GlobalConfig.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") readonly property list timeComponents: timeStr.split(":") readonly property string hourStr: timeComponents[0] ?? "" readonly property string minuteStr: timeComponents[1] ?? "" diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index 3478d90cd..15daf5c16 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -12,7 +12,7 @@ Searcher { id: root readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt` - readonly property list smartArg: Config.services.smartScheme ? [] : ["--no-smart"] + readonly property list smartArg: GlobalConfig.services.smartScheme ? [] : ["--no-smart"] property bool showPreview: false readonly property string current: showPreview ? previewPath : actualCurrent diff --git a/services/Weather.qml b/services/Weather.qml index 504c0bbb0..9c30bf7a8 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -27,7 +27,7 @@ Singleton { readonly property var cachedCities: new Map() function reload(): void { - const configLocation = Config.services.weatherLocation; + const configLocation = GlobalConfig.services.weatherLocation; if (configLocation) { if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) { @@ -215,7 +215,7 @@ Singleton { root.reload(); } - target: Config.services + target: GlobalConfig.services } Timer { diff --git a/utils/Icons.qml b/utils/Icons.qml index 1d958cffe..c864d5530 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -107,7 +107,7 @@ Singleton { } function getAppCategoryIcon(name: string, fallback: string): string { - for (const iconConfig of Config.bar.workspaces.windowIcons) + for (const iconConfig of GlobalConfig.bar.workspaces.windowIcons) if (matchIconConfig(name, iconConfig)) return iconConfig.icon; @@ -212,7 +212,7 @@ Singleton { function getSpecialWsIcon(name: string): string { name = name.toLowerCase().slice("special:".length); - for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) + for (const iconConfig of GlobalConfig.bar.workspaces.specialWorkspaceIcons) if (matchIconConfig(name, iconConfig)) return iconConfig.icon; @@ -230,7 +230,7 @@ Singleton { } function getTrayIcon(id: string, icon: string): string { - for (const sub of Config.bar.tray.iconSubs) + for (const sub of GlobalConfig.bar.tray.iconSubs) if (sub.id === id) return sub.image ? Qt.resolvedUrl(sub.image) : Quickshell.iconPath(sub.icon); diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index 8081340ea..c715b8d28 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -36,11 +36,11 @@ Singleton { root.osIdLike = fd("ID_LIKE").split(" "); const logo = Quickshell.iconPath(fd("LOGO"), true); - if (Config.general.logo === "caelestia") { + if (GlobalConfig.general.logo === "caelestia") { root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`); root.isDefaultLogo = true; - } else if (Config.general.logo) { - root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); + } else if (GlobalConfig.general.logo) { + root.osLogo = Quickshell.iconPath(GlobalConfig.general.logo, true) || "file://" + Paths.absolutePath(GlobalConfig.general.logo); root.isDefaultLogo = false; } else if (logo) { root.osLogo = logo; @@ -54,7 +54,7 @@ Singleton { osRelease.reload(); } - target: Config.general + target: GlobalConfig.general } Timer { From bbba6e162d94e421195a7bf4ce43de74fdc38cbd Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:12:44 +1000 Subject: [PATCH 332/409] fix: global transparency --- plugin/src/Caelestia/Config/appearanceconfig.hpp | 6 +++--- plugin/src/Caelestia/Config/tokensattached.cpp | 5 ++++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index f366482dc..ccb732fdd 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -216,9 +216,9 @@ class AppearanceTransparency : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(bool, enabled, false) - CONFIG_PROPERTY(qreal, base, 0.85) - CONFIG_PROPERTY(qreal, layers, 0.4) + CONFIG_GLOBAL_PROPERTY(bool, enabled, false) + CONFIG_GLOBAL_PROPERTY(qreal, base, 0.85) + CONFIG_GLOBAL_PROPERTY(qreal, layers, 0.4) public: explicit AppearanceTransparency(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index cf4acdb42..6c05bb4b9 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -49,10 +49,13 @@ TOKENS_ATTACHED_GETTER(AppearanceRounding, rounding) TOKENS_ATTACHED_GETTER(AppearanceSpacing, spacing) TOKENS_ATTACHED_GETTER(AppearancePadding, padding) TOKENS_ATTACHED_GETTER(AppearanceFont, font) -TOKENS_ATTACHED_GETTER(AppearanceTransparency, transparency) #undef TOKENS_ATTACHED_GETTER +const AppearanceTransparency* Tokens::transparency() const { + return GlobalConfig::instance()->appearance()->transparency(); // Transparency is always global +} + const SizeTokens* Tokens::sizes() const { if (m_scope && m_scope->tokens()) return m_scope->tokens()->sizes(); From 8883156dd9f6b65e29546f9f91067b80e6ed9407 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 00:25:16 +1000 Subject: [PATCH 333/409] fix: add lock config scope Also global lock settings And fix some accesses --- modules/IdleMonitors.qml | 6 +- modules/lock/LockSurface.qml | 105 +++++++++++---------- modules/lock/Pam.qml | 6 +- plugin/src/Caelestia/Config/lockconfig.hpp | 4 +- utils/Paths.qml | 2 +- 5 files changed, 65 insertions(+), 58 deletions(-) diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml index 839cd6f63..417b0e8ae 100644 --- a/modules/IdleMonitors.qml +++ b/modules/IdleMonitors.qml @@ -11,7 +11,7 @@ Scope { id: root required property Lock lock - readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) + readonly property bool enabled: !GlobalConfig.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) function handleIdleAction(action: var): void { if (!action) @@ -29,7 +29,7 @@ Scope { LogindManager { onAboutToSleep: { - if (Config.general.idle.lockBeforeSleep) + if (GlobalConfig.general.idle.lockBeforeSleep) root.lock.lock.locked = true; } onLockRequested: root.lock.lock.locked = true @@ -37,7 +37,7 @@ Scope { } Variants { - model: Config.general.idle.timeouts + model: GlobalConfig.general.idle.timeouts IdleMonitor { required property var modelData diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index b269e1ea8..70c754d61 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -159,72 +159,79 @@ WlSessionLockSurface { } } - ScreencopyView { - id: background + ConfigScope { + id: configScope anchors.fill: parent - captureSource: root.screen - opacity: 0 - - layer.enabled: true - layer.effect: MultiEffect { - autoPaddingEnabled: false - blurEnabled: true - blur: 1 - blurMax: 64 - blurMultiplier: 1 - } - } - - Item { - id: lockContent - - readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 - readonly property int radius: size / 4 * Tokens.rounding.scale - - anchors.centerIn: parent - implicitWidth: size - implicitHeight: size - - rotation: 180 - scale: 0 + screen: root.screen - StyledRect { - id: lockBg + ScreencopyView { + id: background anchors.fill: parent - color: Colours.palette.m3surface - radius: parent.radius - opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 + captureSource: root.screen + opacity: 0 layer.enabled: true layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) + autoPaddingEnabled: false + blurEnabled: true + blur: 1 + blurMax: 64 + blurMultiplier: 1 } } - MaterialIcon { - id: lockIcon + Item { + id: lockContent + + readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 + readonly property int radius: size / 4 * Tokens.rounding.scale anchors.centerIn: parent - text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 4 - font.bold: true + implicitWidth: size + implicitHeight: size + rotation: 180 - } + scale: 0 - Content { - id: content + StyledRect { + id: lockBg - anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 - height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 + anchors.fill: parent + color: Colours.palette.m3surface + radius: parent.radius + opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 - lock: root - opacity: 0 - scale: 0 + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + blurMax: 15 + shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) + } + } + + MaterialIcon { + id: lockIcon + + anchors.centerIn: parent + text: "lock" + font.pointSize: Tokens.font.size.extraLarge * 4 + font.bold: true + rotation: 180 + } + + Content { + id: content + + anchors.centerIn: parent + width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 + height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 + + lock: root + opacity: 0 + scale: 0 + } } } } diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 8f780ea00..94832b206 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -82,7 +82,7 @@ Scope { property int errorTries function checkAvail(): void { - if (!available || !Config.lock.enableFprint || !root.lock.secure) { + if (!available || !GlobalConfig.lock.enableFprint || !root.lock.secure) { abort(); return; } @@ -113,7 +113,7 @@ Scope { // Isn't actually the real max tries as pam only reports completed // when max tries is reached. tries++; - if (tries < Config.lock.maxFprintTries) { + if (tries < GlobalConfig.lock.maxFprintTries) { // Restart if not actually real max tries root.fprintState = "fail"; start(); @@ -188,6 +188,6 @@ Scope { fprint.checkAvail(); } - target: Config.lock + target: GlobalConfig.lock } } diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp index 444b3051b..0d8aa6ba7 100644 --- a/plugin/src/Caelestia/Config/lockconfig.hpp +++ b/plugin/src/Caelestia/Config/lockconfig.hpp @@ -9,8 +9,8 @@ class LockConfig : public ConfigObject { QML_ANONYMOUS CONFIG_PROPERTY(bool, recolourLogo, false) - CONFIG_PROPERTY(bool, enableFprint, true) - CONFIG_PROPERTY(int, maxFprintTries, 3) + CONFIG_GLOBAL_PROPERTY(bool, enableFprint, true) + CONFIG_GLOBAL_PROPERTY(int, maxFprintTries, 3) CONFIG_PROPERTY(bool, hideNotifs, false) public: diff --git a/utils/Paths.qml b/utils/Paths.qml index aa967913a..97f6448a5 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -19,7 +19,7 @@ Singleton { readonly property string imagecache: `${cache}/imagecache` readonly property string notifimagecache: `${imagecache}/notifs` - readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(Config.paths.wallpaperDir) + readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(GlobalConfig.paths.wallpaperDir) readonly property string recsdir: Quickshell.env("CAELESTIA_RECORDINGS_DIR") || `${videos}/Recordings` readonly property string libdir: Quickshell.env("CAELESTIA_LIB_DIR") || "/usr/lib/caelestia" From 8c8d196e2042e2447088e5ed32258177d8471329 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:06:13 +1000 Subject: [PATCH 334/409] feat: use QQuickAttachedPropertyPropagator Instead of manually trying to find ancestor ConfigScope, set attached prop on ancestor and propagate via attached props --- components/containers/StyledWindow.qml | 10 +- modules/drawers/ContentWindow.qml | 10 +- modules/drawers/Regions.qml | 4 +- modules/lock/LockSurface.qml | 108 +++++++++--------- plugin/src/Caelestia/CMakeLists.txt | 2 +- plugin/src/Caelestia/Config/CMakeLists.txt | 2 +- plugin/src/Caelestia/Config/anim.hpp | 4 +- .../src/Caelestia/Config/configattached.cpp | 53 ++++++--- .../src/Caelestia/Config/configattached.hpp | 21 ++-- plugin/src/Caelestia/Config/configscope.cpp | 64 ----------- plugin/src/Caelestia/Config/configscope.hpp | 45 -------- plugin/src/Caelestia/Config/tokens.hpp | 2 - .../src/Caelestia/Config/tokensattached.cpp | 67 ++++++++--- .../src/Caelestia/Config/tokensattached.hpp | 22 +++- 14 files changed, 182 insertions(+), 232 deletions(-) delete mode 100644 plugin/src/Caelestia/Config/configscope.cpp delete mode 100644 plugin/src/Caelestia/Config/configscope.hpp diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml index c93465c78..8ea3c2273 100644 --- a/components/containers/StyledWindow.qml +++ b/components/containers/StyledWindow.qml @@ -8,16 +8,10 @@ PanelWindow { id: root required property string name - readonly property alias configScope: scope - default property alias contentData: scope.data WlrLayershell.namespace: `caelestia-${name}` color: "transparent" - ConfigScope { - id: scope - - anchors.fill: parent - screen: root.screen.name - } + contentItem.Config.screen: screen.name + contentItem.Tokens.screen: screen.name } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 4ff12c166..a321543b7 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -30,9 +30,9 @@ StyledWindow { } return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; } - property real borderThickness: hasFullscreen ? 0 : configScope.Config.border.thickness - readonly property real borderLayoutThickness: hasFullscreen ? 0 : configScope.Config.border.thickness - property real borderRounding: hasFullscreen ? 0 : configScope.Config.border.rounding + property real borderThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness + readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness + property real borderRounding: hasFullscreen ? 0 : contentItem.Config.border.rounding property real shadowOpacity: hasFullscreen ? 0 : 0.7 readonly property int dragMaskPadding: { @@ -44,8 +44,8 @@ StyledWindow { const thresholds = []; for (const panel of ["dashboard", "launcher", "session", "sidebar"]) - if (configScope.Config[panel].enabled) - thresholds.push(configScope.Config[panel].dragThreshold); + if (contentItem.Config[panel].enabled) + thresholds.push(contentItem.Config[panel].dragThreshold); return Math.max(...thresholds); } diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml index 26b7b4c67..47ad06804 100644 --- a/modules/drawers/Regions.qml +++ b/modules/drawers/Regions.qml @@ -12,8 +12,8 @@ Region { required property Panels panels required property var win - readonly property real borderThickness: win.configScope.Config.border.thickness - readonly property real clampedThickness: win.configScope.Config.border.clampedThickness + readonly property real borderThickness: win.contentItem.Config.border.thickness + readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness x: bar.clampedWidth + win.dragMaskPadding y: clampedThickness + win.dragMaskPadding diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 70c754d61..23bfd65fe 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -15,6 +15,9 @@ WlSessionLockSurface { readonly property alias unlocking: unlockAnim.running + contentItem.Config.screen: screen.name + contentItem.Tokens.screen: screen.name + color: "transparent" Connections { @@ -159,79 +162,72 @@ WlSessionLockSurface { } } - ConfigScope { - id: configScope + ScreencopyView { + id: background anchors.fill: parent - screen: root.screen + captureSource: root.screen + opacity: 0 + + layer.enabled: true + layer.effect: MultiEffect { + autoPaddingEnabled: false + blurEnabled: true + blur: 1 + blurMax: 64 + blurMultiplier: 1 + } + } + + Item { + id: lockContent + + readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 + readonly property int radius: size / 4 * Tokens.rounding.scale + + anchors.centerIn: parent + implicitWidth: size + implicitHeight: size - ScreencopyView { - id: background + rotation: 180 + scale: 0 + + StyledRect { + id: lockBg anchors.fill: parent - captureSource: root.screen - opacity: 0 + color: Colours.palette.m3surface + radius: parent.radius + opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 layer.enabled: true layer.effect: MultiEffect { - autoPaddingEnabled: false - blurEnabled: true - blur: 1 - blurMax: 64 - blurMultiplier: 1 + shadowEnabled: true + blurMax: 15 + shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) } } - Item { - id: lockContent - - readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 - readonly property int radius: size / 4 * Tokens.rounding.scale + MaterialIcon { + id: lockIcon anchors.centerIn: parent - implicitWidth: size - implicitHeight: size - + text: "lock" + font.pointSize: Tokens.font.size.extraLarge * 4 + font.bold: true rotation: 180 - scale: 0 - - StyledRect { - id: lockBg - - anchors.fill: parent - color: Colours.palette.m3surface - radius: parent.radius - opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 - - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) - } - } - - MaterialIcon { - id: lockIcon - - anchors.centerIn: parent - text: "lock" - font.pointSize: Tokens.font.size.extraLarge * 4 - font.bold: true - rotation: 180 - } + } - Content { - id: content + Content { + id: content - anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 - height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 + anchors.centerIn: parent + width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 + height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 - lock: root - opacity: 0 - scale: 0 - } + lock: root + opacity: 0 + scale: 0 } } } diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 01f43b212..fbee04822 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,4 +1,4 @@ -find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick Concurrent Sql Network DBus) +find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt index 9322d76ca..4b26ea02d 100644 --- a/plugin/src/Caelestia/Config/CMakeLists.txt +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -5,7 +5,6 @@ qml_module(caelestia-config configattached.cpp configobject.cpp rootconfig.cpp - configscope.cpp appearanceconfig.cpp tokens.cpp tokensattached.cpp @@ -29,4 +28,5 @@ qml_module(caelestia-config winfoconfig.hpp LIBRARIES Qt::Quick + Qt::QuickControls2 ) diff --git a/plugin/src/Caelestia/Config/anim.hpp b/plugin/src/Caelestia/Config/anim.hpp index 1d5909a78..b90c3bda9 100644 --- a/plugin/src/Caelestia/Config/anim.hpp +++ b/plugin/src/Caelestia/Config/anim.hpp @@ -8,11 +8,11 @@ namespace caelestia::config { class AnimCurves; class AnimDurations; -class ConfigScope; class AnimTokens : public QObject { Q_OBJECT - Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("tokens.hpp") // AnimCurves + Q_MOC_INCLUDE("appearanceconfig.hpp") // AnimDurations QML_ANONYMOUS Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp index cc6ae8f95..5cafe34a3 100644 --- a/plugin/src/Caelestia/Config/configattached.cpp +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -1,33 +1,56 @@ #include "configattached.hpp" #include "config.hpp" -#include "configscope.hpp" #include "monitorconfigmanager.hpp" namespace caelestia::config { -Config::Config(ConfigScope* scope, QObject* parent) - : QObject(parent) - , m_scope(scope) { - connectScope(); +Config::Config(QObject* parent) + : QQuickAttachedPropertyPropagator(parent) { + initialize(); } -ConfigScope* Config::scope() const { - return m_scope; +QString Config::screen() const { + return m_screen; } -void Config::connectScope() { - if (!m_scope) +void Config::inheritScreen(const QString& screen) { + if (screen == m_screen) return; - connect(m_scope, &ConfigScope::configChanged, this, &Config::sourceChanged); - connect(m_scope, &ConfigScope::tokensChanged, this, &Config::sourceChanged); + + m_screen = screen; + + if (m_screen.isEmpty()) + m_config = nullptr; + else + m_config = MonitorConfigManager::instance()->configForScreen(m_screen); + + propagateScreen(); + emit sourceChanged(); +} + +void Config::propagateScreen() { + const auto children = attachedChildren(); + for (auto* const child : children) { + auto* const config = qobject_cast(child); + if (config) + config->inheritScreen(m_screen); + } +} + +void Config::attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { + Q_UNUSED(oldParent); + auto* const config = qobject_cast(newParent); + if (config) + inheritScreen(config->screen()); } #define CONFIG_ATTACHED_GETTER(Type, name) \ const Type* Config::name() const { \ - if (m_scope && m_scope->config()) \ - return m_scope->config()->name(); \ + if (m_config) \ + return m_config->name(); \ if (parent()) \ - qCWarning(lcConfig, "Config.%s accessed without a ConfigScope ancestor on %s", #name, \ + qCWarning(lcConfig, "Config.%s accessed without a screen set on %s", #name, \ parent()->metaObject()->className()); \ return GlobalConfig::instance()->name(); \ } @@ -57,7 +80,7 @@ GlobalConfig* Config::forScreen(const QString& screen) { } Config* Config::qmlAttachedProperties(QObject* object) { - return new Config(ConfigScope::find(object), object); + return new Config(object); } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp index d63c330fe..2b4c32a1d 100644 --- a/plugin/src/Caelestia/Config/configattached.hpp +++ b/plugin/src/Caelestia/Config/configattached.hpp @@ -1,17 +1,18 @@ #pragma once #include "config.hpp" -#include "configscope.hpp" + +#include namespace caelestia::config { -class Config : public QObject { +class Config : public QQuickAttachedPropertyPropagator { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Config) - Q_PROPERTY(caelestia::config::ConfigScope* scope READ scope NOTIFY sourceChanged) + Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) @@ -31,9 +32,10 @@ class Config : public QObject { Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) public: - explicit Config(ConfigScope* scope, QObject* parent = nullptr); + explicit Config(QObject* parent = nullptr); - [[nodiscard]] ConfigScope* scope() const; + [[nodiscard]] QString screen() const; + void inheritScreen(const QString& screen); [[nodiscard]] const AppearanceConfig* appearance() const; [[nodiscard]] const GeneralConfig* general() const; @@ -60,10 +62,15 @@ class Config : public QObject { signals: void sourceChanged(); +protected: + void attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; + private: - void connectScope(); + void propagateScreen(); - ConfigScope* m_scope; + QString m_screen; + GlobalConfig* m_config = nullptr; }; } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configscope.cpp b/plugin/src/Caelestia/Config/configscope.cpp deleted file mode 100644 index 769f2b73f..000000000 --- a/plugin/src/Caelestia/Config/configscope.cpp +++ /dev/null @@ -1,64 +0,0 @@ -#include "configscope.hpp" -#include "monitorconfigmanager.hpp" - -namespace caelestia::config { - -ConfigScope::ConfigScope(QQuickItem* parent) - : QQuickItem(parent) {} - -QString ConfigScope::screen() const { - return m_screen; -} - -GlobalConfig* ConfigScope::config() const { - return m_config; -} - -TokenConfig* ConfigScope::tokens() const { - return m_tokens; -} - -void ConfigScope::setScreen(const QString& screen) { - if (m_screen == screen) - return; - - m_screen = screen; - emit screenChanged(); - resolveConfig(); -} - -void ConfigScope::resolveConfig() { - GlobalConfig* newConfig = nullptr; - TokenConfig* newTokens = nullptr; - - if (!m_screen.isEmpty()) { - if (auto* mgr = MonitorConfigManager::instance()) { - newConfig = mgr->configForScreen(m_screen); - newTokens = mgr->tokensForScreen(m_screen); - } - } - - if (m_config != newConfig) { - m_config = newConfig; - emit configChanged(); - } - - if (m_tokens != newTokens) { - m_tokens = newTokens; - emit tokensChanged(); - } -} - -ConfigScope* ConfigScope::find(QObject* object) { - auto* item = qobject_cast(object); - - while (item) { - if (auto* scope = qobject_cast(item)) - return scope; - item = item->parentItem(); - } - - return nullptr; -} - -} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configscope.hpp b/plugin/src/Caelestia/Config/configscope.hpp deleted file mode 100644 index 828698167..000000000 --- a/plugin/src/Caelestia/Config/configscope.hpp +++ /dev/null @@ -1,45 +0,0 @@ -#pragma once - -#include -#include - -namespace caelestia::config { - -class GlobalConfig; -class TokenConfig; - -class ConfigScope : public QQuickItem { - Q_OBJECT - Q_MOC_INCLUDE("config.hpp") - Q_MOC_INCLUDE("tokens.hpp") - QML_ELEMENT - - Q_PROPERTY(QString screen READ screen WRITE setScreen NOTIFY screenChanged) - Q_PROPERTY(caelestia::config::GlobalConfig* config READ config NOTIFY configChanged) - Q_PROPERTY(caelestia::config::TokenConfig* tokens READ tokens NOTIFY tokensChanged) - -public: - explicit ConfigScope(QQuickItem* parent = nullptr); - - [[nodiscard]] QString screen() const; - void setScreen(const QString& screen); - - [[nodiscard]] GlobalConfig* config() const; - [[nodiscard]] TokenConfig* tokens() const; - - static ConfigScope* find(QObject* object); - -signals: - void screenChanged(); - void configChanged(); - void tokensChanged(); - -private: - void resolveConfig(); - - QString m_screen; - GlobalConfig* m_config = nullptr; - TokenConfig* m_tokens = nullptr; -}; - -} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 1a0c7b8fb..de0a7c263 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -9,8 +9,6 @@ namespace caelestia::config { -class ConfigScope; - class AnimCurves : public ConfigObject { Q_OBJECT QML_ANONYMOUS diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 6c05bb4b9..b9cf5745d 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -1,7 +1,6 @@ #include "tokensattached.hpp" #include "anim.hpp" #include "config.hpp" -#include "configscope.hpp" #include "monitorconfigmanager.hpp" #include "tokens.hpp" @@ -9,29 +8,60 @@ namespace caelestia::config { namespace { -const AppearanceConfig* resolveAppearance(ConfigScope* scope, const char* prop, QObject* parent) { - if (scope && scope->config()) - return scope->config()->appearance(); +const AppearanceConfig* resolveAppearance(GlobalConfig* config, const char* prop, QObject* parent) { + if (config) + return config->appearance(); if (parent) - qCWarning(lcConfig, "Tokens.%s accessed without a ConfigScope ancestor on %s", prop, - parent->metaObject()->className()); + qCWarning(lcConfig, "Tokens.%s accessed without a screen set on %s", prop, parent->metaObject()->className()); return GlobalConfig::instance()->appearance(); } } // namespace -Tokens::Tokens(ConfigScope* scope, QObject* parent) - : QObject(parent) - , m_scope(scope) +Tokens::Tokens(QObject* parent) + : QQuickAttachedPropertyPropagator(parent) , m_anim(new AnimTokens(this)) { - connectScope(); bindAnim(); + initialize(); } -void Tokens::connectScope() { - if (!m_scope) +QString Tokens::screen() const { + return m_screen; +} + +void Tokens::inheritScreen(const QString& screen) { + if (screen == m_screen) return; - connect(m_scope, &ConfigScope::configChanged, this, &Tokens::sourceChanged); + + m_screen = screen; + + if (m_screen.isEmpty()) { + m_config = nullptr; + m_tokens = nullptr; + } else { + m_config = MonitorConfigManager::instance()->configForScreen(m_screen); + m_tokens = MonitorConfigManager::instance()->tokensForScreen(m_screen); + } + + propagateScreen(); + emit sourceChanged(); +} + +void Tokens::propagateScreen() { + const auto children = attachedChildren(); + for (auto* const child : children) { + auto* const tokens = qobject_cast(child); + if (tokens) + tokens->inheritScreen(m_screen); + } +} + +void Tokens::attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { + Q_UNUSED(oldParent); + auto* const tokens = qobject_cast(newParent); + if (tokens) + inheritScreen(tokens->screen()); } void Tokens::bindAnim() { @@ -41,7 +71,7 @@ void Tokens::bindAnim() { #define TOKENS_ATTACHED_GETTER(Type, name) \ const Type* Tokens::name() const { \ - auto* a = resolveAppearance(m_scope, #name, parent()); \ + auto* a = resolveAppearance(m_config, #name, parent()); \ return a ? a->name() : nullptr; \ } @@ -57,11 +87,10 @@ const AppearanceTransparency* Tokens::transparency() const { } const SizeTokens* Tokens::sizes() const { - if (m_scope && m_scope->tokens()) - return m_scope->tokens()->sizes(); + if (m_tokens) + return m_tokens->sizes(); if (parent()) - qCWarning(lcConfig, "Tokens.sizes accessed without a ConfigScope ancestor on %s", - parent()->metaObject()->className()); + qCWarning(lcConfig, "Tokens.sizes accessed without a screen set on %s", parent()->metaObject()->className()); return TokenConfig::instance()->sizes(); } @@ -74,7 +103,7 @@ TokenConfig* Tokens::forScreen(const QString& screen) { } Tokens* Tokens::qmlAttachedProperties(QObject* object) { - return new Tokens(ConfigScope::find(object), object); + return new Tokens(object); } } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp index 88cf01026..4fa1a9b58 100644 --- a/plugin/src/Caelestia/Config/tokensattached.hpp +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -2,17 +2,20 @@ #include "anim.hpp" #include "appearanceconfig.hpp" -#include "configscope.hpp" +#include "config.hpp" #include "tokens.hpp" +#include + namespace caelestia::config { -class Tokens : public QObject { +class Tokens : public QQuickAttachedPropertyPropagator { Q_OBJECT QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Tokens) + Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) @@ -22,7 +25,10 @@ class Tokens : public QObject { Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) public: - explicit Tokens(ConfigScope* scope, QObject* parent = nullptr); + explicit Tokens(QObject* parent = nullptr); + + [[nodiscard]] QString screen() const; + void inheritScreen(const QString& screen); [[nodiscard]] const AppearanceRounding* rounding() const; [[nodiscard]] const AppearanceSpacing* spacing() const; @@ -40,11 +46,17 @@ class Tokens : public QObject { signals: void sourceChanged(); +protected: + void attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; + private: - void connectScope(); + void propagateScreen(); void bindAnim(); - ConfigScope* m_scope; + QString m_screen; + GlobalConfig* m_config = nullptr; + TokenConfig* m_tokens = nullptr; AnimTokens* m_anim = nullptr; }; From 9de094b5edf9a941687ffd9b8d2f415d69cb2d10 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:14:10 +1000 Subject: [PATCH 335/409] fix: suppress warnings before component is complete --- plugin/src/Caelestia/Config/configattached.cpp | 10 +++++++++- plugin/src/Caelestia/Config/configattached.hpp | 7 ++++++- plugin/src/Caelestia/Config/tokensattached.cpp | 14 ++++++++++---- plugin/src/Caelestia/Config/tokensattached.hpp | 7 ++++++- 4 files changed, 31 insertions(+), 7 deletions(-) diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp index 5cafe34a3..0a97c8c06 100644 --- a/plugin/src/Caelestia/Config/configattached.cpp +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -2,6 +2,8 @@ #include "config.hpp" #include "monitorconfigmanager.hpp" +#include + namespace caelestia::config { Config::Config(QObject* parent) @@ -9,6 +11,12 @@ Config::Config(QObject* parent) initialize(); } +void Config::classBegin() {} + +void Config::componentComplete() { + m_complete = true; +} + QString Config::screen() const { return m_screen; } @@ -49,7 +57,7 @@ void Config::attachedParentChange( const Type* Config::name() const { \ if (m_config) \ return m_config->name(); \ - if (parent()) \ + if (m_complete && parent()) /* Suppress warnings before component is complete */ \ qCWarning(lcConfig, "Config.%s accessed without a screen set on %s", #name, \ parent()->metaObject()->className()); \ return GlobalConfig::instance()->name(); \ diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp index 2b4c32a1d..35ccc6e41 100644 --- a/plugin/src/Caelestia/Config/configattached.hpp +++ b/plugin/src/Caelestia/Config/configattached.hpp @@ -6,8 +6,9 @@ namespace caelestia::config { -class Config : public QQuickAttachedPropertyPropagator { +class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { Q_OBJECT + Q_INTERFACES(QQmlParserStatus) QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Config) @@ -67,8 +68,12 @@ class Config : public QQuickAttachedPropertyPropagator { QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; private: + void classBegin() override; + void componentComplete() override; + void propagateScreen(); + bool m_complete = false; QString m_screen; GlobalConfig* m_config = nullptr; }; diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index b9cf5745d..627b50bfd 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -8,10 +8,10 @@ namespace caelestia::config { namespace { -const AppearanceConfig* resolveAppearance(GlobalConfig* config, const char* prop, QObject* parent) { +const AppearanceConfig* resolveAppearance(GlobalConfig* config, bool complete, const char* prop, QObject* parent) { if (config) return config->appearance(); - if (parent) + if (complete && parent) qCWarning(lcConfig, "Tokens.%s accessed without a screen set on %s", prop, parent->metaObject()->className()); return GlobalConfig::instance()->appearance(); } @@ -25,6 +25,12 @@ Tokens::Tokens(QObject* parent) initialize(); } +void Tokens::classBegin() {} + +void Tokens::componentComplete() { + m_complete = true; +} + QString Tokens::screen() const { return m_screen; } @@ -71,7 +77,7 @@ void Tokens::bindAnim() { #define TOKENS_ATTACHED_GETTER(Type, name) \ const Type* Tokens::name() const { \ - auto* a = resolveAppearance(m_config, #name, parent()); \ + auto* a = resolveAppearance(m_config, m_complete, #name, parent()); \ return a ? a->name() : nullptr; \ } @@ -89,7 +95,7 @@ const AppearanceTransparency* Tokens::transparency() const { const SizeTokens* Tokens::sizes() const { if (m_tokens) return m_tokens->sizes(); - if (parent()) + if (m_complete && parent()) qCWarning(lcConfig, "Tokens.sizes accessed without a screen set on %s", parent()->metaObject()->className()); return TokenConfig::instance()->sizes(); } diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp index 4fa1a9b58..6416bda5d 100644 --- a/plugin/src/Caelestia/Config/tokensattached.hpp +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -9,8 +9,9 @@ namespace caelestia::config { -class Tokens : public QQuickAttachedPropertyPropagator { +class Tokens : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { Q_OBJECT + Q_INTERFACES(QQmlParserStatus) QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Tokens) @@ -51,9 +52,13 @@ class Tokens : public QQuickAttachedPropertyPropagator { QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; private: + void classBegin() override; + void componentComplete() override; + void propagateScreen(); void bindAnim(); + bool m_complete = false; QString m_screen; GlobalConfig* m_config = nullptr; TokenConfig* m_tokens = nullptr; From 06ad45d88a4d949b710f498a389e91a832b393ee Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:23:29 +1000 Subject: [PATCH 336/409] fix: more non global access warnings --- modules/controlcenter/appearance/AppearancePane.qml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 6ede9bb37..a1e0e42c6 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -23,7 +23,7 @@ Item { required property Session session - property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 + property real animDurationsScale: GlobalConfig.appearance.anim.durations.scale ?? 1 property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" @@ -31,9 +31,9 @@ Item { property real paddingScale: Config.appearance.padding.scale ?? 1 property real roundingScale: Config.appearance.rounding.scale ?? 1 property real spacingScale: Config.appearance.spacing.scale ?? 1 - property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false - property real transparencyBase: Config.appearance.transparency.base ?? 0.85 - property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4 + property bool transparencyEnabled: GlobalConfig.appearance.transparency.enabled ?? false + property real transparencyBase: GlobalConfig.appearance.transparency.base ?? 0.85 + property real transparencyLayers: GlobalConfig.appearance.transparency.layers ?? 0.4 property real borderRounding: Config.border.rounding ?? 1 property real borderThickness: Config.border.thickness ?? 1 From 6a91535677632ec8c168908c7301935a43158cde Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 03:43:53 +1000 Subject: [PATCH 337/409] fix: don't suppress warnings on non Item components --- plugin/src/Caelestia/Config/configattached.cpp | 4 +++- plugin/src/Caelestia/Config/tokensattached.cpp | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp index 0a97c8c06..8d229049b 100644 --- a/plugin/src/Caelestia/Config/configattached.cpp +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -57,7 +57,9 @@ void Config::attachedParentChange( const Type* Config::name() const { \ if (m_config) \ return m_config->name(); \ - if (m_complete && parent()) /* Suppress warnings before component is complete */ \ + /* Suppress warnings before component is complete if attached to a QQuickItem. */ \ + /* Raw QObjects are unable to inherit the screen (only QQuickItems can). */ \ + if ((m_complete || !qobject_cast(parent())) && parent()) \ qCWarning(lcConfig, "Config.%s accessed without a screen set on %s", #name, \ parent()->metaObject()->className()); \ return GlobalConfig::instance()->name(); \ diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index 627b50bfd..a86550df8 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -4,6 +4,8 @@ #include "monitorconfigmanager.hpp" #include "tokens.hpp" +#include + namespace caelestia::config { namespace { @@ -11,7 +13,7 @@ namespace { const AppearanceConfig* resolveAppearance(GlobalConfig* config, bool complete, const char* prop, QObject* parent) { if (config) return config->appearance(); - if (complete && parent) + if ((complete || !qobject_cast(parent)) && parent) qCWarning(lcConfig, "Tokens.%s accessed without a screen set on %s", prop, parent->metaObject()->className()); return GlobalConfig::instance()->appearance(); } @@ -95,7 +97,7 @@ const AppearanceTransparency* Tokens::transparency() const { const SizeTokens* Tokens::sizes() const { if (m_tokens) return m_tokens->sizes(); - if (m_complete && parent()) + if ((m_complete || !qobject_cast(parent())) && parent()) qCWarning(lcConfig, "Tokens.sizes accessed without a screen set on %s", parent()->metaObject()->className()); return TokenConfig::instance()->sizes(); } From 8106aca17358090088c141f03bba896a05fe382b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 15:53:43 +1000 Subject: [PATCH 338/409] fix: even more non global access --- components/controls/CircularProgress.qml | 4 +-- components/controls/StyledSwitch.qml | 4 +-- modules/BatteryMonitor.qml | 4 +-- modules/background/Background.qml | 6 ++--- modules/bar/popouts/TrayMenu.qml | 2 +- .../controlcenter/dashboard/DashboardPane.qml | 4 +-- modules/dashboard/Media.qml | 8 +++--- modules/dashboard/Performance.qml | 2 +- modules/dashboard/dash/Media.qml | 26 +++++++++---------- modules/drawers/ContentWindow.qml | 2 +- modules/drawers/Drawers.qml | 1 - modules/drawers/Exclusions.qml | 4 +-- modules/lock/Center.qml | 4 +-- modules/lock/LockSurface.qml | 6 ++--- modules/lock/NotifGroup.qml | 4 +-- modules/notifications/Notification.qml | 4 +-- modules/osd/Wrapper.qml | 2 +- modules/sidebar/NotifGroupList.qml | 2 +- .../src/Caelestia/Config/dashboardconfig.hpp | 4 +-- services/NetworkUsage.qml | 2 +- services/SystemUsage.qml | 2 +- 21 files changed, 48 insertions(+), 49 deletions(-) diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index 9e3122807..f66e02e0d 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -27,7 +27,7 @@ Shape { fillColor: "transparent" strokeColor: root.bgColour strokeWidth: root.strokeWidth - capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle + 360 * root.vValue + root.gapAngle @@ -49,7 +49,7 @@ Shape { fillColor: "transparent" strokeColor: root.fgColour strokeWidth: root.strokeWidth - capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index a3439130d..df1bfc718 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -88,10 +88,10 @@ Switch { asynchronous: true ShapePath { - strokeWidth: Tokens.font.size.larger * 0.15 + strokeWidth: root.Tokens.font.size.larger * 0.15 strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest fillColor: "transparent" - capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap startX: icon.start1.x startY: icon.start1.y diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index d0327ba06..2d13b5a32 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -7,7 +7,7 @@ import Caelestia.Config Scope { id: root - readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) + readonly property list warnLevels: [...GlobalConfig.general.battery.warnLevels].sort((a, b) => b.level - a.level) Connections { function onOnBatteryChanged(): void { @@ -38,7 +38,7 @@ Scope { } } - if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) { + if (!hibernateTimer.running && p <= GlobalConfig.general.battery.criticalLevel) { Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error); hibernateTimer.start(); } diff --git a/modules/background/Background.qml b/modules/background/Background.qml index fde27ef45..af63848cb 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -8,7 +8,7 @@ import qs.components.containers import qs.services Variants { - model: Config.background.enabled ? Screens.screens : [] + model: Screens.screens.filter(s => GlobalConfig.forScreen(s.name).background.enabled) StyledWindow { id: win @@ -18,8 +18,8 @@ Variants { screen: modelData name: "background" WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom - color: Config.background.wallpaperEnabled ? "black" : "transparent" + WlrLayershell.layer: contentItem.Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom + color: contentItem.Config.background.wallpaperEnabled ? "black" : "transparent" surfaceFormat.opaque: false anchors.top: true diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 9dfcb4a76..493292386 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -152,7 +152,7 @@ StackView { font.family: label.font.family elide: Text.ElideRight - elideWidth: Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Tokens.spacing.normal : 0) + elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.normal : 0) } Loader { diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 46e33ebcf..6bde196a9 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -22,8 +22,8 @@ Item { // General Settings property bool enabled: Config.dashboard.enabled ?? true property bool showOnHover: Config.dashboard.showOnHover ?? true - property int mediaUpdateInterval: Config.dashboard.mediaUpdateInterval ?? 1000 - property int resourceUpdateInterval: Config.dashboard.resourceUpdateInterval ?? 1000 + property int mediaUpdateInterval: GlobalConfig.dashboard.mediaUpdateInterval ?? 1000 + property int resourceUpdateInterval: GlobalConfig.dashboard.resourceUpdateInterval ?? 1000 property int dragThreshold: Config.dashboard.dragThreshold ?? 50 // Dashboard Tabs diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 0a6979962..277d12cc4 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -67,7 +67,7 @@ Item { Timer { running: Players.active?.isPlaying ?? false - interval: Config.dashboard.mediaUpdateInterval + interval: GlobalConfig.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: { @@ -132,12 +132,12 @@ Item { readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars - readonly property real magnitude: value * Tokens.sizes.dashboard.mediaVisualiserSize + readonly property real magnitude: value * root.Tokens.sizes.dashboard.mediaVisualiserSize readonly property real cos: Math.cos(angle) readonly property real sin: Math.sin(angle) - capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - strokeWidth: 360 / GlobalConfig.services.visualiserBars - Tokens.spacing.small / 4 + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 strokeColor: Colours.palette.m3primary startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index cfed92967..cfcc78f71 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -705,7 +705,7 @@ Item { property: "slideProgress" from: 0 to: 1 - duration: Config.dashboard.resourceUpdateInterval + duration: GlobalConfig.dashboard.resourceUpdateInterval } Behavior on smoothMax { diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 2dd8a4af6..c83efde56 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -26,7 +26,7 @@ Item { Timer { running: Players.active?.isPlaying ?? false - interval: Config.dashboard.mediaUpdateInterval + interval: GlobalConfig.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() @@ -42,16 +42,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness - capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small - radiusY: (cover.height + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small - startAngle: -90 - Tokens.sizes.dashboard.mediaProgressSweep / 2 - sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep + radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 + sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep } Behavior on strokeColor { @@ -62,16 +62,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.palette.m3primary - strokeWidth: Tokens.sizes.dashboard.mediaProgressThickness - capStyle: Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small - radiusY: (cover.height + Tokens.sizes.dashboard.mediaProgressThickness) / 2 + Tokens.spacing.small - startAngle: -90 - Tokens.sizes.dashboard.mediaProgressSweep / 2 - sweepAngle: Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress + radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 + sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress } Behavior on strokeColor { diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index a321543b7..328d13ea0 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -95,7 +95,7 @@ StyledWindow { HyprlandFocusGrab { id: focusGrab - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) + active: (visibilities.launcher && root.contentItem.Config.launcher.enabled) || (visibilities.session && root.contentItem.Config.session.enabled) || (visibilities.sidebar && root.contentItem.Config.sidebar.enabled) || (!root.contentItem.Config.dashboard.showOnHover && visibilities.dashboard && root.contentItem.Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) windows: [root] onCleared: { visibilities.launcher = false; diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index d2cd05f9d..1bfde878c 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -16,7 +16,6 @@ Variants { Exclusions { screen: scope.modelData bar: content.bar - borderThickness: Config.border.thickness } ContentWindow { diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index 87610ee6b..fe72730c3 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell +import Caelestia.Config import qs.components.containers import qs.modules.bar as Bar @@ -10,7 +11,6 @@ Scope { required property ShellScreen screen required property Bar.BarWrapper bar - required property real borderThickness ExclusionZone { anchors.left: true @@ -32,7 +32,7 @@ Scope { component ExclusionZone: StyledWindow { screen: root.screen name: "border-exclusion" - exclusiveZone: root.borderThickness + exclusiveZone: contentItem.Config.border.thickness mask: Region {} implicitWidth: 1 implicitHeight: 1 diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 712e67093..349f44d27 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -160,13 +160,13 @@ ColumnLayout { anchors.centerIn: parent animate: true text: { - if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) + if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) return "fingerprint_off"; if (root.lock.pam.fprint.active) return "fingerprint"; return "lock"; } - color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface + color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface opacity: root.lock.pam.passwd.active ? 0 : 1 Behavior on opacity { diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 23bfd65fe..f08b528d9 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -142,19 +142,19 @@ WlSessionLockSurface { Anim { target: lockBg property: "radius" - to: Tokens.rounding.large * 1.5 + to: lockContent.Tokens.rounding.large * 1.5 } Anim { target: lockContent property: "implicitWidth" - to: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio + to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio duration: Tokens.anim.durations.expressiveDefaultSpatial easing: Tokens.anim.expressiveDefaultSpatial } Anim { target: lockContent property: "implicitHeight" - to: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult + to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult duration: Tokens.anim.durations.expressiveDefaultSpatial easing: Tokens.anim.expressiveDefaultSpatial } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 07a6a1ca2..50ea168d2 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -219,7 +219,7 @@ StyledRect { Repeater { model: ScriptModel { - values: root.notifs.slice(0, Config.notifs.groupPreviewNum) + values: root.notifs.slice(0, root.Config.notifs.groupPreviewNum) } NotifLine { @@ -282,7 +282,7 @@ StyledRect { sourceComponent: ColumnLayout { Repeater { model: ScriptModel { - values: root.notifs.slice(Config.notifs.groupPreviewNum) + values: root.notifs.slice(root.Config.notifs.groupPreviewNum) } NotifLine {} diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 24c0c958f..55dea458b 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -207,9 +207,9 @@ StyledRect { PathAngleArc { id: progressArc - radiusX: progressIndicator.width / 2 - Tokens.padding.small / 2 + radiusX: progressIndicator.width / 2 - root.Tokens.padding.small / 2 centerX: progressIndicator.width / 2 - radiusY: progressIndicator.height / 2 - Tokens.padding.small / 2 + radiusY: progressIndicator.height / 2 - root.Tokens.padding.small / 2 centerY: progressIndicator.height / 2 startAngle: -90 diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 7884c3190..2dac52224 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -87,7 +87,7 @@ Item { Timer { id: timer - interval: Config.osd.hideDelay + interval: root.Config.osd.hideDelay onTriggered: { if (!root.hovered) root.visibilities.osd = false; diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 935068568..7b0463912 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -42,7 +42,7 @@ LazyListView { let count = 0; let i = 0; - const previewNum = Config.notifs.groupPreviewNum; + const previewNum = root.Config.notifs.groupPreviewNum; while (i < root.notifs.length && count < previewNum) { if (!(root.notifs[i]?.closed ?? true)) count++; diff --git a/plugin/src/Caelestia/Config/dashboardconfig.hpp b/plugin/src/Caelestia/Config/dashboardconfig.hpp index d4b0ba893..cb872d1dd 100644 --- a/plugin/src/Caelestia/Config/dashboardconfig.hpp +++ b/plugin/src/Caelestia/Config/dashboardconfig.hpp @@ -30,8 +30,8 @@ class DashboardConfig : public ConfigObject { CONFIG_PROPERTY(bool, showMedia, true) CONFIG_PROPERTY(bool, showPerformance, true) CONFIG_PROPERTY(bool, showWeather, true) - CONFIG_PROPERTY(int, mediaUpdateInterval, 500) - CONFIG_PROPERTY(int, resourceUpdateInterval, 1000) + CONFIG_GLOBAL_PROPERTY(int, mediaUpdateInterval, 500) + CONFIG_GLOBAL_PROPERTY(int, resourceUpdateInterval, 1000) CONFIG_PROPERTY(int, dragThreshold, 50) CONFIG_SUBOBJECT(DashboardPerformance, performance) diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml index a6598713b..6c4dc8d25 100644 --- a/services/NetworkUsage.qml +++ b/services/NetworkUsage.qml @@ -155,7 +155,7 @@ Singleton { } Timer { - interval: Config.dashboard.resourceUpdateInterval + interval: GlobalConfig.dashboard.resourceUpdateInterval running: root.refCount > 0 repeat: true triggeredOnStart: true diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index d2b190751..15dda6111 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -80,7 +80,7 @@ Singleton { Timer { running: root.refCount > 0 - interval: Config.dashboard.resourceUpdateInterval + interval: GlobalConfig.dashboard.resourceUpdateInterval repeat: true triggeredOnStart: true onTriggered: { From 16a77f0df995aa69ad0a893459c398eaa611f0c1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:42:38 +1000 Subject: [PATCH 339/409] chore: remove unused import --- modules/drawers/Drawers.qml | 1 - 1 file changed, 1 deletion(-) diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 1bfde878c..aad8f9f40 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import Caelestia.Config import qs.services Variants { From 3cffc1ff944adbb1beeaf2e0c8bf8ed88aa0175f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 16:52:36 +1000 Subject: [PATCH 340/409] fix: even MORE non global access --- modules/bar/components/workspaces/SpecialWorkspaces.qml | 6 +++--- modules/utilities/toasts/Toasts.qml | 2 +- plugin/src/Caelestia/Config/barconfig.hpp | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index cafecb7bc..f45191f61 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -263,7 +263,7 @@ Item { function onLastIpcObjectChanged(): void { if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; } target: ws.modelData @@ -272,10 +272,10 @@ Item { Connections { function onShowWindowsOnSpecialWorkspacesChanged(): void { if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; } - target: Config.bar.workspaces + target: root.Config.bar.workspaces } Loader { diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index bceb4ea67..cce8b9f26 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -47,7 +47,7 @@ Item { toasts.push(toast); if (!toast.closed) { count++; - if (count > Config.utilities.maxToasts) + if (count > root.Config.utilities.maxToasts) break; } } diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp index 8def79216..6397064f0 100644 --- a/plugin/src/Caelestia/Config/barconfig.hpp +++ b/plugin/src/Caelestia/Config/barconfig.hpp @@ -43,7 +43,7 @@ class BarWorkspaces : public ConfigObject { CONFIG_PROPERTY(bool, occupiedBg, false) CONFIG_PROPERTY(bool, showWindows, true) CONFIG_PROPERTY(bool, showWindowsOnSpecialWorkspaces, true) - CONFIG_PROPERTY(int, maxWindowIcons, 0) + CONFIG_PROPERTY(int, maxWindowIcons, 5) CONFIG_PROPERTY(bool, activeTrail, false) CONFIG_GLOBAL_PROPERTY(bool, perMonitorWorkspaces, true) CONFIG_PROPERTY(QString, label, QStringLiteral(" ")) From 0571adc80cd547d1c3b5a2c3c61b57bfe051276e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:11:54 +1000 Subject: [PATCH 341/409] fix: add back list option defaults --- modules/utilities/cards/Toggles.qml | 2 +- plugin/src/Caelestia/Config/barconfig.hpp | 29 ++++-- plugin/src/Caelestia/Config/configobject.hpp | 11 +++ plugin/src/Caelestia/Config/generalconfig.hpp | 50 ++++++++-- .../src/Caelestia/Config/launcherconfig.hpp | 91 ++++++++++++++++++- plugin/src/Caelestia/Config/serviceconfig.hpp | 19 +++- plugin/src/Caelestia/Config/sessionconfig.hpp | 18 ++-- plugin/src/Caelestia/Config/userpaths.hpp | 16 ++-- .../src/Caelestia/Config/utilitiesconfig.hpp | 15 ++- 9 files changed, 213 insertions(+), 38 deletions(-) diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index b2e145670..9cdf4291d 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -19,7 +19,7 @@ StyledRect { const seenIds = new Set(); return Config.utilities.quickToggles.filter(item => { - if (!item.enabled) + if (!(item.enabled ?? true)) return false; if (seenIds.has(item.id)) { diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp index 6397064f0..43d43205a 100644 --- a/plugin/src/Caelestia/Config/barconfig.hpp +++ b/plugin/src/Caelestia/Config/barconfig.hpp @@ -8,6 +8,8 @@ namespace caelestia::config { +using Qt::StringLiterals::operator""_s; + class BarScrollActions : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -46,12 +48,16 @@ class BarWorkspaces : public ConfigObject { CONFIG_PROPERTY(int, maxWindowIcons, 5) CONFIG_PROPERTY(bool, activeTrail, false) CONFIG_GLOBAL_PROPERTY(bool, perMonitorWorkspaces, true) - CONFIG_PROPERTY(QString, label, QStringLiteral(" ")) - CONFIG_PROPERTY(QString, occupiedLabel, QStringLiteral("\U000f06af")) - CONFIG_PROPERTY(QString, activeLabel, QStringLiteral("\U000f06af")) - CONFIG_PROPERTY(QString, capitalisation, QStringLiteral("preserve")) + CONFIG_PROPERTY(QString, label, u" "_s) + CONFIG_PROPERTY(QString, occupiedLabel, u"󰮯"_s) + CONFIG_PROPERTY(QString, activeLabel, u"󰮯"_s) + CONFIG_PROPERTY(QString, capitalisation, u"preserve"_s) CONFIG_GLOBAL_PROPERTY(QVariantList, specialWorkspaceIcons) - CONFIG_GLOBAL_PROPERTY(QVariantList, windowIcons) + CONFIG_GLOBAL_PROPERTY(QVariantList, windowIcons, + { vmap({ + { u"regex"_s, u"steam(_app_(default|[0-9]+))?"_s }, + { u"icon"_s, u"sports_esports"_s }, + }) }) public: explicit BarWorkspaces(QObject* parent = nullptr) @@ -131,7 +137,18 @@ class BarConfig : public ConfigObject { CONFIG_SUBOBJECT(BarTray, tray) CONFIG_SUBOBJECT(BarStatus, status) CONFIG_SUBOBJECT(BarClock, clock) - CONFIG_PROPERTY(QVariantList, entries) + CONFIG_PROPERTY(QVariantList, entries, + { + vmap({ { u"id"_s, u"logo"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"workspaces"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"activeWindow"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"tray"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"clock"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"statusIcons"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"power"_s }, { u"enabled"_s, true } }), + }) CONFIG_PROPERTY(QStringList, excludedScreens) public: diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp index c35d812c1..c4f4428b3 100644 --- a/plugin/src/Caelestia/Config/configobject.hpp +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -9,6 +9,17 @@ #include #include +namespace caelestia::config { + +inline QVariantMap vmap(std::initializer_list> entries) { + QVariantMap map; + for (const auto& [key, value] : entries) + map.insert(std::move(key), std::move(value)); + return map; +} + +} // namespace caelestia::config + // Declares a serialized config property with getter, setter (change-detected), signal, and member. #define CONFIG_PROPERTY(Type, name, ...) \ Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp index f84cfa34d..73a4915d1 100644 --- a/plugin/src/Caelestia/Config/generalconfig.hpp +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -8,14 +8,16 @@ namespace caelestia::config { +using Qt::StringLiterals::operator""_s; + class GeneralApps : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_GLOBAL_PROPERTY(QStringList, terminal, { QStringLiteral("foot") }) - CONFIG_GLOBAL_PROPERTY(QStringList, audio, { QStringLiteral("pavucontrol") }) - CONFIG_GLOBAL_PROPERTY(QStringList, playback, { QStringLiteral("mpv") }) - CONFIG_GLOBAL_PROPERTY(QStringList, explorer, { QStringLiteral("thunar") }) + CONFIG_GLOBAL_PROPERTY(QStringList, terminal, { u"foot"_s }) + CONFIG_GLOBAL_PROPERTY(QStringList, audio, { u"pavucontrol"_s }) + CONFIG_GLOBAL_PROPERTY(QStringList, playback, { u"mpv"_s }) + CONFIG_GLOBAL_PROPERTY(QStringList, explorer, { u"thunar"_s }) public: explicit GeneralApps(QObject* parent = nullptr) @@ -28,7 +30,22 @@ class GeneralIdle : public ConfigObject { CONFIG_GLOBAL_PROPERTY(bool, lockBeforeSleep, true) CONFIG_GLOBAL_PROPERTY(bool, inhibitWhenAudio, true) - CONFIG_GLOBAL_PROPERTY(QVariantList, timeouts) + CONFIG_GLOBAL_PROPERTY(QVariantList, timeouts, + { + vmap({ + { u"timeout"_s, 180 }, + { u"idleAction"_s, u"lock"_s }, + }), + vmap({ + { u"timeout"_s, 300 }, + { u"idleAction"_s, u"dpms off"_s }, + { u"returnAction"_s, u"dpms on"_s }, + }), + vmap({ + { u"timeout"_s, 600 }, + { u"idleAction"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, + }), + }) public: explicit GeneralIdle(QObject* parent = nullptr) @@ -39,7 +56,28 @@ class GeneralBattery : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_GLOBAL_PROPERTY(QVariantList, warnLevels) + CONFIG_GLOBAL_PROPERTY(QVariantList, warnLevels, + { + vmap({ + { u"level"_s, 20 }, + { u"title"_s, u"Low battery"_s }, + { u"message"_s, u"You might want to plug in a charger"_s }, + { u"icon"_s, u"battery_android_frame_2"_s }, + }), + vmap({ + { u"level"_s, 10 }, + { u"title"_s, u"Did you see the previous message?"_s }, + { u"message"_s, u"You should probably plug in a charger now"_s }, + { u"icon"_s, u"battery_android_frame_1"_s }, + }), + vmap({ + { u"level"_s, 5 }, + { u"title"_s, u"Critical battery level"_s }, + { u"message"_s, u"PLUG THE CHARGER RIGHT NOW!!"_s }, + { u"icon"_s, u"battery_android_alert"_s }, + { u"critical"_s, true }, + }), + }) CONFIG_GLOBAL_PROPERTY(int, criticalLevel, 3) public: diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp index b0c8d06bb..049e213ae 100644 --- a/plugin/src/Caelestia/Config/launcherconfig.hpp +++ b/plugin/src/Caelestia/Config/launcherconfig.hpp @@ -8,6 +8,8 @@ namespace caelestia::config { +using Qt::StringLiterals::operator""_s; + class LauncherUseFuzzy : public ConfigObject { Q_OBJECT QML_ANONYMOUS @@ -31,15 +33,98 @@ class LauncherConfig : public ConfigObject { CONFIG_PROPERTY(bool, showOnHover, false) CONFIG_PROPERTY(int, maxShown, 7) CONFIG_PROPERTY(int, maxWallpapers, 9) - CONFIG_GLOBAL_PROPERTY(QString, specialPrefix, QStringLiteral("@")) - CONFIG_GLOBAL_PROPERTY(QString, actionPrefix, QStringLiteral(">")) + CONFIG_GLOBAL_PROPERTY(QString, specialPrefix, u"@"_s) + CONFIG_GLOBAL_PROPERTY(QString, actionPrefix, u">"_s) CONFIG_GLOBAL_PROPERTY(bool, enableDangerousActions, false) CONFIG_PROPERTY(int, dragThreshold, 50) CONFIG_GLOBAL_PROPERTY(bool, vimKeybinds, false) CONFIG_GLOBAL_PROPERTY(QStringList, favouriteApps) CONFIG_GLOBAL_PROPERTY(QStringList, hiddenApps) CONFIG_SUBOBJECT(LauncherUseFuzzy, useFuzzy) - CONFIG_GLOBAL_PROPERTY(QVariantList, actions) + CONFIG_GLOBAL_PROPERTY(QVariantList, actions, + { + vmap({ + { u"name"_s, u"Calculator"_s }, + { u"icon"_s, u"calculate"_s }, + { u"description"_s, u"Do simple math equations (powered by Qalc)"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"calc"_s } }, + }), + vmap({ + { u"name"_s, u"Scheme"_s }, + { u"icon"_s, u"palette"_s }, + { u"description"_s, u"Change the current colour scheme"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"scheme"_s } }, + }), + vmap({ + { u"name"_s, u"Wallpaper"_s }, + { u"icon"_s, u"image"_s }, + { u"description"_s, u"Change the current wallpaper"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"wallpaper"_s } }, + }), + vmap({ + { u"name"_s, u"Variant"_s }, + { u"icon"_s, u"colors"_s }, + { u"description"_s, u"Change the current scheme variant"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"variant"_s } }, + }), + vmap({ + { u"name"_s, u"Random"_s }, + { u"icon"_s, u"casino"_s }, + { u"description"_s, u"Switch to a random wallpaper"_s }, + { u"command"_s, QStringList{ u"caelestia"_s, u"wallpaper"_s, u"-r"_s } }, + }), + vmap({ + { u"name"_s, u"Light"_s }, + { u"icon"_s, u"light_mode"_s }, + { u"description"_s, u"Change the scheme to light mode"_s }, + { u"command"_s, QStringList{ u"setMode"_s, u"light"_s } }, + }), + vmap({ + { u"name"_s, u"Dark"_s }, + { u"icon"_s, u"dark_mode"_s }, + { u"description"_s, u"Change the scheme to dark mode"_s }, + { u"command"_s, QStringList{ u"setMode"_s, u"dark"_s } }, + }), + vmap({ + { u"name"_s, u"Shutdown"_s }, + { u"icon"_s, u"power_settings_new"_s }, + { u"description"_s, u"Shutdown the system"_s }, + { u"command"_s, QStringList{ u"systemctl"_s, u"poweroff"_s } }, + { u"dangerous"_s, true }, + }), + vmap({ + { u"name"_s, u"Reboot"_s }, + { u"icon"_s, u"cached"_s }, + { u"description"_s, u"Reboot the system"_s }, + { u"command"_s, QStringList{ u"systemctl"_s, u"reboot"_s } }, + { u"dangerous"_s, true }, + }), + vmap({ + { u"name"_s, u"Logout"_s }, + { u"icon"_s, u"exit_to_app"_s }, + { u"description"_s, u"Log out of the current session"_s }, + { u"command"_s, QStringList{ u"loginctl"_s, u"terminate-user"_s, u""_s } }, + { u"dangerous"_s, true }, + }), + vmap({ + { u"name"_s, u"Lock"_s }, + { u"icon"_s, u"lock"_s }, + { u"description"_s, u"Lock the current session"_s }, + { u"command"_s, QStringList{ u"loginctl"_s, u"lock-session"_s } }, + }), + vmap({ + { u"name"_s, u"Sleep"_s }, + { u"icon"_s, u"bedtime"_s }, + { u"description"_s, u"Suspend then hibernate"_s }, + { u"command"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, + }), + vmap({ + { u"name"_s, u"Settings"_s }, + { u"icon"_s, u"settings"_s }, + { u"description"_s, u"Configure the shell"_s }, + { u"command"_s, QStringList{ u"caelestia"_s, u"shell"_s, u"controlCenter"_s, u"open"_s } }, + }), + }) public: explicit LauncherConfig(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp index 439092af6..b97c69c1e 100644 --- a/plugin/src/Caelestia/Config/serviceconfig.hpp +++ b/plugin/src/Caelestia/Config/serviceconfig.hpp @@ -7,24 +7,33 @@ namespace caelestia::config { +using Qt::StringLiterals::operator""_s; + class ServiceConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS CONFIG_GLOBAL_PROPERTY(QString, weatherLocation) - CONFIG_GLOBAL_PROPERTY(bool, useFahrenheit, false) + // Guess based on locale + CONFIG_GLOBAL_PROPERTY(bool, useFahrenheit, + QLocale().measurementSystem() == QLocale::ImperialUSSystem || + QLocale().measurementSystem() == QLocale::ImperialUKSystem) + // This is always false by default cause apparently even imperial system users don't use it for perf temps? CONFIG_GLOBAL_PROPERTY(bool, useFahrenheitPerformance, false) - CONFIG_GLOBAL_PROPERTY(bool, useTwelveHourClock, false) + // Attempt to guess based on locale + CONFIG_GLOBAL_PROPERTY( + bool, useTwelveHourClock, QLocale().timeFormat(QLocale::ShortFormat).toLower().contains(u"a"_s)) CONFIG_GLOBAL_PROPERTY(QString, gpuType) CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 45) CONFIG_GLOBAL_PROPERTY(qreal, audioIncrement, 0.1) CONFIG_GLOBAL_PROPERTY(qreal, brightnessIncrement, 0.1) CONFIG_GLOBAL_PROPERTY(qreal, maxVolume, 1.0) CONFIG_GLOBAL_PROPERTY(bool, smartScheme, true) - CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, QStringLiteral("Spotify")) - CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases) + CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, u"Spotify"_s) + CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases, + { vmap({ { u"from"_s, u"com.github.th_ch.youtube_music"_s }, { u"to"_s, u"YT Music"_s } }) }) CONFIG_GLOBAL_PROPERTY(bool, showLyrics, false) - CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, QStringLiteral("Auto")) + CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, u"Auto"_s) public: explicit ServiceConfig(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/sessionconfig.hpp b/plugin/src/Caelestia/Config/sessionconfig.hpp index 629b3b6e0..7e7548f1b 100644 --- a/plugin/src/Caelestia/Config/sessionconfig.hpp +++ b/plugin/src/Caelestia/Config/sessionconfig.hpp @@ -7,14 +7,16 @@ namespace caelestia::config { +using Qt::StringLiterals::operator""_s; + class SessionIcons : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QString, logout, QStringLiteral("logout")) - CONFIG_PROPERTY(QString, shutdown, QStringLiteral("power_settings_new")) - CONFIG_PROPERTY(QString, hibernate, QStringLiteral("downloading")) - CONFIG_PROPERTY(QString, reboot, QStringLiteral("cached")) + CONFIG_PROPERTY(QString, logout, u"logout"_s) + CONFIG_PROPERTY(QString, shutdown, u"power_settings_new"_s) + CONFIG_PROPERTY(QString, hibernate, u"downloading"_s) + CONFIG_PROPERTY(QString, reboot, u"cached"_s) public: explicit SessionIcons(QObject* parent = nullptr) @@ -25,10 +27,10 @@ class SessionCommands : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QStringList, logout, { QStringLiteral("loginctl"), QStringLiteral("terminate-user"), QString() }) - CONFIG_PROPERTY(QStringList, shutdown, { QStringLiteral("systemctl"), QStringLiteral("poweroff") }) - CONFIG_PROPERTY(QStringList, hibernate, { QStringLiteral("systemctl"), QStringLiteral("hibernate") }) - CONFIG_PROPERTY(QStringList, reboot, { QStringLiteral("systemctl"), QStringLiteral("reboot") }) + CONFIG_PROPERTY(QStringList, logout, { u"loginctl"_s, u"terminate-user"_s, u""_s }) + CONFIG_PROPERTY(QStringList, shutdown, { u"systemctl"_s, u"poweroff"_s }) + CONFIG_PROPERTY(QStringList, hibernate, { u"systemctl"_s, u"hibernate"_s }) + CONFIG_PROPERTY(QStringList, reboot, { u"systemctl"_s, u"reboot"_s }) public: explicit SessionCommands(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/userpaths.hpp b/plugin/src/Caelestia/Config/userpaths.hpp index f0d0fb2e8..da6973a78 100644 --- a/plugin/src/Caelestia/Config/userpaths.hpp +++ b/plugin/src/Caelestia/Config/userpaths.hpp @@ -8,17 +8,19 @@ namespace caelestia::config { +using Qt::StringLiterals::operator""_s; + class UserPaths : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_GLOBAL_PROPERTY(QString, wallpaperDir, - QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + QStringLiteral("/Wallpapers")) - CONFIG_GLOBAL_PROPERTY(QString, lyricsDir, QDir::homePath() + QStringLiteral("/Music/lyrics/")) - CONFIG_PROPERTY(QString, sessionGif, QStringLiteral("root:/assets/kurukuru.gif")) - CONFIG_PROPERTY(QString, mediaGif, QStringLiteral("root:/assets/bongocat.gif")) - CONFIG_PROPERTY(QString, noNotifsPic, QStringLiteral("root:/assets/dino.png")) - CONFIG_PROPERTY(QString, lockNoNotifsPic, QStringLiteral("root:/assets/dino.png")) + CONFIG_GLOBAL_PROPERTY( + QString, wallpaperDir, QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + u"/Wallpapers"_s) + CONFIG_GLOBAL_PROPERTY(QString, lyricsDir, QDir::homePath() + u"/Music/lyrics/"_s) + CONFIG_PROPERTY(QString, sessionGif, u"root:/assets/kurukuru.gif"_s) + CONFIG_PROPERTY(QString, mediaGif, u"root:/assets/bongocat.gif"_s) + CONFIG_PROPERTY(QString, noNotifsPic, u"root:/assets/dino.png"_s) + CONFIG_PROPERTY(QString, lockNoNotifsPic, u"root:/assets/dino.png"_s) public: explicit UserPaths(QObject* parent = nullptr) diff --git a/plugin/src/Caelestia/Config/utilitiesconfig.hpp b/plugin/src/Caelestia/Config/utilitiesconfig.hpp index cabb877e4..3e5625bb0 100644 --- a/plugin/src/Caelestia/Config/utilitiesconfig.hpp +++ b/plugin/src/Caelestia/Config/utilitiesconfig.hpp @@ -7,11 +7,13 @@ namespace caelestia::config { +using Qt::StringLiterals::operator""_s; + class UtilitiesToasts : public ConfigObject { Q_OBJECT QML_ANONYMOUS - CONFIG_PROPERTY(QString, fullscreen, QStringLiteral("off")) + CONFIG_PROPERTY(QString, fullscreen, u"off"_s) CONFIG_GLOBAL_PROPERTY(bool, configLoaded, true) CONFIG_GLOBAL_PROPERTY(bool, chargingChanged, true) CONFIG_GLOBAL_PROPERTY(bool, gameModeChanged, true) @@ -50,7 +52,16 @@ class UtilitiesConfig : public ConfigObject { CONFIG_PROPERTY(int, maxToasts, 4) CONFIG_SUBOBJECT(UtilitiesToasts, toasts) CONFIG_SUBOBJECT(UtilitiesVpn, vpn) - CONFIG_PROPERTY(QVariantList, quickToggles) + CONFIG_PROPERTY(QVariantList, quickToggles, + { + vmap({ { u"id"_s, u"wifi"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"bluetooth"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"mic"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"settings"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"gameMode"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"dnd"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"vpn"_s }, { u"enabled"_s, false } }), + }) public: explicit UtilitiesConfig(QObject* parent = nullptr) From 982bb9759af532cb9a73fc5e29f981c4116fed30 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:25:24 +1000 Subject: [PATCH 342/409] fix: EVEN MORE non global access Also Tokens.sizes.notifs.image -> global --- modules/launcher/services/Actions.qml | 2 +- modules/lock/NotifGroup.qml | 14 +++++++------- modules/notifications/Content.qml | 2 +- modules/notifications/Notification.qml | 22 +++++++++++----------- modules/sidebar/NotifGroup.qml | 16 ++++++++-------- plugin/src/Caelestia/Config/tokens.hpp | 2 +- services/NotifData.qml | 6 +++--- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 4ec249da2..634b3e6ad 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -20,7 +20,7 @@ Searcher { Variants { id: variants - model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (GlobalConfig.launcher.enableDangerousActions || !(a.dangerous ?? false))) + model: GlobalConfig.launcher.actions.filter(a => (a.enabled ?? true) && (GlobalConfig.launcher.enableDangerousActions || !(a.dangerous ?? false))) Action {} } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 50ea168d2..14afc8bdd 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -64,8 +64,8 @@ StyledRect { Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Tokens.sizes.notifs.image - implicitHeight: Tokens.sizes.notifs.image + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image Component { id: imageComp @@ -73,12 +73,12 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: Tokens.sizes.notifs.image - sourceSize.height: Tokens.sizes.notifs.image + sourceSize.width: TokenConfig.sizes.notifs.image + sourceSize.height: TokenConfig.sizes.notifs.image cache: false asynchronous: true - width: Tokens.sizes.notifs.image - height: Tokens.sizes.notifs.image + width: TokenConfig.sizes.notifs.image + height: TokenConfig.sizes.notifs.image } } @@ -86,7 +86,7 @@ StyledRect { id: appIconComp ColouredIcon { - implicitSize: Math.round(Tokens.sizes.notifs.image * 0.6) + implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index fcbf41f2f..aeaa7deaf 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -170,7 +170,7 @@ Item { Anim { target: notif property: "x" - to: (notif.x >= 0 ? Tokens.sizes.notifs.width : -Tokens.sizes.notifs.width) * 2 + to: (notif.x >= 0 ? wrapper.Tokens.sizes.notifs.width : -wrapper.Tokens.sizes.notifs.width) * 2 duration: Tokens.anim.durations.normal easing: Tokens.anim.emphasized } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 55dea458b..73c7c2879 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -114,21 +114,21 @@ StyledRect { anchors.left: parent.left anchors.top: parent.top - width: Tokens.sizes.notifs.image - height: Tokens.sizes.notifs.image + width: TokenConfig.sizes.notifs.image + height: TokenConfig.sizes.notifs.image visible: root.hasImage || root.hasAppIcon sourceComponent: ClippingRectangle { radius: Tokens.rounding.full - implicitWidth: Tokens.sizes.notifs.image - implicitHeight: Tokens.sizes.notifs.image + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image Image { anchors.fill: parent source: Qt.resolvedUrl(root.modelData.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: Tokens.sizes.notifs.image - sourceSize.height: Tokens.sizes.notifs.image + sourceSize.width: TokenConfig.sizes.notifs.image + sourceSize.height: TokenConfig.sizes.notifs.image cache: false asynchronous: true } @@ -149,8 +149,8 @@ StyledRect { sourceComponent: StyledRect { radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer - implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : Tokens.sizes.notifs.image - implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : Tokens.sizes.notifs.image + implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image + implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image Loader { id: icon @@ -251,7 +251,7 @@ StyledRect { font.family: appName.font.family font.pointSize: appName.font.pointSize elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3 + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } StyledText { @@ -303,7 +303,7 @@ StyledRect { font.family: summary.font.family font.pointSize: summary.font.pointSize elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Tokens.spacing.small * 3 + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } StyledText { @@ -516,7 +516,7 @@ StyledRect { elide: Text.ElideRight elideWidth: { const numActions = root.modelData.actions.length + 1; - return (inner.width - actions.spacing * (numActions - 1)) / numActions - Tokens.padding.normal * 2; + return (inner.width - actions.spacing * (numActions - 1)) / numActions - root.Tokens.padding.normal * 2; } } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index dee6f20b6..8c9429bb0 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -53,7 +53,7 @@ StyledRect { readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); const columnHeight = headerHeight + notifList.layoutHeight + column.Layout.topMargin + column.Layout.bottomMargin; - return Math.round(Math.max(Tokens.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); + return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) @@ -91,8 +91,8 @@ StyledRect { Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Tokens.sizes.notifs.image - implicitHeight: Tokens.sizes.notifs.image + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image Component { id: imageComp @@ -100,12 +100,12 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: Tokens.sizes.notifs.image - sourceSize.height: Tokens.sizes.notifs.image + sourceSize.width: TokenConfig.sizes.notifs.image + sourceSize.height: TokenConfig.sizes.notifs.image cache: false asynchronous: true - width: Tokens.sizes.notifs.image - height: Tokens.sizes.notifs.image + width: TokenConfig.sizes.notifs.image + height: TokenConfig.sizes.notifs.image } } @@ -113,7 +113,7 @@ StyledRect { id: appIconComp ColouredIcon { - implicitSize: Math.round(Tokens.sizes.notifs.image * 0.6) + implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index de0a7c263..5ff1ce5e3 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -194,7 +194,7 @@ class NotifsTokens : public ConfigObject { QML_ANONYMOUS CONFIG_PROPERTY(int, width, 400) - CONFIG_PROPERTY(int, image, 41) + CONFIG_GLOBAL_PROPERTY(int, image, 41) CONFIG_PROPERTY(int, badge, 20) public: diff --git a/services/NotifData.qml b/services/NotifData.qml index c92a3ad78..6f470038a 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -54,14 +54,14 @@ QtObject { // qmllint disable uncreatable-type PanelWindow { // qmllint enable uncreatable-type - implicitWidth: Tokens.sizes.notifs.image - implicitHeight: Tokens.sizes.notifs.image + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image color: "transparent" mask: Region {} Image { function tryCache(): void { - if (status !== Image.Ready || width != Tokens.sizes.notifs.image || height != Tokens.sizes.notifs.image) + if (status !== Image.Ready || width != TokenConfig.sizes.notifs.image || height != TokenConfig.sizes.notifs.image) return; const cacheKey = notif.appName + notif.summary + notif.id; From 876517081de089bfadbeb36320a5a9be79937e05 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:53:03 +1000 Subject: [PATCH 343/409] chore: update readme global opts --- README.md | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index bd3303953..b7ead9bc1 100644 --- a/README.md +++ b/README.md @@ -230,13 +230,25 @@ For example, to disable the bar on DP-1: > Not all options are respect per-monitor overrides. Most notably, the following options will only read > from the global config, and ignore the respective option in per-monitor config files. > -> - All animation configs are not per-monitor -> - `general` (logo, terminal app, idle settings) -> - `services` (weather, audio, GPU, player settings, etc.) -> - `paths` (wallpaper dir, lyrics dir, etc.) -> - `notifs` (notification behavior) -> - `utilities` (individual toast enabled status, vpn configs) -> - `launcher` (actions, favourite apps, hidden apps) +>
Ignored options +> +> - `appearance` (`anim`, `transparency`) +> - `general` (`logo`, `apps`, `idle`, `battery`) +> - `bar.workspaces` (`perMonitorWorkspaces`, `specialWorkspaceIcons`, `windowIcons`) +> - `bar.tray` (`iconSubs`, `hiddenIcons`) +> - `dashboard` (`mediaUpdateInterval`, `resourceUpdateInterval`) +> - `launcher` (`specialPrefix`, `actionPrefix`, `enableDangerousActions`, `vimKeybinds`, +> `favouriteApps`, `hiddenApps`, `actions`) +> - `launcher.useFuzzy` (`apps`, `actions`, `schemes`, `variants`, `wallpapers`) +> - `notifs` (`expire`, `fullscreen`, `defaultExpireTimeout`, `actionOnClick`) +> - `lock` (`enableFprint`, `maxFprintTries`) +> - `utilities` (`toasts`, `vpn`) +> - `services` (`weatherLocation`, `useFahrenheit`, `useFahrenheitPerformance`, `useTwelveHourClock`, +> `gpuType`, `visualiserBars`, `audioIncrement`, `brightnessIncrement`, `maxVolume`, `smartScheme`, +> `defaultPlayer`, `playerAliases`, `showLyrics`, `lyricsBackend`) +> - `paths` (`wallpaperDir`, `lyricsDir`) +> +>
### Example configuration From 455c4c59a6ace33012e305fe8016ee8da03fc931 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 18:58:24 +1000 Subject: [PATCH 344/409] chore: remove unnecessary id --- components/containers/StyledWindow.qml | 2 -- 1 file changed, 2 deletions(-) diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml index 8ea3c2273..72bbed6f1 100644 --- a/components/containers/StyledWindow.qml +++ b/components/containers/StyledWindow.qml @@ -5,8 +5,6 @@ import Caelestia.Config // qmllint disable uncreatable-type PanelWindow { // qmllint enable uncreatable-type - id: root - required property string name WlrLayershell.namespace: `caelestia-${name}` From b1401c1c8c48a70317ae082e3fd9b519a4fedf57 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:09:43 +1000 Subject: [PATCH 345/409] feat: border rounding only affect outer corners --- modules/drawers/ContentWindow.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 328d13ea0..0824d209a 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -301,7 +301,7 @@ StyledWindow { y: panel.y + root.borderThickness implicitWidth: panel.width implicitHeight: panel.height - radius: Config.border.rounding + radius: Tokens.rounding.large deformScale: deformAmount / 10000 } } From 50c4eaf75bbb7dd6aec90531572d77e652e3d4be Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 22:30:49 +1000 Subject: [PATCH 346/409] feat: add border smoothing option --- README.md | 1 + modules/drawers/ContentWindow.qml | 1 + plugin/src/Caelestia/Config/borderconfig.hpp | 1 + 3 files changed, 3 insertions(+) diff --git a/README.md b/README.md index b7ead9bc1..f5849cb8d 100644 --- a/README.md +++ b/README.md @@ -483,6 +483,7 @@ For example, to disable the bar on DP-1: }, "border": { "rounding": 25, + "smoothing": 32, "thickness": 10 }, "dashboard": { diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 0824d209a..b69408dc0 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -131,6 +131,7 @@ StyledWindow { id: blobGroup color: Colours.palette.m3surface + smoothing: root.contentItem.Config.border.smoothing Behavior on color { CAnim {} diff --git a/plugin/src/Caelestia/Config/borderconfig.hpp b/plugin/src/Caelestia/Config/borderconfig.hpp index abdd07d94..9de604e82 100644 --- a/plugin/src/Caelestia/Config/borderconfig.hpp +++ b/plugin/src/Caelestia/Config/borderconfig.hpp @@ -12,6 +12,7 @@ class BorderConfig : public ConfigObject { CONFIG_PROPERTY(int, thickness, 10) CONFIG_PROPERTY(int, rounding, 25) + CONFIG_PROPERTY(int, smoothing, 32) Q_PROPERTY(int minThickness READ minThickness CONSTANT) Q_PROPERTY(int clampedThickness READ clampedThickness NOTIFY thicknessChanged) From c517b7102437d203d5782f7f714898f5f6d6a525 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 13 Apr 2026 23:03:36 +1000 Subject: [PATCH 347/409] fix: utilities recording modal bg --- modules/drawers/ContentWindow.qml | 1 + modules/utilities/Content.qml | 2 + modules/utilities/RecordingDeleteModal.qml | 52 +++++++++++++--------- modules/utilities/Wrapper.qml | 2 + 4 files changed, 35 insertions(+), 22 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index b69408dc0..b89907f7f 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -250,6 +250,7 @@ StyledWindow { borderThickness: root.borderThickness utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 + utilities.deformMatrix: utilsBg.rawDeformMatrix dashboard.transform: Matrix4x4 { matrix: dashBg.deformMatrix diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 5ec2d33a7..e03c546fd 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -11,6 +11,7 @@ Item { required property var props required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts + required property matrix4x4 deformMatrix implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight @@ -37,5 +38,6 @@ Item { RecordingDeleteModal { props: root.props + deformMatrix: root.deformMatrix } } diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 2b7e14965..8eda4607f 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -14,6 +14,7 @@ Loader { id: root required property var props + required property matrix4x4 deformMatrix asynchronous: true anchors.fill: parent @@ -40,7 +41,9 @@ Loader { StyledRect { anchors.fill: parent - topLeftRadius: Config.border.rounding + anchors.rightMargin: -parent.width * (1 - root.deformMatrix.m11) / 2 // Additional bit to account for deform + anchors.bottomMargin: -parent.height * 0.1 // Additional bit to account for overshoot + topLeftRadius: Tokens.rounding.large color: Colours.palette.m3scrim } @@ -51,13 +54,14 @@ Loader { preferredRendererType: Shape.CurveRenderer asynchronous: true + // Bottom left ShapePath { - startX: -Config.border.rounding * 2 - startY: shape.height - Config.border.thickness + startX: -root.Config.border.smoothing * 2 + startY: shape.height - root.Config.border.thickness strokeWidth: 0 fillGradient: LinearGradient { orientation: LinearGradient.Horizontal - x1: -Config.border.rounding * 2 + x1: -root.Config.border.smoothing * 2 GradientStop { position: 0 @@ -70,31 +74,34 @@ Loader { } PathLine { - relativeX: Config.border.rounding + relativeX: root.Config.border.smoothing relativeY: 0 } - PathArc { - relativeY: -Config.border.rounding - radiusX: Config.border.rounding - radiusY: Config.border.rounding - direction: PathArc.Counterclockwise + PathCubic { + relativeX: root.Config.border.smoothing + relativeY: -root.Config.border.smoothing + relativeControl1X: root.Config.border.smoothing * 0.93 + relativeControl1Y: -root.Config.border.smoothing * 0.07 + relativeControl2X: root.Config.border.smoothing * 0.93 + relativeControl2Y: -root.Config.border.smoothing * 0.07 } PathLine { relativeX: 0 - relativeY: Config.border.rounding + Config.border.thickness + relativeY: root.Config.border.smoothing + root.Config.border.thickness } PathLine { - relativeX: -Config.border.rounding * 2 + relativeX: -root.Config.border.smoothing * 2 relativeY: 0 } } + // Top right curve ShapePath { - startX: shape.width - Config.border.rounding - Config.border.thickness + startX: shape.width - root.Config.border.smoothing - root.Config.border.thickness + (1 - root.deformMatrix.m11) * shape.width / 2 strokeWidth: 0 fillGradient: LinearGradient { orientation: LinearGradient.Vertical - y1: -Config.border.rounding * 2 + y1: -root.Config.border.smoothing * 2 GradientStop { position: 0 @@ -106,19 +113,20 @@ Loader { } } - PathArc { - relativeX: Config.border.rounding - relativeY: -Config.border.rounding - radiusX: Config.border.rounding - radiusY: Config.border.rounding - direction: PathArc.Counterclockwise + PathCubic { + relativeX: root.Config.border.smoothing + relativeY: -root.Config.border.smoothing + relativeControl1X: root.Config.border.smoothing * 0.93 + relativeControl1Y: -root.Config.border.smoothing * 0.07 + relativeControl2X: root.Config.border.smoothing * 0.93 + relativeControl2Y: -root.Config.border.smoothing * 0.07 } PathLine { relativeX: 0 - relativeY: -Config.border.rounding + relativeY: -root.Config.border.smoothing } PathLine { - relativeX: Config.border.thickness + relativeX: root.Config.border.thickness relativeY: 0 } PathLine { diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index a87f8a590..137e817b9 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -14,6 +14,7 @@ Item { required property Sidebar.Wrapper sidebar required property BarPopouts.Wrapper popouts property real horizontalStretch + property matrix4x4 deformMatrix readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false @@ -84,6 +85,7 @@ Item { props: root.props visibilities: root.visibilities popouts: root.popouts + deformMatrix: root.deformMatrix } } } From 90528c513f409296f26deb54a9371529e6a5dfb7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:18:20 +1000 Subject: [PATCH 348/409] fix: remove config install from cmake --- CMakeLists.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f9d762bd7..b23f7b485 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,7 +60,7 @@ if("plugin" IN_LIST ENABLE_MODULES) endif() if("shell" IN_LIST ENABLE_MODULES) - foreach(dir assets components config modules services utils) + foreach(dir assets components modules services utils) install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") endforeach() From 7df53a05d0b3e7235b68c5a23cb2902ad503b499 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 15 Apr 2026 17:31:26 +1000 Subject: [PATCH 349/409] feat: add anim type shorthand + anchor anim --- components/AnchorAnim.qml | 51 +++++++++++++++++++++++++++++++++++++++ components/Anim.qml | 45 ++++++++++++++++++++++++++++++++-- 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 components/AnchorAnim.qml diff --git a/components/AnchorAnim.qml b/components/AnchorAnim.qml new file mode 100644 index 000000000..dd62dc366 --- /dev/null +++ b/components/AnchorAnim.qml @@ -0,0 +1,51 @@ +import QtQuick +import Caelestia.Config + +AnchorAnimation { + enum Type { + StandardSmall = 0, + Standard, + StandardLarge, + StandardExtraLarge, + EmphasizedSmall, + Emphasized, + EmphasizedLarge, + EmphasizedExtraLarge, + FastSpatial, + DefaultSpatial, + SlowSpatial + } + + property int type: AnchorAnim.DefaultSpatial + + duration: { + if (type < AnchorAnim.StandardSmall || type > AnchorAnim.SlowSpatial) + return Tokens.anim.durations.expressiveDefaultSpatial; + + if (type == AnchorAnim.FastSpatial) + return Tokens.anim.durations.expressiveFastSpatial; + if (type == AnchorAnim.DefaultSpatial) + return Tokens.anim.durations.expressiveDefaultSpatial; + if (type == AnchorAnim.SlowSpatial) + return Tokens.anim.durations.expressiveSlowSpatial; + + const types = ["small", "normal", "large", "extraLarge"]; + const idx = type % 4; // 0-7 are the 4 standard types + return Tokens.anim.durations[types[idx]]; + } + easing: { + if (type == AnchorAnim.FastSpatial) + return Tokens.anim.expressiveFastSpatial; + if (type == AnchorAnim.DefaultSpatial) + return Tokens.anim.expressiveDefaultSpatial; + if (type == AnchorAnim.SlowSpatial) + return Tokens.anim.expressiveSlowSpatial; + + if (type >= AnchorAnim.StandardSmall && type <= AnchorAnim.StandardExtraLarge) + return Tokens.anim.standard; + if (type >= AnchorAnim.EmphasizedSmall && type <= AnchorAnim.EmphasizedExtraLarge) + return Tokens.anim.emphasized; + + return Tokens.anim.expressiveDefaultSpatial; + } +} diff --git a/components/Anim.qml b/components/Anim.qml index 69484591e..8bc4468ac 100644 --- a/components/Anim.qml +++ b/components/Anim.qml @@ -2,6 +2,47 @@ import QtQuick import Caelestia.Config NumberAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + enum Type { + StandardSmall = 0, + Standard, + StandardLarge, + StandardExtraLarge, + EmphasizedSmall, + Emphasized, + EmphasizedLarge, + EmphasizedExtraLarge, + FastSpatial, + DefaultSpatial, + SlowSpatial + } + + property int type: Anim.Standard + + duration: { + if (type < Anim.StandardSmall || type > Anim.SlowSpatial) + return Tokens.anim.durations.normal; + + if (type == Anim.FastSpatial) + return Tokens.anim.durations.expressiveFastSpatial; + if (type == Anim.DefaultSpatial) + return Tokens.anim.durations.expressiveDefaultSpatial; + if (type == Anim.SlowSpatial) + return Tokens.anim.durations.expressiveSlowSpatial; + + const types = ["small", "normal", "large", "extraLarge"]; + const idx = type % 4; // 0-7 are the 4 standard types + return Tokens.anim.durations[types[idx]]; + } + easing: { + if (type == Anim.FastSpatial) + return Tokens.anim.expressiveFastSpatial; + if (type == Anim.DefaultSpatial) + return Tokens.anim.expressiveDefaultSpatial; + if (type == Anim.SlowSpatial) + return Tokens.anim.expressiveSlowSpatial; + + if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge) + return Tokens.anim.emphasized; + return Tokens.anim.standard; + } } From d675478e918106b3278c9b1a219fc092ac5ee21e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:21:09 +1000 Subject: [PATCH 350/409] refactor: use new anim type prop --- components/controls/CollapsibleSection.qml | 15 +++------- components/controls/FilledSlider.qml | 2 +- components/controls/Menu.qml | 3 +- components/controls/SplitButton.qml | 2 +- components/controls/StyledTextField.qml | 2 +- components/controls/ToggleButton.qml | 6 ++-- components/controls/Tooltip.qml | 6 ++-- components/filedialog/FolderContents.qml | 6 ++-- components/widgets/ExtraIndicator.qml | 3 +- modules/areapicker/Picker.qml | 7 ++--- modules/background/DesktopClock.qml | 5 ++-- modules/bar/BarWrapper.qml | 5 ++-- modules/bar/components/ActiveWindow.qml | 3 +- modules/bar/components/Tray.qml | 3 +- .../components/workspaces/ActiveIndicator.qml | 2 +- .../workspaces/SpecialWorkspaces.qml | 8 ++--- .../bar/components/workspaces/Workspaces.qml | 2 +- modules/bar/popouts/ClipWrapper.qml | 3 +- modules/bar/popouts/Content.qml | 2 +- modules/bar/popouts/WirelessPassword.qml | 3 +- modules/controlcenter/NavRail.qml | 13 ++++---- modules/controlcenter/bluetooth/Details.qml | 18 +++++------ modules/controlcenter/bluetooth/Settings.qml | 12 +++----- .../components/ConnectedButtonGroup.qml | 6 ++-- .../controlcenter/launcher/LauncherPane.qml | 4 +-- modules/controlcenter/network/VpnDetails.qml | 12 +++----- modules/controlcenter/network/VpnList.qml | 30 +++++++------------ .../network/WirelessPasswordDialog.qml | 3 +- modules/dashboard/Content.qml | 6 ++-- modules/dashboard/LyricsView.qml | 2 +- modules/dashboard/Media.qml | 5 ++-- modules/dashboard/Performance.qml | 14 ++++----- modules/dashboard/Tabs.qml | 2 -- modules/dashboard/Wrapper.qml | 3 +- modules/dashboard/dash/Calendar.qml | 6 ++-- modules/dashboard/dash/Media.qml | 2 +- modules/dashboard/dash/Resources.qml | 2 +- modules/dashboard/dash/User.qml | 3 +- modules/drawers/ContentWindow.qml | 12 +++----- modules/launcher/AppList.qml | 5 ++-- modules/launcher/Content.qml | 4 +-- modules/launcher/ContentList.qml | 4 +-- modules/launcher/Wrapper.qml | 3 +- modules/launcher/items/CalcItem.qml | 2 +- modules/lock/Center.qml | 4 +-- modules/lock/InputField.qml | 3 +- modules/lock/LockSurface.qml | 26 +++++++--------- modules/lock/Media.qml | 8 ++--- modules/lock/NotifDock.qml | 11 +++---- modules/lock/NotifGroup.qml | 3 +- modules/lock/Resources.qml | 2 +- modules/notifications/Notification.qml | 3 +- modules/osd/Content.qml | 2 +- modules/osd/Wrapper.qml | 3 +- modules/session/Wrapper.qml | 3 +- modules/sidebar/Notif.qml | 3 +- modules/sidebar/NotifActionList.qml | 6 ++-- modules/sidebar/NotifDock.qml | 5 ++-- modules/sidebar/NotifDockList.qml | 12 +++----- modules/sidebar/NotifGroup.qml | 6 ++-- modules/sidebar/NotifGroupList.qml | 6 ++-- modules/sidebar/Wrapper.qml | 3 +- modules/utilities/RecordingDeleteModal.qml | 3 +- modules/utilities/Wrapper.qml | 3 +- modules/utilities/cards/IdleInhibit.qml | 8 ++--- modules/utilities/cards/RecordingList.qml | 3 +- modules/utilities/cards/Toggles.qml | 3 +- modules/utilities/toasts/Toasts.qml | 6 ++-- 68 files changed, 147 insertions(+), 249 deletions(-) diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 75a6b9aa1..52aa73c42 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -55,8 +55,7 @@ ColumnLayout { Behavior on rotation { Anim { - duration: Tokens.anim.durations.small - easing: Tokens.anim.standard + type: Anim.StandardSmall } } } @@ -83,9 +82,7 @@ ColumnLayout { clip: true Behavior on Layout.preferredHeight { - Anim { - easing: Tokens.anim.standard - } + Anim {} } StyledRect { @@ -98,9 +95,7 @@ ColumnLayout { visible: root.showBackground Behavior on opacity { - Anim { - easing: Tokens.anim.standard - } + Anim {} } } @@ -117,9 +112,7 @@ ColumnLayout { opacity: root.expanded ? 1.0 : 0.0 Behavior on opacity { - Anim { - easing: Tokens.anim.standard - } + Anim {} } StyledText { diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index e8593b96d..794f707a6 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -140,7 +140,7 @@ Slider { Behavior on value { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 3f4878e6b..8dcf0ad67 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -108,8 +108,7 @@ Elevation { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index ba75a6781..d3e36e92b 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -86,7 +86,7 @@ Row { Behavior on Layout.preferredWidth { Anim { - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } } diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 7eec78b84..0d193e116 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -61,7 +61,7 @@ TextField { Behavior on opacity { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 0ece6d4ef..39abe4454 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -87,15 +87,13 @@ StyledRect { Behavior on radius { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on Layout.preferredWidth { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index d00c144ba..00252be3e 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -100,8 +100,7 @@ Popup { property: "opacity" from: 0 to: 1 - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } @@ -110,8 +109,7 @@ Popup { property: "opacity" from: 1 to: 0 - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index b5f34a487..c5e19cd2c 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -120,8 +120,7 @@ Item { properties: "opacity,scale" from: 0 to: 1 - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -144,8 +143,7 @@ Item { } Anim { properties: "x,y" - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index 1fa34837a..9c7a1ced9 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -44,8 +44,7 @@ StyledRect { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 30c47afdc..bb71e5fb3 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -167,7 +167,7 @@ MouseArea { target: root property: "opacity" to: 0 - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } ExAnim { target: root @@ -278,7 +278,7 @@ MouseArea { Behavior on opacity { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } @@ -307,7 +307,6 @@ MouseArea { } component ExAnim: Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index be6dff610..c7415be8b 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -158,14 +158,13 @@ Item { Behavior on clockScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on implicitWidth { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 7c8c5973d..4fde197a0 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -57,8 +57,7 @@ Item { Anim { target: root property: "implicitWidth" - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } }, Transition { @@ -68,7 +67,7 @@ Item { Anim { target: root property: "implicitWidth" - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } ] diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index b4bd3bba8..f39aa4c39 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -101,8 +101,7 @@ Item { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 80d5d0091..85a920c13 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -116,8 +116,7 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index 19d6c7834..ebc0caf26 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -95,6 +95,6 @@ StyledRect { } component EAnim: Anim { - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index f45191f61..3ed382e90 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -127,12 +127,12 @@ Item { Anim { property: "scale" to: 0.5 - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } Anim { property: "opacity" to: 0 - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } @@ -192,13 +192,13 @@ Item { Behavior on y { Anim { - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } Behavior on implicitHeight { Anim { - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } } diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index 698312b24..2030bf80e 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -140,7 +140,7 @@ StyledClippingRect { Behavior on blur { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index 30fd4aaa1..04a437f66 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -35,8 +35,7 @@ Item { Behavior on offsetScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index f1c2456ba..42c5a7667 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -194,7 +194,7 @@ Item { SequentialAnimation { Anim { properties: "opacity,scale" - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } PropertyAction { target: popout diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 0c3bae708..909d79f27 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -454,8 +454,7 @@ ColumnLayout { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 4522573b3..420300d44 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -96,22 +96,20 @@ Item { Behavior on opacity { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } Behavior on implicitWidth { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } @@ -156,13 +154,12 @@ Item { transitions: Transition { Anim { property: "opacity" - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } Anim { properties: "implicitWidth,implicitHeight" - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index ec47e5f1c..09cc74c94 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -264,8 +264,7 @@ StyledFlickable { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } @@ -498,12 +497,11 @@ StyledFlickable { ParallelAnimation { Anim { property: "implicitWidth" - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "opacity" - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } @@ -518,12 +516,11 @@ StyledFlickable { ParallelAnimation { Anim { property: "implicitWidth" - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "opacity" - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } @@ -566,7 +563,7 @@ StyledFlickable { Behavior on Layout.preferredWidth { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } @@ -611,8 +608,7 @@ StyledFlickable { transitions: Transition { Anim { properties: "implicitWidth,implicitHeight" - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } Anim { properties: "radius,font.pointSize" diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index d5c9a28a7..c3f15672a 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -170,8 +170,7 @@ ColumnLayout { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } @@ -250,15 +249,13 @@ ColumnLayout { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } @@ -404,8 +401,7 @@ ColumnLayout { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml index 90f9614a8..f782838d6 100644 --- a/modules/controlcenter/components/ConnectedButtonGroup.qml +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -98,15 +98,13 @@ StyledRect { Behavior on Layout.preferredWidth { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on radius { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 70649bcea..754678733 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -269,13 +269,13 @@ Item { Behavior on width { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } Behavior on opacity { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index c146554f2..7ac2dc50f 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -274,15 +274,13 @@ DeviceDetails { property: "opacity" from: 0 to: 1 - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "scale" from: 0.7 to: 1 - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } @@ -291,15 +289,13 @@ DeviceDetails { property: "opacity" from: 1 to: 0 - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "scale" from: 1 to: 0.7 - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 23fdf5b13..1022117b6 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -378,15 +378,13 @@ ColumnLayout { property: "opacity" from: 0 to: 1 - duration: Tokens.anim.durations.normal - easing: Tokens.anim.emphasized + type: Anim.Emphasized } Anim { property: "scale" from: 0.7 to: 1 - duration: Tokens.anim.durations.normal - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } } @@ -397,15 +395,13 @@ ColumnLayout { property: "opacity" from: 1 to: 0 - duration: Tokens.anim.durations.small - easing: Tokens.anim.emphasized + type: Anim.EmphasizedSmall } Anim { property: "scale" from: 1 to: 0.7 - duration: Tokens.anim.durations.small - easing: Tokens.anim.emphasized + type: Anim.EmphasizedSmall } } } @@ -431,8 +427,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } } @@ -442,8 +437,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } @@ -457,8 +451,7 @@ ColumnLayout { Behavior on opacity { Anim { - duration: Tokens.anim.durations.small - easing: Tokens.anim.emphasized + type: Anim.EmphasizedSmall } } @@ -577,8 +570,7 @@ ColumnLayout { Behavior on opacity { Anim { - duration: Tokens.anim.durations.small - easing: Tokens.anim.emphasized + type: Anim.EmphasizedSmall } } @@ -815,8 +807,7 @@ ColumnLayout { target: selectionContent property: "opacity" to: 0 - duration: Tokens.anim.durations.small - easing: Tokens.anim.emphasized + type: Anim.EmphasizedSmall } } @@ -831,8 +822,7 @@ ColumnLayout { target: formContent property: "opacity" to: 1 - duration: Tokens.anim.durations.small - easing: Tokens.anim.emphasized + type: Anim.EmphasizedSmall } } } diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index f5b3afa97..5ff48f771 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -378,8 +378,7 @@ Item { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 771d4bb40..95d2a08a7 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -199,15 +199,13 @@ Item { Behavior on implicitWidth { Anim { - duration: Tokens.anim.durations.large - easing: Tokens.anim.emphasized + type: Anim.EmphasizedLarge } } Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.large - easing: Tokens.anim.emphasized + type: Anim.EmphasizedLarge } } } diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml index 6ccbd26ea..c0188304d 100644 --- a/modules/dashboard/LyricsView.qml +++ b/modules/dashboard/LyricsView.qml @@ -96,7 +96,7 @@ StyledListView { } Behavior on scale { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 277d12cc4..22ed204be 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -61,7 +61,7 @@ Item { Behavior on playerProgress { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } @@ -505,8 +505,7 @@ Item { Behavior on Layout.preferredWidth { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index cfcc78f71..c93499ca6 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -271,7 +271,7 @@ Item { Behavior on animatedPercentage { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } @@ -323,7 +323,7 @@ Item { Behavior on animatedValue { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } @@ -438,13 +438,13 @@ Item { Behavior on animatedUsage { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } Behavior on animatedTemp { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } @@ -512,7 +512,7 @@ Item { Behavior on animatedPercentage { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } @@ -639,7 +639,7 @@ Item { Behavior on animatedPercentage { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } @@ -710,7 +710,7 @@ Item { Behavior on smoothMax { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index fc5d7ec15..32e8c469e 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -170,8 +170,6 @@ Item { target: ripple property: "opacity" to: 0 - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard } } diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index dd097e7b6..f7f037426 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -40,8 +40,7 @@ Item { Behavior on offsetScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index 990e380d5..1c6bcadae 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -236,15 +236,13 @@ CustomMouseArea { Behavior on x { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on y { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index c83efde56..46e56b24a 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -20,7 +20,7 @@ Item { Behavior on playerProgress { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index df5603691..2e0f085b7 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -80,7 +80,7 @@ Row { Behavior on value { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index 791297fff..d862e411e 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -90,8 +90,7 @@ Row { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index b89907f7f..eb44ebbaf 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -73,22 +73,19 @@ StyledWindow { Behavior on borderThickness { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on borderRounding { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on shadowOpacity { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -219,8 +216,7 @@ StyledWindow { Behavior on extraWidth { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index ac5bea65b..a2109c040 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -42,8 +42,7 @@ StyledListView { Behavior on y { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } @@ -197,7 +196,7 @@ StyledListView { addDisplaced: Transition { Anim { property: "y" - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } Anim { properties: "opacity,scale" diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index 48a3d4faf..69be0c3ae 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -176,13 +176,13 @@ Item { Behavior on width { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } Behavior on opacity { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index 482d6c0d2..db7abdf92 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -60,7 +60,7 @@ Item { property: "opacity" from: 1 to: 0 - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } PropertyAction {} Anim { @@ -68,7 +68,7 @@ Item { property: "opacity" from: 0 to: 1 - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index e6cc8114e..d5630b233 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -41,8 +41,7 @@ Item { Behavior on offsetScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 534b6c10c..447795446 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -120,7 +120,7 @@ Item { Behavior on implicitWidth { Anim { - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } } diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 349f44d27..9d6fd2c53 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -397,13 +397,13 @@ ColumnLayout { target: message property: "scale" to: 0.7 - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } Anim { target: message property: "opacity" to: 0 - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index c1c1e31fe..1b6c34a73 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -134,8 +134,7 @@ Item { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index f08b528d9..322773fd8 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -36,8 +36,7 @@ WlSessionLockSurface { target: lockContent properties: "implicitWidth,implicitHeight" to: lockContent.size - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } Anim { target: lockBg @@ -48,26 +47,25 @@ WlSessionLockSurface { target: content property: "scale" to: 0 - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } Anim { target: content property: "opacity" to: 0 - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } Anim { target: lockIcon property: "opacity" to: 1 - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } Anim { target: background property: "opacity" to: 0 - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } SequentialAnimation { PauseAnimation { @@ -96,7 +94,7 @@ WlSessionLockSurface { target: background property: "opacity" to: 1 - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } SequentialAnimation { ParallelAnimation { @@ -104,8 +102,7 @@ WlSessionLockSurface { target: lockContent property: "scale" to: 1 - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } Anim { target: lockContent @@ -136,8 +133,7 @@ WlSessionLockSurface { target: content property: "scale" to: 1 - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } Anim { target: lockBg @@ -148,15 +144,13 @@ WlSessionLockSurface { target: lockContent property: "implicitWidth" to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } Anim { target: lockContent property: "implicitHeight" to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 169d06f4b..5882c3228 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -34,7 +34,7 @@ Item { Behavior on opacity { Anim { - duration: Tokens.anim.durations.extraLarge + type: Anim.StandardExtraLarge } } } @@ -193,15 +193,13 @@ Item { Behavior on Layout.preferredWidth { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on radius { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 070d4a33f..bf827579e 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -73,7 +73,7 @@ ColumnLayout { Behavior on opacity { Anim { - duration: Tokens.anim.durations.extraLarge + type: Anim.StandardExtraLarge } } } @@ -103,8 +103,7 @@ ColumnLayout { property: "scale" from: 0 to: 1 - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -126,8 +125,7 @@ ColumnLayout { } Anim { property: "y" - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -138,8 +136,7 @@ ColumnLayout { } Anim { property: "y" - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 14afc8bdd..b07b5193d 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -298,8 +298,7 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 5babda203..1f38aa00d 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -86,7 +86,7 @@ GridLayout { Behavior on value { Anim { - duration: Tokens.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 73c7c2879..2598ccf68 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -101,8 +101,7 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 7ea58ab73..3ad3ef886 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -117,7 +117,7 @@ Item { Behavior on Layout.preferredHeight { Anim { - easing: Tokens.anim.emphasized + type: Anim.Emphasized } } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 2dac52224..3ea0e1a36 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -46,8 +46,7 @@ Item { Behavior on offsetScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 081752d3e..41d9ba1ae 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -23,8 +23,7 @@ Item { Behavior on offsetScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 54dfb5ff3..2a390926d 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -124,8 +124,7 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index 843d6ef9f..c1bd9d7dc 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -183,15 +183,13 @@ Item { Behavior on Layout.preferredWidth { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on radius { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index 914164e6b..bf978ec0b 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -119,7 +119,7 @@ Item { Behavior on opacity { Anim { - duration: Tokens.anim.durations.extraLarge + type: Anim.StandardExtraLarge } } } @@ -202,8 +202,7 @@ Item { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index 991802bb6..2677698b8 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -129,8 +129,7 @@ LazyListView { enabled: notif.LazyListView.ready Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -140,15 +139,13 @@ LazyListView { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on x { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } @@ -159,7 +156,6 @@ LazyListView { target: root.container property: "contentY" - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 8c9429bb0..2f2d15f17 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -236,15 +236,13 @@ StyledRect { Behavior on rotation { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on Layout.topMargin { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 7b0463912..75e79040d 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -132,8 +132,7 @@ LazyListView { enabled: notif.LazyListView.ready Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -147,8 +146,7 @@ LazyListView { Behavior on x { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 0ccc62b33..108c51a26 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -20,8 +20,7 @@ Item { Behavior on offsetScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 8eda4607f..13125f2c3 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -203,8 +203,7 @@ Loader { Behavior on scale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 137e817b9..958d90e6b 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -65,8 +65,7 @@ Item { Behavior on offsetScale { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 13d6f7a12..c37ef3c52 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -101,14 +101,13 @@ StyledRect { Behavior on anchors.bottomMargin { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on opacity { Anim { - duration: Tokens.anim.durations.small + type: Anim.StandardSmall } } @@ -119,8 +118,7 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index c239e06b4..a0ec101b3 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -233,8 +233,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 9cdf4291d..8c294eb38 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -163,8 +163,7 @@ StyledRect { Behavior on Layout.preferredWidth { Anim { - duration: Tokens.anim.durations.expressiveFastSpatial - easing: Tokens.anim.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index cce8b9f26..21f2934e1 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -111,8 +111,7 @@ Item { properties: "opacity,scale" from: 0 to: 1 - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } ParallelAnimation { @@ -148,8 +147,7 @@ Item { Behavior on anchors.bottomMargin { Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } From 921c619ccb740a52c5cb5b228d346583564d9ecb Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:30:09 +1000 Subject: [PATCH 351/409] refactor: use new AnchorAnim component --- modules/background/Background.qml | 6 ++---- modules/bar/popouts/Battery.qml | 5 ++--- modules/controlcenter/bluetooth/Details.qml | 5 ++--- modules/controlcenter/bluetooth/Settings.qml | 5 ++--- modules/notifications/Notification.qml | 10 ++++------ 5 files changed, 12 insertions(+), 19 deletions(-) diff --git a/modules/background/Background.qml b/modules/background/Background.qml index af63848cb..70f6914dd 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Quickshell.Wayland import Caelestia.Config +import qs.components import qs.components.containers import qs.services @@ -145,10 +146,7 @@ Variants { ] transitions: Transition { - AnchorAnimation { - duration: Tokens.anim.durations.expressiveDefaultSpatial - easing: Tokens.anim.expressiveDefaultSpatial - } + AnchorAnim {} } sourceComponent: DesktopClock { diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index fdfb1089c..ac17bdfee 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -147,9 +147,8 @@ Column { ] transitions: Transition { - AnchorAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.emphasized + AnchorAnim { + type: AnchorAnim.Emphasized } } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 09cc74c94..41d7cc027 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -172,9 +172,8 @@ StyledFlickable { } transitions: Transition { - AnchorAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + AnchorAnim { + type: AnchorAnim.Standard } Anim { properties: "implicitHeight,opacity,padding" diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index c3f15672a..ad5f6b340 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -310,9 +310,8 @@ ColumnLayout { } transitions: Transition { - AnchorAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + AnchorAnim { + type: AnchorAnim.Standard } Anim { properties: "implicitHeight,opacity,padding" diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 2598ccf68..eb0676612 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -284,9 +284,8 @@ StyledRect { target: summary property: "maximumLineCount" } - AnchorAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + AnchorAnim { + type: AnchorAnim.Standard } } @@ -327,9 +326,8 @@ StyledRect { } transitions: Transition { - AnchorAnimation { - duration: Tokens.anim.durations.normal - easing: Tokens.anim.standard + AnchorAnim { + type: AnchorAnim.Standard } } } From 20dca094ecab5db2c1a361cd05bcac7fbd886635 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 15 Apr 2026 18:33:01 +1000 Subject: [PATCH 352/409] chore: remove unused imports --- modules/areapicker/Picker.qml | 1 - modules/bar/popouts/ClipWrapper.qml | 1 - 2 files changed, 2 deletions(-) diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index bb71e5fb3..7a13150cb 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -6,7 +6,6 @@ import Quickshell import Quickshell.Io import Quickshell.Wayland import Caelestia -import Caelestia.Config import qs.components import qs.services diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml index 04a437f66..ab80d2cc9 100644 --- a/modules/bar/popouts/ClipWrapper.qml +++ b/modules/bar/popouts/ClipWrapper.qml @@ -2,7 +2,6 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell -import Caelestia.Config import qs.components import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others From 6e234d6656ef9177958aa8755b8daf39829b4ce1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:04:04 +1000 Subject: [PATCH 353/409] refactor: use signals instead of functions --- components/StateLayer.qml | 201 ++++++++++++------ components/controls/CollapsibleSection.qml | 9 +- components/controls/CustomSpinBox.qml | 18 +- components/controls/IconButton.qml | 7 +- components/controls/IconTextButton.qml | 5 +- components/controls/Menu.qml | 7 +- components/controls/SplitButton.qml | 7 +- components/controls/StyledRadioButton.qml | 4 +- components/controls/TextButton.qml | 5 +- components/controls/ToggleButton.qml | 5 +- components/filedialog/DialogButtons.qml | 6 +- components/filedialog/FolderContents.qml | 5 +- components/filedialog/HeaderBar.qml | 6 +- components/filedialog/Sidebar.qml | 5 +- modules/background/Wallpaper.qml | 5 +- modules/bar/components/Power.qml | 5 +- modules/bar/components/Settings.qml | 39 ---- modules/bar/components/SettingsIcon.qml | 39 ---- modules/bar/popouts/ActiveWindow.qml | 5 +- modules/bar/popouts/Battery.qml | 5 +- modules/bar/popouts/Bluetooth.qml | 10 +- modules/bar/popouts/Network.qml | 21 +- modules/bar/popouts/TrayMenu.qml | 21 +- modules/bar/popouts/WirelessPassword.qml | 5 +- modules/bar/popouts/kblayout/KbLayout.qml | 2 +- modules/controlcenter/NavRail.qml | 4 +- modules/controlcenter/WindowTitle.qml | 2 +- .../sections/ColorSchemeSection.qml | 2 +- .../sections/ColorVariantSection.qml | 2 +- .../appearance/sections/FontsSection.qml | 6 +- modules/controlcenter/audio/AudioPane.qml | 10 +- modules/controlcenter/bluetooth/Details.qml | 8 +- .../controlcenter/bluetooth/DeviceList.qml | 4 +- modules/controlcenter/bluetooth/Settings.qml | 8 +- .../components/WallpaperGrid.qml | 2 +- .../controlcenter/launcher/LauncherPane.qml | 2 +- .../controlcenter/network/EthernetList.qml | 4 +- modules/controlcenter/network/VpnList.qml | 6 +- .../controlcenter/network/WirelessList.qml | 4 +- .../network/WirelessPasswordDialog.qml | 2 +- modules/dashboard/dash/Calendar.qml | 9 +- modules/dashboard/dash/Media.qml | 31 +-- modules/dashboard/dash/User.qml | 5 +- modules/launcher/items/ActionItem.qml | 5 +- modules/launcher/items/AppItem.qml | 5 +- modules/launcher/items/CalcItem.qml | 7 +- modules/launcher/items/SchemeItem.qml | 5 +- modules/launcher/items/VariantItem.qml | 5 +- modules/launcher/items/WallpaperItem.qml | 5 +- modules/lock/Center.qml | 7 +- modules/lock/Media.qml | 27 +-- modules/lock/NotifGroup.qml | 5 +- modules/notifications/Notification.qml | 10 +- modules/session/Content.qml | 5 +- modules/sidebar/NotifActionList.qml | 2 +- modules/sidebar/NotifGroup.qml | 5 +- modules/windowinfo/Buttons.qml | 30 +-- 57 files changed, 281 insertions(+), 400 deletions(-) delete mode 100644 modules/bar/components/Settings.qml delete mode 100644 modules/bar/components/SettingsIcon.qml diff --git a/components/StateLayer.qml b/components/StateLayer.qml index a18e41f98..20a2f6848 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -1,4 +1,5 @@ import QtQuick +import QtQuick.Shapes import Caelestia.Config import qs.services @@ -7,90 +8,168 @@ MouseArea { property bool disabled property bool showHoverBackground: true - property color color: Colours.palette.m3onSurface - // Pick up radius from parent if it has one (parent can be anything with a radius property) - property real radius: parent?.radius ?? 0 // qmllint disable missing-property - property alias rect: hoverLayer + readonly property alias rect: base + + property bool shapeMorph + property real stateOpacity: pressed ? 0.1 : containsMouse ? 0.08 : 0 + + property real pressX: width / 2 + property real pressY: height / 2 + property real circleRadius + + property alias color: base.color + property alias radius: base.radius + property alias topLeftRadius: base.topLeftRadius + property alias topRightRadius: base.topRightRadius + property alias bottomLeftRadius: base.bottomLeftRadius + property alias bottomRightRadius: base.bottomRightRadius + + readonly property real endRadius: { + const d1 = distSq(0, 0); + const d2 = distSq(width, 0); + const d3 = distSq(0, height); + const d4 = distSq(width, height); + return Math.sqrt(Math.max(d1, d2, d3, d4)) * (shapeMorph ? 1.16 : 1); + } + property real endRadiusAtPress - function onClicked(): void { + function distSq(x: real, y: real): real { + return (pressX - x) ** 2 + (pressY - y) ** 2; } - anchors.fill: parent + function press(x: real, y: real): void { + pressX = x; + pressY = y; + fadeAnim.complete(); + circleRadius = 0; + circle.opacity = 0.1; + rippleAnim.restart(); + endRadiusAtPress = endRadius; + } + anchors.fill: parent enabled: !disabled cursorShape: disabled ? undefined : Qt.PointingHandCursor hoverEnabled: true - onPressed: event => { - if (disabled) - return; - - rippleAnim.x = event.x; - rippleAnim.y = event.y; - - const dist = (ox, oy) => ox * ox + oy * oy; - rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); - - rippleAnim.restart(); + onPressedChanged: { + if (!pressed && !rippleAnim.running && circle.opacity > 0) + fadeAnim.start(); } - onClicked: event => !disabled && onClicked(event) + onCircleRadiusChanged: { + if (!pressed && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) + fadeAnim.start(); + } - SequentialAnimation { + Anim { id: rippleAnim - property real x - property real y - property real radius + alwaysRunToEnd: true + target: root + property: "circleRadius" + to: root.endRadius + easing: Tokens.anim.standardDecel + duration: Tokens.anim.durations.normal * 2 + } - PropertyAction { - target: ripple - property: "x" - value: rippleAnim.x - } - PropertyAction { - target: ripple - property: "y" - value: rippleAnim.y - } - PropertyAction { - target: ripple - property: "opacity" - value: 0.08 - } - Anim { - target: ripple - properties: "implicitWidth,implicitHeight" - from: 0 - to: rippleAnim.radius * 2 - easing: Tokens.anim.standardDecel - } - Anim { - target: ripple - property: "opacity" - to: 0 - } + Anim { + id: fadeAnim + + target: circle + property: "opacity" + to: 0 } - StyledClippingRect { - id: hoverLayer + StyledRect { + id: base anchors.fill: parent + opacity: root.stateOpacity + color: Colours.palette.m3onSurface + // Pick up radius from parent if it has one (parent can be anything with a radius property) + radius: root.parent?.radius ?? 0 // qmllint disable missing-property + } - color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) - radius: root.radius + Shape { + id: circle - StyledRect { - id: ripple + anchors.fill: parent + opacity: 0 + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 0 + strokeColor: "transparent" + fillColor: base.color + fillGradient: RadialGradient { + centerX: root.pressX + centerY: root.pressY + centerRadius: root.circleRadius + focalX: centerX + focalY: centerY + + GradientStop { + position: 0 + color: Qt.alpha(base.color, 1) + } + GradientStop { + position: 0.99 + color: Qt.alpha(base.color, 1) + } + GradientStop { + position: 1 + color: Qt.alpha(base.color, 0) + } + } - radius: Tokens.rounding.full - color: root.color - opacity: 0 + startX: base.topLeftRadius + startY: 0 - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 + PathLine { + x: root.width - base.topLeftRadius + y: 0 + } + PathArc { + relativeX: base.topLeftRadius + relativeY: base.topLeftRadius + radiusX: base.topLeftRadius + radiusY: base.topLeftRadius + } + PathLine { + x: root.width + y: root.height - base.bottomRightRadius + } + PathArc { + relativeX: -base.bottomRightRadius + relativeY: base.bottomRightRadius + radiusX: base.bottomRightRadius + radiusY: base.bottomRightRadius + } + PathLine { + x: base.bottomLeftRadius + y: root.height + } + PathArc { + relativeX: -base.bottomLeftRadius + relativeY: -base.bottomLeftRadius + radiusX: base.bottomLeftRadius + radiusY: base.bottomLeftRadius + } + PathLine { + x: 0 + y: base.topLeftRadius + } + PathArc { + x: base.topLeftRadius + y: 0 + radiusX: base.topLeftRadius + radiusY: base.topLeftRadius } } } + + Behavior on stateOpacity { + Anim {} + } } diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 52aa73c42..1c1eccec9 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -62,15 +62,14 @@ ColumnLayout { } StateLayer { - function onClicked(): void { - root.toggleRequested(); - root.expanded = !root.expanded; - } - anchors.fill: parent color: Colours.palette.m3onSurface radius: Tokens.rounding.normal showHoverBackground: false + onClicked: { + root.toggleRequested(); + root.expanded = !root.expanded; + } } } diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index b5f4f8ddd..96175192e 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -94,7 +94,12 @@ RowLayout { StateLayer { id: upState - function onClicked(): void { + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() + + onClicked: { let newValue = Math.min(root.max, root.value + root.step); // Round to avoid floating point precision errors const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; @@ -103,11 +108,6 @@ RowLayout { root.displayText = newValue.toString(); root.valueModified(newValue); } - - color: Colours.palette.m3onPrimary - - onPressAndHold: timer.start() - onReleased: timer.stop() } MaterialIcon { @@ -129,7 +129,7 @@ RowLayout { StateLayer { id: downState - function onClicked(): void { + onClicked: { let newValue = Math.max(root.min, root.value - root.step); // Round to avoid floating point precision errors const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; @@ -162,9 +162,9 @@ RowLayout { triggeredOnStart: true onTriggered: { if (upState.pressed) - upState.onClicked(); + upState.clicked(); else if (downState.pressed) - downState.onClicked(); + downState.clicked(); } } } diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index d0e6ac318..01be6d8be 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -53,14 +53,13 @@ StyledRect { StateLayer { id: stateLayer - function onClicked(): void { + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled + onClicked: { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - disabled: root.disabled } MaterialIcon { diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index 919c1f1d1..c93ec9dd3 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -45,13 +45,12 @@ StyledRect { StateLayer { id: stateLayer - function onClicked(): void { + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + onClicked: { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } RowLayout { diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 8dcf0ad67..d0a35318b 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -52,15 +52,14 @@ Elevation { color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) StateLayer { - function onClicked(): void { + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + disabled: !root.expanded + onClicked: { root.itemSelected(item.modelData); root.active = item.modelData; item.modelData.clicked(); root.expanded = false; } - - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - disabled: !root.expanded } RowLayout { diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index d3e36e92b..e3e2cfa82 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -47,14 +47,11 @@ Row { StateLayer { id: stateLayer - function onClicked(): void { - root.active?.clicked(); - } - rect.topRightRadius: parent.topRightRadius rect.bottomRightRadius: parent.bottomRightRadius color: root.textColour disabled: root.disabled + onClicked: root.active?.clicked() } RowLayout { @@ -109,7 +106,7 @@ Row { StateLayer { id: expandStateLayer - function onClicked(): void { + onClicked: { root.expanded = !root.expanded; } diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index a141b2451..d9a489ff6 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -24,13 +24,11 @@ RadioButton { anchors.verticalCenter: parent.verticalCenter StateLayer { - function onClicked(): void { - root.click(); - } anchors.margins: -Tokens.padding.smaller color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary z: -1 + onClicked: root.click() } StyledRect { diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index 159d63b4a..f18714056 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -56,13 +56,12 @@ StyledRect { StateLayer { id: stateLayer - function onClicked(): void { + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + onClicked: { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); } - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour } StyledText { diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 39abe4454..e470091c4 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -46,11 +46,8 @@ StyledRect { StateLayer { id: toggleStateLayer - function onClicked(): void { - root.clicked(); - } - color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] + onClicked: root.clicked() } RowLayout { diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index abd49d339..d65384938 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -49,11 +49,9 @@ StyledRect { implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { - root.dialog.accepted(root.folder.currentItem.modelData.path); - } disabled: !root.dialog.selectionValid + onClicked: root.dialog.accepted(root.folder.currentItem.modelData.path) } StyledText { @@ -75,7 +73,7 @@ StyledRect { implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { root.dialog.rejected(); } } diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index c5e19cd2c..d8869b83f 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -173,10 +173,7 @@ Item { clip: true StateLayer { - function onClicked(): void { - view.currentIndex = item.index; - } - + onClicked: view.currentIndex = item.index onDoubleClicked: { if (item.modelData.isDir) root.dialog.cwd.push(item.modelData.name); diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index 7bd66b2cd..e536a4230 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -28,12 +28,10 @@ StyledRect { implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { - function onClicked(): void { - root.dialog.cwd.pop(); - } radius: Tokens.rounding.small disabled: root.dialog.cwd.length === 1 + onClicked: root.dialog.cwd.pop() } MaterialIcon { @@ -94,7 +92,7 @@ StyledRect { anchors.fill: parent active: folder.index < root.dialog.cwd.length - 1 sourceComponent: StateLayer { - function onClicked(): void { + onClicked: { root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); } diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index ad10afbaf..e9c0918bf 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -52,14 +52,13 @@ StyledRect { color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) StateLayer { - function onClicked(): void { + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + onClicked: { if (place.modelData === "Home") root.dialog.cwd = ["Home"]; else root.dialog.cwd = ["Home", place.modelData]; } - - color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } RowLayout { diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index b5eac8dd1..8cc2df9d3 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -79,12 +79,9 @@ Item { } StateLayer { - function onClicked(): void { - dialog.open(); - } - radius: parent.radius color: Colours.palette.m3onPrimary + onClicked: dialog.open() } StyledText { diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 8fca7adce..681a805d7 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -13,15 +13,12 @@ Item { StateLayer { // Cursed workaround to make the height larger than the parent - function onClicked(): void { - root.visibilities.session = !root.visibilities.session; - } - anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 radius: Tokens.rounding.full + onClicked: root.visibilities.session = !root.visibilities.session } MaterialIcon { diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml deleted file mode 100644 index 5424e4145..000000000 --- a/modules/bar/components/Settings.qml +++ /dev/null @@ -1,39 +0,0 @@ -import QtQuick -import Caelestia.Config -import qs.components -import qs.services -import qs.modules.controlcenter - -Item { - id: root - - implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: icon.implicitHeight - - StateLayer { - // Cursed workaround to make the height larger than the parent - function onClicked(): void { - WindowFactory.create(null, { - active: "network" - }); - } - - anchors.fill: undefined - anchors.centerIn: parent - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 - radius: Tokens.rounding.full - } - - MaterialIcon { - id: icon - - anchors.centerIn: parent - anchors.horizontalCenterOffset: -1 - - text: "settings" - color: Colours.palette.m3onSurface - font.bold: true - font.pointSize: Tokens.font.size.normal - } -} diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml deleted file mode 100644 index 5424e4145..000000000 --- a/modules/bar/components/SettingsIcon.qml +++ /dev/null @@ -1,39 +0,0 @@ -import QtQuick -import Caelestia.Config -import qs.components -import qs.services -import qs.modules.controlcenter - -Item { - id: root - - implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: icon.implicitHeight - - StateLayer { - // Cursed workaround to make the height larger than the parent - function onClicked(): void { - WindowFactory.create(null, { - active: "network" - }); - } - - anchors.fill: undefined - anchors.centerIn: parent - implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 - radius: Tokens.rounding.full - } - - MaterialIcon { - id: icon - - anchors.centerIn: parent - anchors.horizontalCenterOffset: -1 - - text: "settings" - color: Colours.palette.m3onSurface - font.bold: true - font.pointSize: Tokens.font.size.normal - } -} diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index 59aabce13..122466451 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -65,11 +65,8 @@ Item { Layout.alignment: Qt.AlignVCenter StateLayer { - function onClicked(): void { - root.popouts.detachRequested("winfo"); - } - radius: Tokens.rounding.normal + onClicked: root.popouts.detachRequested("winfo") } MaterialIcon { diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index ac17bdfee..93d2012bd 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -203,12 +203,9 @@ Column { implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 StateLayer { - function onClicked(): void { - PowerProfiles.profile = parent.profile; - } - radius: Tokens.rounding.full color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + onClicked: PowerProfiles.profile = parent.profile } MaterialIcon { diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 931ad222b..baca115ea 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -124,12 +124,9 @@ ColumnLayout { } StateLayer { - function onClicked(): void { - device.modelData.connected = !device.modelData.connected; - } - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type disabled: device.loading + onClicked: device.modelData.connected = !device.modelData.connected } MaterialIcon { @@ -157,11 +154,8 @@ ColumnLayout { implicitHeight: connectBtn.implicitHeight StateLayer { - function onClicked(): void { - device.modelData.forget(); - } - radius: Tokens.rounding.full + onClicked: device.modelData.forget() } MaterialIcon { diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 956d4bac7..63b69b571 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -123,7 +123,10 @@ ColumnLayout { } StateLayer { - function onClicked(): void { + color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: networkItem.loading || !Nmcli.wifiEnabled + + onClicked: { if (networkItem.modelData.active) { Nmcli.disconnectFromNetwork(); } else { @@ -139,9 +142,6 @@ ColumnLayout { // This is handled by the onActiveChanged connection below } } - - color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Nmcli.wifiEnabled } MaterialIcon { @@ -173,12 +173,9 @@ ColumnLayout { color: Colours.palette.m3primaryContainer StateLayer { - function onClicked(): void { - Nmcli.rescanWifi(); - } - color: Colours.palette.m3onPrimaryContainer disabled: Nmcli.scanning || !Nmcli.wifiEnabled + onClicked: Nmcli.rescanWifi() } RowLayout { @@ -303,16 +300,16 @@ ColumnLayout { } StateLayer { - function onClicked(): void { + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading + + onClicked: { if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); } else { Nmcli.connectEthernet(ethernetItem.modelData.connection || "", ethernetItem.modelData.interface || "", () => {}); } } - - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: ethernetItem.loading } MaterialIcon { diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 493292386..7975d1bf4 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -97,7 +97,14 @@ StackView { implicitHeight: label.implicitHeight StateLayer { - function onClicked(): void { + anchors.margins: -Tokens.padding.small / 2 + anchors.leftMargin: -Tokens.padding.smaller + anchors.rightMargin: -Tokens.padding.smaller + + radius: item.radius + disabled: !item.modelData.enabled + + onClicked: { const entry = item.modelData; if (entry.hasChildren) root.push(subMenuComp.createObject(null, { @@ -109,13 +116,6 @@ StackView { root.popouts.hasCurrent = false; } } - - anchors.margins: -Tokens.padding.small / 2 - anchors.leftMargin: -Tokens.padding.smaller - anchors.rightMargin: -Tokens.padding.smaller - - radius: item.radius - disabled: !item.modelData.enabled } Loader { @@ -197,12 +197,9 @@ StackView { color: Colours.palette.m3secondaryContainer StateLayer { - function onClicked(): void { - root.pop(); - } - radius: parent.radius color: Colours.palette.m3onSecondaryContainer + onClicked: root.pop() } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 909d79f27..f6a635fed 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -363,13 +363,10 @@ ColumnLayout { } StateLayer { - function onClicked(): void { - passwordContainer.forceActiveFocus(); - } - hoverEnabled: false cursorShape: Qt.IBeamCursor radius: Tokens.rounding.normal + onClicked: passwordContainer.forceActiveFocus() } StyledText { diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 2d6f94120..d8f9a462d 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -95,7 +95,7 @@ ColumnLayout { StateLayer { id: layer - function onClicked(): void { + onClicked: { if (!kbDelegate.isDisabled) kb.switchTo(kbDelegate.layoutIndex); } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index 420300d44..a126c8009 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -59,7 +59,7 @@ Item { StateLayer { id: normalWinState - function onClicked(): void { + onClicked: { root.session.root.close(); WindowFactory.create(null, { active: root.session.active, @@ -173,7 +173,7 @@ Item { implicitHeight: icon.implicitHeight + Tokens.padding.small StateLayer { - function onClicked(): void { + onClicked: { // Prevent tab switching during initial opening animation to avoid blank pages if (!root.initialOpeningComplete) { return; diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index 70ea22ea4..8f66968ba 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -34,7 +34,7 @@ StyledRect { implicitHeight: closeIcon.implicitHeight + Tokens.padding.small StateLayer { - function onClicked(): void { + onClicked: { QsWindow.window.destroy(); } diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 90eaeb25a..0a65cbdc0 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -38,7 +38,7 @@ CollapsibleSection { implicitHeight: schemeRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { const name = modelData.name; const flavour = modelData.flavour; const schemeKey = `${name} ${flavour}`; diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index a5de4ad8a..c612485d0 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -35,7 +35,7 @@ CollapsibleSection { implicitHeight: variantRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { const variant = modelData.variant; Schemes.currentVariant = variant; diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 1791fc69b..10040c587 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -58,7 +58,7 @@ CollapsibleSection { implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { rootPane.fontFamilySans = modelData; rootPane.saveConfig(); } @@ -139,7 +139,7 @@ CollapsibleSection { implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { rootPane.fontFamilyMono = modelData; rootPane.saveConfig(); } @@ -222,7 +222,7 @@ CollapsibleSection { implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { rootPane.fontFamilyMaterial = modelData; rootPane.saveConfig(); } diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index a1da681e9..f3edb7319 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -97,7 +97,7 @@ Item { implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { Audio.setAudioSink(modelData); } } @@ -174,7 +174,7 @@ Item { implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { Audio.setAudioSource(modelData); } } @@ -318,7 +318,7 @@ Item { color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { - function onClicked(): void { + onClicked: { if (Audio.sink?.audio) { Audio.sink.audio.muted = !Audio.sink.audio.muted; } @@ -436,7 +436,7 @@ Item { color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { - function onClicked(): void { + onClicked: { if (Audio.source?.audio) { Audio.source.audio.muted = !Audio.source.audio.muted; } @@ -570,7 +570,7 @@ Item { color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { - function onClicked(): void { + onClicked: { Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); } } diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 41d7cc027..415752265 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -239,7 +239,7 @@ StyledFlickable { scale: root.session.bt.editingDeviceName ? 1 : 0.5 StateLayer { - function onClicked(): void { + onClicked: { root.session.bt.editingDeviceName = false; deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); } @@ -276,7 +276,7 @@ StyledFlickable { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { - function onClicked(): void { + onClicked: { root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; if (root.session.bt.editingDeviceName) deviceNameEdit.forceActiveFocus(); @@ -527,7 +527,7 @@ StyledFlickable { ] StateLayer { - function onClicked(): void { + onClicked: { root.session.bt.fabMenuOpen = false; const name = fabMenuItem.modelData.name; @@ -624,7 +624,7 @@ StyledFlickable { StateLayer { id: fabState - function onClicked(): void { + onClicked: { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 9833eacd6..419410eef 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -145,7 +145,7 @@ DeviceList { StateLayer { id: stateLayer - function onClicked(): void { + onClicked: { if (device.modelData) root.session.bt.active = device.modelData; } @@ -222,7 +222,7 @@ DeviceList { } StateLayer { - function onClicked(): void { + onClicked: { if (device.loading) return; diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index ad5f6b340..1202cc5bf 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -131,7 +131,7 @@ ColumnLayout { implicitHeight: adapterPicker.implicitHeight + Tokens.padding.smaller * 2 StateLayer { - function onClicked(): void { + onClicked: { adapterPickerButton.expanded = !adapterPickerButton.expanded; } @@ -209,7 +209,7 @@ ColumnLayout { implicitHeight: adapterInner.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { adapterPickerButton.expanded = false; root.session.bt.currentAdapter = adapter.modelData; } @@ -376,7 +376,7 @@ ColumnLayout { scale: root.session.bt.editingAdapterName ? 1 : 0.5 StateLayer { - function onClicked(): void { + onClicked: { root.session.bt.editingAdapterName = false; adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } @@ -413,7 +413,7 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { - function onClicked(): void { + onClicked: { root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; if (root.session.bt.editingAdapterName) adapterNameEdit.forceActiveFocus(); diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index efab91452..44a615c6b 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -40,7 +40,7 @@ GridView { height: root.cellHeight StateLayer { - function onClicked(): void { + onClicked: { Wallpapers.setWallpaper(modelData.path); } diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 754678733..f2372dbe1 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -328,7 +328,7 @@ Item { } StateLayer { - function onClicked(): void { + onClicked: { root.session.launcher.active = modelData; } } diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index 595233c5d..3a947c866 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -70,7 +70,7 @@ DeviceList { StateLayer { id: stateLayer - function onClicked(): void { + onClicked: { root.session.ethernet.active = modelData; } } @@ -147,7 +147,7 @@ DeviceList { color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { - function onClicked(): void { + onClicked: { if (modelData.connected && modelData.connection) { Nmcli.disconnectEthernet(modelData.connection, () => {}); } else { diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 1022117b6..d9266b34e 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -109,7 +109,7 @@ ColumnLayout { radius: Tokens.rounding.normal StateLayer { - function onClicked(): void { + onClicked: { if (root.session && root.session.vpn) { root.session.vpn.active = modelData; } @@ -211,7 +211,7 @@ ColumnLayout { color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) StateLayer { - function onClicked(): void { + onClicked: { const clickedIndex = modelData.index; if (modelData.enabled) { @@ -271,7 +271,7 @@ ColumnLayout { color: "transparent" StateLayer { - function onClicked(): void { + onClicked: { const providers = []; for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i !== modelData.index) { diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 9eefb1729..b6acd0792 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -116,7 +116,7 @@ DeviceList { radius: Tokens.rounding.normal StateLayer { - function onClicked(): void { + onClicked: { root.session.network.active = modelData; if (modelData && modelData.ssid) { root.checkSavedProfileForNetwork(modelData.ssid); @@ -197,7 +197,7 @@ DeviceList { color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) StateLayer { - function onClicked(): void { + onClicked: { if (modelData.active) { Nmcli.disconnectFromNetwork(); } else { diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 5ff48f771..ac88f9fca 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -288,7 +288,7 @@ Item { } StateLayer { - function onClicked(): void { + onClicked: { passwordContainer.forceActiveFocus(); } diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index 1c6bcadae..e2af3c015 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -51,11 +51,8 @@ CustomMouseArea { StateLayer { id: prevMonthStateLayer - function onClicked(): void { - root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - } - radius: Tokens.rounding.full + onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) } MaterialIcon { @@ -76,7 +73,7 @@ CustomMouseArea { implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.small * 2 StateLayer { - function onClicked(): void { + onClicked: { root.dashState.currentDate = new Date(); } @@ -111,7 +108,7 @@ CustomMouseArea { StateLayer { id: nextMonthStateLayer - function onClicked(): void { + onClicked: { root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 46e56b24a..777a24b98 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -173,31 +173,22 @@ Item { spacing: Tokens.spacing.small - Control { - function onClicked(): void { - Players.active?.previous(); - } - + PlayerControl { icon: "skip_previous" canUse: Players.active?.canGoPrevious ?? false + onClicked: Players.active?.previous() } - Control { - function onClicked(): void { - Players.active?.togglePlaying(); - } - + PlayerControl { icon: Players.active?.isPlaying ? "pause" : "play_arrow" canUse: Players.active?.canTogglePlaying ?? false + onClicked: Players.active?.togglePlaying() } - Control { - function onClicked(): void { - Players.active?.next(); - } - + PlayerControl { icon: "skip_next" canUse: Players.active?.canGoNext ?? false + onClicked: Players.active?.next() } } @@ -219,25 +210,21 @@ Item { fillMode: AnimatedImage.PreserveAspectFit } - component Control: StyledRect { + component PlayerControl: StyledRect { id: control required property string icon required property bool canUse - function onClicked(): void { - } + signal clicked implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small implicitHeight: implicitWidth StateLayer { - function onClicked(): void { - control.onClicked(); - } - disabled: !control.canUse radius: Tokens.rounding.full + onClicked: control.clicked() } MaterialIcon { diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index d862e411e..79787de51 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -69,12 +69,11 @@ Row { opacity: parent.containsMouse ? 1 : 0 StateLayer { - function onClicked(): void { + color: Colours.palette.m3onPrimary + onClicked: { root.visibilities.launcher = false; root.facePicker.open(); } - - color: Colours.palette.m3onPrimary } MaterialIcon { diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index a0473844a..0f9fb5dd6 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -15,11 +15,8 @@ Item { anchors.right: parent?.right StateLayer { - function onClicked(): void { - root.modelData?.onClicked(root.list); - } - radius: Tokens.rounding.normal + onClicked: root.modelData?.onClicked(root.list) } Item { diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index a78349cde..80739c8f1 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -19,12 +19,11 @@ Item { anchors.right: parent?.right StateLayer { - function onClicked(): void { + radius: Tokens.rounding.normal + onClicked: { Apps.launch(root.modelData); root.visibilities.launcher = false; } - - radius: Tokens.rounding.normal } Item { diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 447795446..e7f11babe 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -28,11 +28,8 @@ Item { anchors.right: parent?.right StateLayer { - function onClicked(): void { - root.onClicked(); - } - radius: Tokens.rounding.normal + onClicked: root.onClicked() } RowLayout { @@ -80,7 +77,7 @@ Item { StateLayer { id: stateLayer - function onClicked(): void { + onClicked: { Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); root.list.visibilities.launcher = false; } diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 96e08548e..ab0784bd6 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -16,11 +16,8 @@ Item { anchors.right: parent?.right StateLayer { - function onClicked(): void { - root.modelData?.onClicked(root.list); - } - radius: Tokens.rounding.normal + onClicked: root.modelData?.onClicked(root.list) } Item { diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index a95839ad2..b13d5d8e4 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -16,11 +16,8 @@ Item { anchors.right: parent?.right StateLayer { - function onClicked(): void { - root.modelData?.onClicked(root.list); - } - radius: Tokens.rounding.normal + onClicked: root.modelData?.onClicked(root.list) } Item { diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 771c26caf..58be068d7 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -25,12 +25,11 @@ Item { implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal StateLayer { - function onClicked(): void { + radius: Tokens.rounding.normal + onClicked: { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; } - - radius: Tokens.rounding.normal } Elevation { diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 9d6fd2c53..a987d4eb9 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -135,7 +135,7 @@ ColumnLayout { } StateLayer { - function onClicked(): void { + onClicked: { parent.forceActiveFocus(); } @@ -194,11 +194,8 @@ ColumnLayout { radius: Tokens.rounding.full StateLayer { - function onClicked(): void { - root.lock.pam.passwd.start(); - } - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + onClicked: root.lock.pam.passwd.start() } MaterialIcon { diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 5882c3228..05c9daaf2 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -110,34 +110,31 @@ Item { spacing: Tokens.spacing.large PlayerControl { - function onClicked(): void { + icon: "skip_previous" + onClicked: { if (Players.active?.canGoPrevious) Players.active.previous(); } - - icon: "skip_previous" } PlayerControl { - function onClicked(): void { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } - animate: true icon: active ? "pause" : "play_arrow" colour: "Primary" level: active ? 2 : 1 active: Players.active?.isPlaying ?? false + onClicked: { + if (Players.active?.canTogglePlaying) + Players.active.togglePlaying(); + } } PlayerControl { - function onClicked(): void { + icon: "skip_next" + onClicked: { if (Players.active?.canGoNext) Players.active.next(); } - - icon: "skip_next" } } } @@ -151,8 +148,7 @@ Item { property string colour: "Secondary" property int level: 1 - function onClicked(): void { - } + signal clicked Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : active ? Tokens.padding.small * 2 : 0) implicitWidth: controlIcon.implicitWidth + Tokens.padding.large * 2 @@ -171,11 +167,8 @@ Item { StateLayer { id: controlState - function onClicked(): void { - control.onClicked(); - } - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] + onClicked: control.clicked() } MaterialIcon { diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index b07b5193d..0a5b6d738 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -176,11 +176,8 @@ StyledRect { Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 StateLayer { - function onClicked(): void { - root.expanded = !root.expanded; - } - color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface + onClicked: root.expanded = !root.expanded } RowLayout { diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index eb0676612..ceeef0069 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -356,12 +356,9 @@ StyledRect { implicitHeight: expandIcon.height StateLayer { - function onClicked() { - root.expanded = !root.expanded; - } - radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + onClicked: root.expanded = !root.expanded } MaterialIcon { @@ -487,12 +484,9 @@ StyledRect { implicitHeight: actionText.height + Tokens.padding.small * 2 StateLayer { - function onClicked(): void { - action.modelData.invoke(); - } - radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface + onClicked: action.modelData.invoke() } StyledText { diff --git a/modules/session/Content.qml b/modules/session/Content.qml index ed6dff117..7ff170900 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -115,12 +115,9 @@ Column { } StateLayer { - function onClicked(): void { - Quickshell.execDetached(button.command); - } - radius: parent.radius color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + onClicked: Quickshell.execDetached(button.command) } MaterialIcon { diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index c1bd9d7dc..084fe6781 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -131,7 +131,7 @@ Item { StateLayer { id: actionStateLayer - function onClicked(): void { + onClicked: { if (action.modelData.isClose) { root.notif.close(); } else if (action.modelData.isCopy) { diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 2f2d15f17..a2375df3f 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -204,11 +204,8 @@ StyledRect { radius: Tokens.rounding.full StateLayer { - function onClicked(): void { - root.toggleExpand(!root.expanded); - } - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface + onClicked: root.toggleExpand(!root.expanded) } RowLayout { diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 5d9752ca9..c4fbbd53a 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -37,11 +37,8 @@ ColumnLayout { implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.small StateLayer { - function onClicked(): void { - root.moveToWsExpanded = !root.moveToWsExpanded; - } - color: Colours.palette.m3onPrimary + onClicked: root.moveToWsExpanded = !root.moveToWsExpanded } MaterialIcon { @@ -83,7 +80,7 @@ ColumnLayout { readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 readonly property bool isCurrent: root.client?.workspace.id === wsId - function onClicked(): void { + onClicked: { Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); } @@ -109,13 +106,10 @@ ColumnLayout { spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.normal : Tokens.spacing.small Button { - function onClicked(): void { - Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); - } - color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") + onClicked: Hypr.dispatch(`togglefloating address:0x${root.client?.address}`) } Loader { @@ -126,24 +120,18 @@ ColumnLayout { Layout.rightMargin: active ? 0 : -parent.spacing sourceComponent: Button { - function onClicked(): void { - Hypr.dispatch(`pin address:0x${root.client?.address}`); - } - color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") + onClicked: Hypr.dispatch(`pin address:0x${root.client?.address}`) } } Button { - function onClicked(): void { - Hypr.dispatch(`killwindow address:0x${root.client?.address}`); - } - color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Kill") + onClicked: Hypr.dispatch(`killwindow address:0x${root.client?.address}`) } } @@ -152,8 +140,7 @@ ColumnLayout { property alias disabled: stateLayer.disabled property alias text: label.text - function onClicked(): void { - } + signal clicked radius: Tokens.rounding.small @@ -163,11 +150,8 @@ ColumnLayout { StateLayer { id: stateLayer - function onClicked(): void { - parent.onClicked(); - } - color: parent.onColor + onClicked: parent.clicked() } StyledText { From 4ed029f3afc529daf95e530b0a12bf262bb6ceb6 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:36:37 +1000 Subject: [PATCH 354/409] chore: format --- components/controls/StyledRadioButton.qml | 1 - components/filedialog/DialogButtons.qml | 1 - components/filedialog/HeaderBar.qml | 1 - 3 files changed, 3 deletions(-) diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index d9a489ff6..24c7d6e8b 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -24,7 +24,6 @@ RadioButton { anchors.verticalCenter: parent.verticalCenter StateLayer { - anchors.margins: -Tokens.padding.smaller color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary z: -1 diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index d65384938..b46b4f721 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -49,7 +49,6 @@ StyledRect { implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 StateLayer { - disabled: !root.dialog.selectionValid onClicked: root.dialog.accepted(root.folder.currentItem.modelData.path) } diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index e536a4230..5d482d487 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -28,7 +28,6 @@ StyledRect { implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { - radius: Tokens.rounding.small disabled: root.dialog.cwd.length === 1 onClicked: root.dialog.cwd.pop() From a6ce66814e64b265f300caf1d295eab21fe0ce4e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 18 Apr 2026 01:37:43 +1000 Subject: [PATCH 355/409] chore: .. imports -> qs.components --- components/containers/StyledFlickable.qml | 2 +- components/containers/StyledListView.qml | 2 +- components/controls/CircularIndicator.qml | 2 +- components/controls/CircularProgress.qml | 2 +- components/controls/CollapsibleSection.qml | 1 - components/controls/CustomSpinBox.qml | 2 +- components/controls/FilledSlider.qml | 2 +- components/controls/IconButton.qml | 2 +- components/controls/IconTextButton.qml | 2 +- components/controls/Menu.qml | 2 +- components/controls/SpinBoxRow.qml | 1 - components/controls/SplitButton.qml | 2 +- components/controls/SplitButtonRow.qml | 1 - components/controls/StyledInputField.qml | 1 - components/controls/StyledScrollBar.qml | 2 +- components/controls/StyledSwitch.qml | 2 +- components/controls/StyledTextField.qml | 2 +- components/controls/SwitchRow.qml | 8 +++----- components/controls/TextButton.qml | 2 +- components/controls/ToggleButton.qml | 6 ++---- components/controls/Tooltip.qml | 2 +- components/effects/Colouriser.qml | 2 +- components/effects/Elevation.qml | 2 +- components/effects/InnerBorder.qml | 2 +- components/filedialog/CurrentItem.qml | 2 +- components/filedialog/HeaderBar.qml | 2 +- components/widgets/ExtraIndicator.qml | 2 +- 27 files changed, 26 insertions(+), 34 deletions(-) diff --git a/components/containers/StyledFlickable.qml b/components/containers/StyledFlickable.qml index bc6ae0f62..b792e5833 100644 --- a/components/containers/StyledFlickable.qml +++ b/components/containers/StyledFlickable.qml @@ -1,5 +1,5 @@ -import ".." import QtQuick +import qs.components Flickable { id: root diff --git a/components/containers/StyledListView.qml b/components/containers/StyledListView.qml index 626d20635..8b5a4401e 100644 --- a/components/containers/StyledListView.qml +++ b/components/containers/StyledListView.qml @@ -1,5 +1,5 @@ -import ".." import QtQuick +import qs.components ListView { id: root diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index 763b112ca..aabda13a7 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -1,8 +1,8 @@ -import ".." import QtQuick import QtQuick.Templates import Caelestia.Config import Caelestia.Internal +import qs.components import qs.services BusyIndicator { diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index f66e02e0d..2372c86cd 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,7 +1,7 @@ -import ".." import QtQuick import QtQuick.Shapes import Caelestia.Config +import qs.components import qs.services Shape { diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index 1c1eccec9..80ea8fe8d 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -1,4 +1,3 @@ -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index 96175192e..d7885c30b 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config +import qs.components import qs.services RowLayout { diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index 794f707a6..1d8c85a9c 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -1,8 +1,8 @@ -import ".." import "../effects" import QtQuick import QtQuick.Templates import Caelestia.Config +import qs.components import qs.services Slider { diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index 01be6d8be..58af806a6 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -1,6 +1,6 @@ -import ".." import QtQuick import Caelestia.Config +import qs.components import qs.services StyledRect { diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index c93ec9dd3..2a1dacec7 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -1,7 +1,7 @@ -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config +import qs.components import qs.services StyledRect { diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index d0a35318b..8bd6830e1 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import ".." import "../effects" import QtQuick import QtQuick.Layouts import Caelestia.Config +import qs.components import qs.services Elevation { diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index d15414f0f..dc52c19c1 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -1,4 +1,3 @@ -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index e3e2cfa82..f4297db96 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -1,7 +1,7 @@ -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config +import qs.components import qs.services Row { diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index c097bfd53..bd1ecc622 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -1,6 +1,5 @@ pragma ComponentBehavior: Bound -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index 38b1208b1..ff7772cac 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -1,6 +1,5 @@ pragma ComponentBehavior: Bound -import ".." import QtQuick import Caelestia.Config import qs.components diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index aaeaff29d..8cfe3f8da 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -1,7 +1,7 @@ -import ".." import QtQuick import QtQuick.Templates import Caelestia.Config +import qs.components import qs.services ScrollBar { diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index df1bfc718..2e31adbff 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -1,8 +1,8 @@ -import ".." import QtQuick import QtQuick.Shapes import QtQuick.Templates import Caelestia.Config +import qs.components import qs.services Switch { diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 0d193e116..4ab2f824c 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import ".." import QtQuick import QtQuick.Controls import Caelestia.Config +import qs.components import qs.services TextField { diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 8b2d617dd..b909739fe 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -1,4 +1,3 @@ -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config @@ -10,7 +9,8 @@ StyledRect { required property string label required property bool checked - property var onToggled: function (checked) {} + + signal toggled(checked: bool) Layout.fillWidth: true implicitHeight: row.implicitHeight + Tokens.padding.large * 2 @@ -38,9 +38,7 @@ StyledRect { StyledSwitch { checked: root.checked enabled: root.enabled - onToggled: { - root.onToggled(checked); // qmllint disable use-proper-function - } + onToggled: root.toggled(checked) } } } diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index f18714056..94d76ff86 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -1,6 +1,6 @@ -import ".." import QtQuick import Caelestia.Config +import qs.components import qs.services StyledRect { diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index e470091c4..232426c17 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -1,6 +1,5 @@ pragma ComponentBehavior: Bound -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config @@ -23,9 +22,8 @@ StyledRect { signal clicked - Component.onCompleted: { - hovered = toggleStateLayer.containsMouse; - } + Component.onCompleted: hovered = toggleStateLayer.containsMouse + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.normal * 2 : toggled ? Tokens.padding.small * 2 : 0) implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index 00252be3e..6c62f43d5 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -1,7 +1,7 @@ -import ".." import QtQuick import QtQuick.Controls import Caelestia.Config +import qs.components import qs.components.effects import qs.services diff --git a/components/effects/Colouriser.qml b/components/effects/Colouriser.qml index 2948155d6..b8abebb14 100644 --- a/components/effects/Colouriser.qml +++ b/components/effects/Colouriser.qml @@ -1,6 +1,6 @@ -import ".." import QtQuick import QtQuick.Effects +import qs.components MultiEffect { property color sourceColor: "black" diff --git a/components/effects/Elevation.qml b/components/effects/Elevation.qml index cfe5cb79a..9e0962dad 100644 --- a/components/effects/Elevation.qml +++ b/components/effects/Elevation.qml @@ -1,6 +1,6 @@ -import ".." import QtQuick import QtQuick.Effects +import qs.components import qs.services RectangularShadow { diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index 2723a04e1..d5442e8f9 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import ".." import QtQuick import QtQuick.Effects import Caelestia.Config +import qs.components import qs.services StyledRect { diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index 036828e55..83cb4a922 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -1,7 +1,7 @@ -import ".." import QtQuick import QtQuick.Shapes import Caelestia.Config +import qs.components import qs.services Item { diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index 5d482d487..c53d8f765 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import ".." import QtQuick import QtQuick.Layouts import Caelestia.Config +import qs.components import qs.services StyledRect { diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index 9c7a1ced9..d426eed88 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -1,7 +1,7 @@ -import ".." import "../effects" import QtQuick import Caelestia.Config +import qs.components import qs.services StyledRect { From 850d9fc1c91a11582442f433040d53c73664fe06 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:04:47 +1000 Subject: [PATCH 356/409] feat: forward declare where possible Hopefully speeds up compile times by at least a bit --- plugin/src/Caelestia/Config/config.cpp | 17 ++++++ plugin/src/Caelestia/Config/config.hpp | 56 ++++++++++++------- .../src/Caelestia/Config/configattached.hpp | 17 ++++++ plugin/src/Caelestia/Config/tokens.hpp | 2 - .../src/Caelestia/Config/tokensattached.cpp | 1 + .../src/Caelestia/Config/tokensattached.hpp | 20 +++++-- plugin/src/Caelestia/Internal/hyprextras.cpp | 1 + plugin/src/Caelestia/Internal/hyprextras.hpp | 6 +- .../src/Caelestia/Internal/sparklineitem.cpp | 1 + .../src/Caelestia/Internal/sparklineitem.hpp | 5 +- 10 files changed, 97 insertions(+), 29 deletions(-) diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp index 9cdcc9826..ff939f998 100644 --- a/plugin/src/Caelestia/Config/config.cpp +++ b/plugin/src/Caelestia/Config/config.cpp @@ -1,6 +1,23 @@ #include "config.hpp" +#include "appearanceconfig.hpp" +#include "backgroundconfig.hpp" +#include "barconfig.hpp" +#include "borderconfig.hpp" +#include "controlcenterconfig.hpp" +#include "dashboardconfig.hpp" +#include "generalconfig.hpp" +#include "launcherconfig.hpp" +#include "lockconfig.hpp" #include "monitorconfigmanager.hpp" +#include "notifsconfig.hpp" +#include "osdconfig.hpp" +#include "serviceconfig.hpp" +#include "sessionconfig.hpp" +#include "sidebarconfig.hpp" #include "tokens.hpp" +#include "userpaths.hpp" +#include "utilitiesconfig.hpp" +#include "winfoconfig.hpp" #include #include diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp index 3a66c0b00..248a3f225 100644 --- a/plugin/src/Caelestia/Config/config.hpp +++ b/plugin/src/Caelestia/Config/config.hpp @@ -1,32 +1,50 @@ #pragma once -#include - -#include "appearanceconfig.hpp" -#include "backgroundconfig.hpp" -#include "barconfig.hpp" -#include "borderconfig.hpp" -#include "controlcenterconfig.hpp" -#include "dashboardconfig.hpp" -#include "generalconfig.hpp" -#include "launcherconfig.hpp" -#include "lockconfig.hpp" -#include "notifsconfig.hpp" -#include "osdconfig.hpp" #include "rootconfig.hpp" -#include "serviceconfig.hpp" -#include "sessionconfig.hpp" -#include "sidebarconfig.hpp" -#include "userpaths.hpp" -#include "utilitiesconfig.hpp" -#include "winfoconfig.hpp" + +#include namespace caelestia::config { +class AppearanceConfig; +class BackgroundConfig; +class BarConfig; +class BorderConfig; +class ControlCenterConfig; +class DashboardConfig; +class GeneralConfig; +class LauncherConfig; +class LockConfig; +class NotifsConfig; +class OsdConfig; +class ServiceConfig; +class SessionConfig; +class SidebarConfig; +class UserPaths; +class UtilitiesConfig; +class WInfoConfig; + class GlobalConfig : public RootConfig { Q_OBJECT QML_ELEMENT QML_SINGLETON + Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("backgroundconfig.hpp") + Q_MOC_INCLUDE("barconfig.hpp") + Q_MOC_INCLUDE("borderconfig.hpp") + Q_MOC_INCLUDE("controlcenterconfig.hpp") + Q_MOC_INCLUDE("dashboardconfig.hpp") + Q_MOC_INCLUDE("generalconfig.hpp") + Q_MOC_INCLUDE("launcherconfig.hpp") + Q_MOC_INCLUDE("lockconfig.hpp") + Q_MOC_INCLUDE("notifsconfig.hpp") + Q_MOC_INCLUDE("osdconfig.hpp") + Q_MOC_INCLUDE("serviceconfig.hpp") + Q_MOC_INCLUDE("sessionconfig.hpp") + Q_MOC_INCLUDE("sidebarconfig.hpp") + Q_MOC_INCLUDE("userpaths.hpp") + Q_MOC_INCLUDE("utilitiesconfig.hpp") + Q_MOC_INCLUDE("winfoconfig.hpp") CONFIG_PROPERTY(bool, enabled, true) CONFIG_SUBOBJECT(AppearanceConfig, appearance) diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp index 35ccc6e41..815b10805 100644 --- a/plugin/src/Caelestia/Config/configattached.hpp +++ b/plugin/src/Caelestia/Config/configattached.hpp @@ -12,6 +12,23 @@ class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Config) + Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("backgroundconfig.hpp") + Q_MOC_INCLUDE("barconfig.hpp") + Q_MOC_INCLUDE("borderconfig.hpp") + Q_MOC_INCLUDE("controlcenterconfig.hpp") + Q_MOC_INCLUDE("dashboardconfig.hpp") + Q_MOC_INCLUDE("generalconfig.hpp") + Q_MOC_INCLUDE("launcherconfig.hpp") + Q_MOC_INCLUDE("lockconfig.hpp") + Q_MOC_INCLUDE("notifsconfig.hpp") + Q_MOC_INCLUDE("osdconfig.hpp") + Q_MOC_INCLUDE("serviceconfig.hpp") + Q_MOC_INCLUDE("sessionconfig.hpp") + Q_MOC_INCLUDE("sidebarconfig.hpp") + Q_MOC_INCLUDE("userpaths.hpp") + Q_MOC_INCLUDE("utilitiesconfig.hpp") + Q_MOC_INCLUDE("winfoconfig.hpp") Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 5ff1ce5e3..fd7a05691 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -1,7 +1,5 @@ #pragma once -#include "anim.hpp" -#include "appearanceconfig.hpp" #include "rootconfig.hpp" #include diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp index a86550df8..16f863b8c 100644 --- a/plugin/src/Caelestia/Config/tokensattached.cpp +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -1,5 +1,6 @@ #include "tokensattached.hpp" #include "anim.hpp" +#include "appearanceconfig.hpp" #include "config.hpp" #include "monitorconfigmanager.hpp" #include "tokens.hpp" diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp index 6416bda5d..810b0c0de 100644 --- a/plugin/src/Caelestia/Config/tokensattached.hpp +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -1,20 +1,30 @@ #pragma once -#include "anim.hpp" -#include "appearanceconfig.hpp" -#include "config.hpp" -#include "tokens.hpp" - +#include +#include #include namespace caelestia::config { +class AnimTokens; +class AppearanceFont; +class AppearancePadding; +class AppearanceRounding; +class AppearanceSpacing; +class AppearanceTransparency; +class GlobalConfig; +class SizeTokens; +class TokenConfig; + class Tokens : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { Q_OBJECT Q_INTERFACES(QQmlParserStatus) QML_ELEMENT QML_UNCREATABLE("") QML_ATTACHED(Tokens) + Q_MOC_INCLUDE("anim.hpp") + Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("tokens.hpp") Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) diff --git a/plugin/src/Caelestia/Internal/hyprextras.cpp b/plugin/src/Caelestia/Internal/hyprextras.cpp index e95dfa574..c73b6e0ba 100644 --- a/plugin/src/Caelestia/Internal/hyprextras.cpp +++ b/plugin/src/Caelestia/Internal/hyprextras.cpp @@ -1,4 +1,5 @@ #include "hyprextras.hpp" +#include "hyprdevices.hpp" #include #include diff --git a/plugin/src/Caelestia/Internal/hyprextras.hpp b/plugin/src/Caelestia/Internal/hyprextras.hpp index 14563c0a3..48eceea92 100644 --- a/plugin/src/Caelestia/Internal/hyprextras.hpp +++ b/plugin/src/Caelestia/Internal/hyprextras.hpp @@ -1,15 +1,19 @@ #pragma once -#include "hyprdevices.hpp" #include #include #include +#include +#include namespace caelestia::internal::hypr { +class HyprDevices; + class HyprExtras : public QObject { Q_OBJECT QML_ELEMENT + Q_MOC_INCLUDE("hyprdevices.hpp") Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) Q_PROPERTY(caelestia::internal::hypr::HyprDevices* devices READ devices CONSTANT) diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp index 5ffcc2f84..4e6b07194 100644 --- a/plugin/src/Caelestia/Internal/sparklineitem.cpp +++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp @@ -1,4 +1,5 @@ #include "sparklineitem.hpp" +#include "circularbuffer.hpp" #include #include diff --git a/plugin/src/Caelestia/Internal/sparklineitem.hpp b/plugin/src/Caelestia/Internal/sparklineitem.hpp index 945a1b34f..22632a906 100644 --- a/plugin/src/Caelestia/Internal/sparklineitem.hpp +++ b/plugin/src/Caelestia/Internal/sparklineitem.hpp @@ -5,13 +5,14 @@ #include #include -#include "circularbuffer.hpp" - namespace caelestia::internal { +class CircularBuffer; + class SparklineItem : public QQuickPaintedItem { Q_OBJECT QML_ELEMENT + Q_MOC_INCLUDE("circularbuffer.hpp") Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed) Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed) From f68fb78c801737fb8de29dde6151a1f7f74095da Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:16:16 +1000 Subject: [PATCH 357/409] feat: use pch to speed up builds --- plugin/src/Caelestia/CMakeLists.txt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index fbee04822..6f4ca9804 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -38,9 +38,24 @@ function(qml_module arg_TARGET) install(FILES "${module_qmldir}" DESTINATION "${module_dir}") install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") - target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES}) + target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) endfunction() +add_library(caelestia-pch INTERFACE) +target_precompile_headers(caelestia-pch INTERFACE + + + + + + + + + + + +) + qml_module(caelestia URI Caelestia SOURCES From a6a51a0b881525388572ef4ff211d00618dfd598 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 18 Apr 2026 02:28:00 +1000 Subject: [PATCH 358/409] fix: dash tab hover --- modules/dashboard/Tabs.qml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 32e8c469e..5bf99ca3e 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -121,6 +121,7 @@ Item { implicitWidth: Math.max(icon.width, label.width) implicitHeight: icon.height + label.height + hoverEnabled: true cursorShape: Qt.PointingHandCursor onPressed: event => { @@ -190,7 +191,7 @@ Item { anchors.fill: parent color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface - opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 + opacity: mouse.pressed ? 0.1 : mouse.containsMouse ? 0.08 : 0 Behavior on opacity { Anim {} From 2bb28197a08de98f22ffd732ab9da0d8c48c370b Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 19 Apr 2026 16:09:15 +1000 Subject: [PATCH 359/409] fix: don't use easingCurve type Fixes #1401 Nix does not have Qt 6.11 yet --- modules/bar/popouts/Wrapper.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 9a18f9117..7a66d3b97 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -36,7 +36,7 @@ Item { // Anim configs are not per-monitor readonly property QtObject dummy: QtObject {} property int animLength: dummy.Tokens.anim.durations.expressiveDefaultSpatial - property easingCurve animCurve: dummy.Tokens.anim.expressiveDefaultSpatial + property var animCurve: dummy.Tokens.anim.expressiveDefaultSpatial // The easingCurve type is Qt 6.11+ so we gotta use var for now function setAnims(detach: bool): void { const type = `expressive${detach ? "Slow" : "Default"}Spatial`; From ec8ce658572077b94621d8341f69d5bdb559ccff Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:15:31 +1000 Subject: [PATCH 360/409] feat: m3 expressive menus Also fix the player selector being click through Fixes #1405 Closes #1350 --- components/controls/Menu.qml | 221 ++++++++++++------ components/controls/SplitButton.qml | 24 +- modules/drawers/ContentWindow.qml | 3 + .../src/Caelestia/Config/appearanceconfig.cpp | 4 + .../src/Caelestia/Config/appearanceconfig.hpp | 2 + plugin/src/Caelestia/Config/tokens.cpp | 1 - plugin/src/Caelestia/Config/tokens.hpp | 1 + 7 files changed, 167 insertions(+), 89 deletions(-) diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index 8bd6830e1..482a4d409 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -1,96 +1,187 @@ pragma ComponentBehavior: Bound -import "../effects" import QtQuick import QtQuick.Layouts +import Quickshell import Caelestia.Config import qs.components +import qs.components.effects import qs.services +import qs.modules.drawers -Elevation { +MouseArea { id: root + enum Side { + Top, + Bottom, + Left, + Right + } + + required property Item attachTo + property int attachSideX: Menu.Right + property int attachSideY: Menu.Bottom + property int thisSideX: Menu.Right + property int thisSideY: Menu.Top + property real marginX + property real marginY + property list items property MenuItem active: items[0] ?? null property bool expanded signal itemSelected(item: MenuItem) - radius: Tokens.rounding.small / 2 - level: 2 + parent: { + const win = QsWindow.window; + const contentWin = win as ContentWindow; // If inside the drawer content window, put it inside the interaction wrapper so hover works + return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem; + } + anchors.fill: parent - implicitWidth: Math.max(200, column.implicitWidth) - implicitHeight: root.expanded ? column.implicitHeight : 0 - opacity: root.expanded ? 1 : 0 + enabled: expanded + onClicked: expanded = false - StyledClippingRect { - anchors.fill: parent - radius: parent.radius - color: Colours.palette.m3surfaceContainer + opacity: expanded ? 1 : 0 + layer.enabled: opacity < 1 - ColumnLayout { - id: column + Behavior on opacity { + Anim { + duration: Tokens.anim.durations.small + } + } - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 + TransformWatcher { + id: watcher - Repeater { - model: root.items + a: root.parent + b: root.attachTo + } - StyledRect { - id: item + Elevation { + id: menu - required property int index - required property MenuItem modelData - readonly property bool active: modelData === root.active + x: { + watcher.transform; // mapToItem is not reactive so this forces updates + const item = root.attachTo; + let off = root.attachSideX === Menu.Left ? 0 : item.width; + if (root.thisSideX === Menu.Right) + off -= width; + return item.mapToItem(root.parent, off, 0).x + root.marginX; + } + y: { + watcher.transform; // mapToItem is not reactive so this forces updates + const item = root.attachTo; + let off = root.attachSideY === Menu.Top ? 0 : item.height; + if (root.thisSideY === Menu.Bottom) + off -= height; + return item.mapToItem(root.parent, 0, off).y + root.marginY; + } - Layout.fillWidth: true - implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 - implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 + radius: Tokens.rounding.normal + level: 2 - color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) + implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2) + implicitHeight: column.implicitHeight + column.anchors.margins * 2 - StateLayer { - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - disabled: !root.expanded - onClicked: { - root.itemSelected(item.modelData); - root.active = item.modelData; - item.modelData.clicked(); - root.expanded = false; - } - } + transform: Scale { + yScale: root.expanded ? 1 : 0.1 + origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0 + + Behavior on yScale { + Anim { + type: Anim.DefaultSpatial + } + } + } + + StyledRect { + anchors.fill: parent + radius: parent.radius + color: Colours.palette.m3surfaceContainerLow + + ColumnLayout { + id: column + + anchors.fill: parent + anchors.margins: Tokens.padding.small + spacing: 0 + + Repeater { + id: repeater - RowLayout { - id: menuOptionRow + model: root.items - anchors.fill: parent - anchors.margins: Tokens.padding.normal - spacing: Tokens.spacing.small + StyledRect { + id: item - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - text: item.modelData.icon - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant + required property int index + required property MenuItem modelData + readonly property bool active: modelData === root.active + + Layout.fillWidth: true + implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 + + radius: active ? 12 : Tokens.rounding.extraSmall // This should use a token, but tokens are currently extremely scuffed + topLeftRadius: index === 0 ? Tokens.rounding.small : radius + topRightRadius: index === 0 ? Tokens.rounding.small : radius + bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius + bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius + + color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0) + + Behavior on radius { + Anim {} } - StyledText { - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - text: item.modelData.text - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + StateLayer { + topLeftRadius: parent.topLeftRadius + topRightRadius: parent.topRightRadius + bottomLeftRadius: parent.bottomLeftRadius + bottomRightRadius: parent.bottomRightRadius + + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface + disabled: !root.expanded + onClicked: { + root.itemSelected(item.modelData); + root.active = item.modelData; + item.modelData.clicked(); + root.expanded = false; + } } - Loader { - asynchronous: true - Layout.alignment: Qt.AlignVCenter - active: item.modelData.trailingIcon.length > 0 - visible: active + RowLayout { + id: menuOptionRow + + anchors.fill: parent + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: item.modelData.icon + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + text: item.modelData.text + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface + } + + Loader { + asynchronous: true + Layout.alignment: Qt.AlignVCenter + active: item.modelData.trailingIcon.length > 0 + visible: active - sourceComponent: MaterialIcon { - text: item.modelData.trailingIcon - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + sourceComponent: MaterialIcon { + text: item.modelData.trailingIcon + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant + } } } } @@ -98,16 +189,4 @@ Elevation { } } } - - Behavior on opacity { - Anim { - duration: Tokens.anim.durations.expressiveDefaultSpatial - } - } - - Behavior on implicitHeight { - Anim { - type: Anim.DefaultSpatial - } - } } diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index f4297db96..16d840ef5 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -138,24 +138,14 @@ Row { Behavior on rad { Anim {} } + } - Menu { - id: menu - - states: State { - when: root.menuOnTop - - AnchorChanges { - target: menu - anchors.top: undefined - anchors.bottom: expandBtn.top - } - } + Menu { + id: menu - anchors.top: parent.bottom - anchors.right: parent.right - anchors.topMargin: Tokens.spacing.small - anchors.bottomMargin: Tokens.spacing.small - } + attachTo: expandBtn + attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom + thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top + marginY: Tokens.spacing.small * (root.menuOnTop ? -1 : 1) } } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index eb44ebbaf..aeb968899 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -17,6 +17,7 @@ StyledWindow { id: root readonly property alias bar: bar + readonly property alias interactionWrapper: interactions readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0 @@ -229,6 +230,8 @@ StyledWindow { } Interactions { + id: interactions + screen: root.screen popouts: panels.popouts visibilities: visibilities diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp index 6f2433aab..55ab0470d 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.cpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -28,6 +28,10 @@ void AppearanceRounding::bindTokens(RoundingTokens* tokens) { connectTokenSignals(tokens, this); } +int AppearanceRounding::extraSmall() const { + return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; +} + int AppearanceRounding::small() const { return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; } diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index ccb732fdd..61850d620 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -19,6 +19,7 @@ class AppearanceRounding : public ConfigObject { CONFIG_PROPERTY(qreal, scale, 1) + Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) Q_PROPERTY(int small READ small NOTIFY valuesChanged) Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) Q_PROPERTY(int large READ large NOTIFY valuesChanged) @@ -30,6 +31,7 @@ class AppearanceRounding : public ConfigObject { void bindTokens(RoundingTokens* tokens); + [[nodiscard]] int extraSmall() const; [[nodiscard]] int small() const; [[nodiscard]] int normal() const; [[nodiscard]] int large() const; diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp index 3d3f48c94..6d171cb35 100644 --- a/plugin/src/Caelestia/Config/tokens.cpp +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -1,5 +1,4 @@ #include "tokens.hpp" -#include "config.hpp" #include "monitorconfigmanager.hpp" #include diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index fd7a05691..7dcb334fe 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -39,6 +39,7 @@ class RoundingTokens : public ConfigObject { Q_OBJECT QML_ANONYMOUS + CONFIG_PROPERTY(int, extraSmall, 4) CONFIG_PROPERTY(int, small, 12) CONFIG_PROPERTY(int, normal, 17) CONFIG_PROPERTY(int, large, 25) From d97bbb34a619abd3739e330dff4bfbbb708bb3de Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:21:03 +1000 Subject: [PATCH 361/409] chore: format --- components/controls/SplitButton.qml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index 16d840ef5..4acd98cf6 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -106,14 +106,11 @@ Row { StateLayer { id: expandStateLayer - onClicked: { - root.expanded = !root.expanded; - } - rect.topLeftRadius: parent.topLeftRadius rect.bottomLeftRadius: parent.bottomLeftRadius color: root.textColour disabled: root.disabled + onClicked: root.expanded = !root.expanded } MaterialIcon { From b0d5700be175194059d1bb7263e10bd6304870ba Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 19 Apr 2026 19:53:58 +1000 Subject: [PATCH 362/409] fix: state layer ripple Also improve ripple anim --- components/StateLayer.qml | 57 +++++++++++-------- plugin/src/Caelestia/Config/anim.cpp | 15 +++++ plugin/src/Caelestia/Config/anim.hpp | 9 +++ .../src/Caelestia/Config/appearanceconfig.cpp | 12 ++++ .../src/Caelestia/Config/appearanceconfig.hpp | 6 ++ plugin/src/Caelestia/Config/tokens.hpp | 11 +++- 6 files changed, 86 insertions(+), 24 deletions(-) diff --git a/components/StateLayer.qml b/components/StateLayer.qml index 20a2f6848..2ce8c5008 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -37,6 +37,10 @@ MouseArea { return (pressX - x) ** 2 + (pressY - y) ** 2; } + function clamp(r: real): real { + return Math.max(0, Math.min(r, width / 2, height / 2)); + } + function press(x: real, y: real): void { pressX = x; pressY = y; @@ -52,6 +56,8 @@ MouseArea { cursorShape: disabled ? undefined : Qt.PointingHandCursor hoverEnabled: true + onPressed: e => press(e.x, e.y) + onPressedChanged: { if (!pressed && !rippleAnim.running && circle.opacity > 0) fadeAnim.start(); @@ -69,8 +75,8 @@ MouseArea { target: root property: "circleRadius" to: root.endRadius - easing: Tokens.anim.standardDecel - duration: Tokens.anim.durations.normal * 2 + easing: Tokens.anim.expressiveSlowEffects + duration: Tokens.anim.durations.expressiveSlowEffects * 2 } Anim { @@ -79,6 +85,8 @@ MouseArea { target: circle property: "opacity" to: 0 + easing: Tokens.anim.expressiveSlowEffects + duration: Tokens.anim.durations.expressiveSlowEffects } StyledRect { @@ -123,53 +131,56 @@ MouseArea { } } - startX: base.topLeftRadius + startX: root.clamp(base.topLeftRadius) startY: 0 PathLine { - x: root.width - base.topLeftRadius + x: root.width - root.clamp(base.topLeftRadius) y: 0 } PathArc { - relativeX: base.topLeftRadius - relativeY: base.topLeftRadius - radiusX: base.topLeftRadius - radiusY: base.topLeftRadius + relativeX: root.clamp(base.topLeftRadius) + relativeY: root.clamp(base.topLeftRadius) + radiusX: root.clamp(base.topLeftRadius) + radiusY: root.clamp(base.topLeftRadius) } PathLine { x: root.width - y: root.height - base.bottomRightRadius + y: root.height - root.clamp(base.bottomRightRadius) } PathArc { - relativeX: -base.bottomRightRadius - relativeY: base.bottomRightRadius - radiusX: base.bottomRightRadius - radiusY: base.bottomRightRadius + relativeX: -root.clamp(base.bottomRightRadius) + relativeY: root.clamp(base.bottomRightRadius) + radiusX: root.clamp(base.bottomRightRadius) + radiusY: root.clamp(base.bottomRightRadius) } PathLine { - x: base.bottomLeftRadius + x: root.clamp(base.bottomLeftRadius) y: root.height } PathArc { - relativeX: -base.bottomLeftRadius - relativeY: -base.bottomLeftRadius - radiusX: base.bottomLeftRadius - radiusY: base.bottomLeftRadius + relativeX: -root.clamp(base.bottomLeftRadius) + relativeY: -root.clamp(base.bottomLeftRadius) + radiusX: root.clamp(base.bottomLeftRadius) + radiusY: root.clamp(base.bottomLeftRadius) } PathLine { x: 0 - y: base.topLeftRadius + y: root.clamp(base.topLeftRadius) } PathArc { - x: base.topLeftRadius + x: root.clamp(base.topLeftRadius) y: 0 - radiusX: base.topLeftRadius - radiusY: base.topLeftRadius + radiusX: root.clamp(base.topLeftRadius) + radiusY: root.clamp(base.topLeftRadius) } } } Behavior on stateOpacity { - Anim {} + Anim { + easing: Tokens.anim.expressiveDefaultEffects + duration: Tokens.anim.durations.expressiveDefaultEffects + } } } diff --git a/plugin/src/Caelestia/Config/anim.cpp b/plugin/src/Caelestia/Config/anim.cpp index 824523c2b..e307e2f9b 100644 --- a/plugin/src/Caelestia/Config/anim.cpp +++ b/plugin/src/Caelestia/Config/anim.cpp @@ -45,6 +45,18 @@ QEasingCurve AnimTokens::expressiveSlowSpatial() const { return m_expressiveSlowSpatial; } +QEasingCurve AnimTokens::expressiveFastEffects() const { + return m_expressiveFastEffects; +} + +QEasingCurve AnimTokens::expressiveDefaultEffects() const { + return m_expressiveDefaultEffects; +} + +QEasingCurve AnimTokens::expressiveSlowEffects() const { + return m_expressiveSlowEffects; +} + AnimDurations* AnimTokens::durations() const { return m_durations; } @@ -78,6 +90,9 @@ void AnimTokens::rebuildCurves() { m_expressiveFastSpatial = buildCurve(m_curves->expressiveFastSpatial()); m_expressiveDefaultSpatial = buildCurve(m_curves->expressiveDefaultSpatial()); m_expressiveSlowSpatial = buildCurve(m_curves->expressiveSlowSpatial()); + m_expressiveFastEffects = buildCurve(m_curves->expressiveFastEffects()); + m_expressiveDefaultEffects = buildCurve(m_curves->expressiveDefaultEffects()); + m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects()); emit curvesChanged(); } diff --git a/plugin/src/Caelestia/Config/anim.hpp b/plugin/src/Caelestia/Config/anim.hpp index b90c3bda9..895b8e067 100644 --- a/plugin/src/Caelestia/Config/anim.hpp +++ b/plugin/src/Caelestia/Config/anim.hpp @@ -24,6 +24,9 @@ class AnimTokens : public QObject { Q_PROPERTY(QEasingCurve expressiveFastSpatial READ expressiveFastSpatial NOTIFY curvesChanged) Q_PROPERTY(QEasingCurve expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY curvesChanged) Q_PROPERTY(QEasingCurve expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveFastEffects READ expressiveFastEffects NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY curvesChanged) Q_PROPERTY(caelestia::config::AnimDurations* durations READ durations NOTIFY durationsChanged) @@ -42,6 +45,9 @@ class AnimTokens : public QObject { [[nodiscard]] QEasingCurve expressiveFastSpatial() const; [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const; [[nodiscard]] QEasingCurve expressiveSlowSpatial() const; + [[nodiscard]] QEasingCurve expressiveFastEffects() const; + [[nodiscard]] QEasingCurve expressiveDefaultEffects() const; + [[nodiscard]] QEasingCurve expressiveSlowEffects() const; [[nodiscard]] AnimDurations* durations() const; signals: @@ -64,6 +70,9 @@ class AnimTokens : public QObject { QEasingCurve m_expressiveFastSpatial; QEasingCurve m_expressiveDefaultSpatial; QEasingCurve m_expressiveSlowSpatial; + QEasingCurve m_expressiveFastEffects; + QEasingCurve m_expressiveDefaultEffects; + QEasingCurve m_expressiveSlowEffects; }; } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp index 55ab0470d..59228c671 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.cpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -168,4 +168,16 @@ int AnimDurations::expressiveSlowSpatial() const { return m_tokens ? static_cast(m_tokens->expressiveSlowSpatial() * m_scale) : 0; } +int AnimDurations::expressiveFastEffects() const { + return m_tokens ? static_cast(m_tokens->expressiveFastEffects() * m_scale) : 0; +} + +int AnimDurations::expressiveDefaultEffects() const { + return m_tokens ? static_cast(m_tokens->expressiveDefaultEffects() * m_scale) : 0; +} + +int AnimDurations::expressiveSlowEffects() const { + return m_tokens ? static_cast(m_tokens->expressiveSlowEffects() * m_scale) : 0; +} + } // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index 61850d620..090ed7a66 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -180,6 +180,9 @@ class AnimDurations : public ConfigObject { Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY valuesChanged) Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY valuesChanged) Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY valuesChanged) + Q_PROPERTY(int expressiveFastEffects READ expressiveFastEffects NOTIFY valuesChanged) + Q_PROPERTY(int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY valuesChanged) + Q_PROPERTY(int expressiveSlowEffects READ expressiveSlowEffects NOTIFY valuesChanged) public: explicit AnimDurations(QObject* parent = nullptr) @@ -194,6 +197,9 @@ class AnimDurations : public ConfigObject { [[nodiscard]] int expressiveFastSpatial() const; [[nodiscard]] int expressiveDefaultSpatial() const; [[nodiscard]] int expressiveSlowSpatial() const; + [[nodiscard]] int expressiveFastEffects() const; + [[nodiscard]] int expressiveDefaultEffects() const; + [[nodiscard]] int expressiveSlowEffects() const; signals: void valuesChanged(); diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp index 7dcb334fe..28bac1f1a 100644 --- a/plugin/src/Caelestia/Config/tokens.hpp +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -20,6 +20,9 @@ class AnimCurves : public ConfigObject { CONFIG_GLOBAL_PROPERTY(QList, expressiveFastSpatial) CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultSpatial) CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowSpatial) + CONFIG_GLOBAL_PROPERTY(QList, expressiveFastEffects) + CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultEffects) + CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowEffects) public: explicit AnimCurves(QObject* parent = nullptr) @@ -32,7 +35,10 @@ class AnimCurves : public ConfigObject { , m_standardDecel({ 0, 0, 0, 1, 1, 1 }) , m_expressiveFastSpatial({ 0.42, 1.67, 0.21, 0.9, 1, 1 }) , m_expressiveDefaultSpatial({ 0.38, 1.21, 0.22, 1, 1, 1 }) - , m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) {} + , m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) + , m_expressiveFastEffects({ 0.31, 0.94, 0.34, 1, 1, 1 }) + , m_expressiveDefaultEffects({ 0.34, 0.8, 0.34, 1, 1, 1 }) + , m_expressiveSlowEffects({ 0.34, 0.88, 0.34, 1, 1, 1 }) {} }; class RoundingTokens : public ConfigObject { @@ -107,6 +113,9 @@ class AnimDurationTokens : public ConfigObject { CONFIG_GLOBAL_PROPERTY(int, expressiveFastSpatial, 350) CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultSpatial, 500) CONFIG_GLOBAL_PROPERTY(int, expressiveSlowSpatial, 650) + CONFIG_GLOBAL_PROPERTY(int, expressiveFastEffects, 150) + CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultEffects, 200) + CONFIG_GLOBAL_PROPERTY(int, expressiveSlowEffects, 300) public: explicit AnimDurationTokens(QObject* parent = nullptr) From 31c484b041fdc686d37b81005750c3b880734cc9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 19 Apr 2026 20:02:50 +1000 Subject: [PATCH 363/409] fix: wrap media position by length (#1410) --- modules/dashboard/Media.qml | 4 ++-- modules/dashboard/dash/Media.qml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 22ed204be..ccb5f0c4b 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -27,7 +27,7 @@ Item { property real playerProgress: { const active = Players.active; - return active?.length ? active.position / active.length : 0; + return active?.length ? (active.position % active.length) / active.length : 0; } function lengthStr(length: int): string { @@ -354,7 +354,7 @@ Item { anchors.left: parent.left - text: root.lengthStr(Players.active?.position ?? -1) + text: root.lengthStr(Players.active ? Players.active.position % Players.active.length : -1) color: Colours.palette.m3onSurfaceVariant font.pointSize: Tokens.font.size.small } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 777a24b98..b8e78abde 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -11,7 +11,7 @@ Item { property real playerProgress: { const active = Players.active; - return active?.length ? active.position / active.length : 0; + return active?.length ? (active.position % active.length) / active.length : 0; } anchors.top: parent.top From 92276be49a820d1d00dd782f3d0563d33e47b9b4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 19 Apr 2026 21:14:56 +1000 Subject: [PATCH 364/409] ci: update stable branch on release (#1414) --- .github/workflows/release.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b0dff7dce..29e704a96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -35,3 +35,8 @@ jobs: caelestia-shell-${{ github.ref_name }}.tar.gz caelestia-shell-latest.tar.gz generate_release_notes: true + + - name: Update stable branch + run: | + git branch -f stable "$GITHUB_SHA" + git push origin stable --force From fffe6f494304876246ab61df7c19220b063315a9 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:14:22 +1000 Subject: [PATCH 365/409] fix: sidebar notif group height anim --- modules/sidebar/NotifGroup.qml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index a2375df3f..aecefdb27 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -73,12 +73,18 @@ StyledRect { anchors.left: parent?.left anchors.right: parent?.right - implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 + implicitHeight: nonAnimHeight clip: true radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + Behavior on implicitHeight { + Anim { + type: Anim.DefaultSpatial + } + } + RowLayout { id: content From b004edad7d41d6a868222e1252862817d7dac52d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:27:05 +1000 Subject: [PATCH 366/409] feat: improve sidebar notif expand anim No layout so no shifting from pixel alignment Also remove top/bottom margins (were causing some weird position glitches) --- modules/sidebar/NotifGroup.qml | 20 +++++++++----------- modules/sidebar/NotifGroupList.qml | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index aecefdb27..25f531ec0 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -52,7 +52,7 @@ StyledRect { readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); - const columnHeight = headerHeight + notifList.layoutHeight + column.Layout.topMargin + column.Layout.bottomMargin; + const columnHeight = headerHeight + notifList.layoutHeight; return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) @@ -172,19 +172,21 @@ StyledRect { } } - ColumnLayout { + Column { id: column - Layout.topMargin: -Tokens.padding.small - Layout.bottomMargin: -Tokens.padding.small / 2 Layout.fillWidth: true - spacing: 0 + spacing: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 + + Behavior on spacing { + Anim {} + } RowLayout { id: header - Layout.bottomMargin: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 - Layout.fillWidth: true + anchors.left: parent.left + anchors.right: parent.right spacing: Tokens.spacing.smaller StyledText { @@ -251,10 +253,6 @@ StyledRect { } } } - - Behavior on Layout.bottomMargin { - Anim {} - } } NotifGroupList { diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index 75e79040d..72b8b3a8e 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,7 +1,6 @@ pragma ComponentBehavior: Bound import QtQuick -import QtQuick.Layouts import Quickshell import Caelestia.Components import Caelestia.Config @@ -19,7 +18,8 @@ LazyListView { signal requestToggleExpand(expand: bool) - Layout.fillWidth: true + anchors.left: parent.left + anchors.right: parent.right implicitHeight: contentHeight spacing: Math.round(Tokens.spacing.small / 2) From 89cb3b9d7c6c074c460e900e4fc2bce596df4f77 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 00:52:32 +1000 Subject: [PATCH 367/409] fix: don't cache icons + fix bg colour when transparent image Also don't double cache --- modules/notifications/Notification.qml | 4 +-- modules/sidebar/NotifGroup.qml | 43 +++++++------------------- services/NotifData.qml | 14 +++++---- 3 files changed, 22 insertions(+), 39 deletions(-) diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index ceeef0069..70f58af08 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -4,7 +4,6 @@ import QtQuick import QtQuick.Layouts import QtQuick.Shapes import Quickshell -import Quickshell.Widgets import Quickshell.Services.Notifications import Caelestia.Config import qs.components @@ -117,8 +116,9 @@ StyledRect { height: TokenConfig.sizes.notifs.image visible: root.hasImage || root.hasAppIcon - sourceComponent: ClippingRectangle { + sourceComponent: StyledClippingRect { radius: Tokens.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer implicitWidth: TokenConfig.sizes.notifs.image implicitHeight: TokenConfig.sizes.notifs.image diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 25f531ec0..57ee06b4f 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -19,36 +19,17 @@ StyledRect { required property DrawerVisibilities visibilities readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) - readonly property var groupProps: { - let count = 0; - let img = ""; - let icon = ""; - let hasCritical = false; - let hasNormal = false; - for (const n of notifs) { - if (!n.closed) { - count++; - if (!img && n.image.length > 0) - img = n.image; - if (!icon && n.appIcon.length > 0) - icon = n.appIcon; - if (n.urgency === NotificationUrgency.Critical) - hasCritical = true; - else if (n.urgency === NotificationUrgency.Normal) - hasNormal = true; - } - } - return { - count, - img, - icon, - urgency: hasCritical ? NotificationUrgency.Critical : hasNormal ? NotificationUrgency.Normal : NotificationUrgency.Low - }; + readonly property list activeNotifs: notifs.filter(n => !n.closed) + readonly property int notifCount: activeNotifs.length + readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? "" + readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" + readonly property int urgency: { + if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical)) + return NotificationUrgency.Critical; + if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal)) + return NotificationUrgency.Normal; + return NotificationUrgency.Low; } - readonly property int notifCount: groupProps.count - readonly property string image: groupProps.img - readonly property string appIcon: groupProps.icon - readonly property int urgency: groupProps.urgency readonly property int nonAnimHeight: { const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); @@ -130,7 +111,7 @@ StyledRect { id: materialIconComp MaterialIcon { - text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) + text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency) color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer font.pointSize: Tokens.font.size.large } @@ -199,7 +180,7 @@ StyledRect { StyledText { animate: true - text: root.notifs.find(n => !n.closed)?.timeStr ?? "" + text: root.activeNotifs[0]?.timeStr ?? "" color: Colours.palette.m3outline font.pointSize: Tokens.font.size.small } diff --git a/services/NotifData.qml b/services/NotifData.qml index 6f470038a..337942436 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -64,7 +64,7 @@ QtObject { if (status !== Image.Ready || width != TokenConfig.sizes.notifs.image || height != TokenConfig.sizes.notifs.image) return; - const cacheKey = notif.appName + notif.summary + notif.id; + const cacheKey = notif.appName + notif.summary + notif.id + notif.image; let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; for (let i = 0; i < cacheKey.length; i++) { ch = cacheKey.charCodeAt(i); @@ -77,7 +77,6 @@ QtObject { h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - Paths; // Screw you qmlls const cache = `${Paths.notifimagecache}/${hash}.png`; CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { notif.image = cache; @@ -122,8 +121,7 @@ QtObject { function onImageChanged(): void { notif.image = notif.notification.image; - if (notif.notification?.image) - notif.dummyImageLoader.active = true; + notif.maybeTriggerDummyImageLoader(); } function onExpireTimeoutChanged(): void { @@ -183,6 +181,11 @@ QtObject { } } + function maybeTriggerDummyImageLoader(): void { + if (image && !image.startsWith("image://icon/") && !image.startsWith(Paths.notifimagecache)) + dummyImageLoader.active = true; + } + function lock(item: Item): void { locks.add(item); } @@ -212,8 +215,7 @@ QtObject { appIcon = notification.appIcon; appName = notification.appName; image = notification.image; - if (notification?.image) - dummyImageLoader.active = true; + maybeTriggerDummyImageLoader(); expireTimeout = notification.expireTimeout; hints = notification.hints; urgency = notification.urgency; From f77ba920ead8b8e82753c1f66ad417cc734631a3 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 01:00:13 +1000 Subject: [PATCH 368/409] feat: add deform scale config option --- modules/drawers/ContentWindow.qml | 2 +- plugin/src/Caelestia/Config/appearanceconfig.hpp | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index aeb968899..dab1d606f 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -303,6 +303,6 @@ StyledWindow { implicitWidth: panel.width implicitHeight: panel.height radius: Tokens.rounding.large - deformScale: deformAmount / 10000 + deformScale: (deformAmount * Config.appearance.deformScale) / 10000 } } diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp index 090ed7a66..3446ea72c 100644 --- a/plugin/src/Caelestia/Config/appearanceconfig.hpp +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -237,6 +237,7 @@ class AppearanceConfig : public ConfigObject { Q_OBJECT QML_ANONYMOUS + CONFIG_PROPERTY(qreal, deformScale, 1) CONFIG_SUBOBJECT(AppearanceRounding, rounding) CONFIG_SUBOBJECT(AppearanceSpacing, spacing) CONFIG_SUBOBJECT(AppearancePadding, padding) From 45d7cdf02c2b8185226412d9743c6fa1b9ea7c9a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 16:59:49 +1000 Subject: [PATCH 369/409] fix: clamp blob radii at half min dimension --- plugin/src/Caelestia/Blobs/blobrect.cpp | 11 ++++++----- plugin/src/Caelestia/Blobs/blobshape.cpp | 3 ++- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp index fa7b4b1ed..15486d83c 100644 --- a/plugin/src/Caelestia/Blobs/blobrect.cpp +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -157,11 +157,12 @@ void BlobRect::setBottomRightRadius(qreal r) { } void BlobRect::cornerRadii(float out[4]) const { - const auto base = static_cast(m_radius); - out[0] = m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base; - out[1] = m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base; - out[2] = m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base; - out[3] = m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base; + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto base = std::min(static_cast(m_radius), maxR); + out[0] = std::min(m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base, maxR); + out[1] = std::min(m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base, maxR); + out[2] = std::min(m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base, maxR); + out[3] = std::min(m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base, maxR); } bool BlobRect::isExcluded(const BlobShape* other) const { diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index 718c03e7f..9fcb622b6 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -96,7 +96,8 @@ void BlobShape::updateCenteredDeformMatrix() { } void BlobShape::cornerRadii(float out[4]) const { - const auto r = static_cast(m_radius); + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto r = std::min(static_cast(m_radius), maxR); out[0] = r; out[1] = r; out[2] = r; From cc775227f764e6c7d2b72833510c67df081eb63a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:13:53 +1000 Subject: [PATCH 370/409] chore: add deformScale to example config --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index f5849cb8d..41c74df95 100644 --- a/README.md +++ b/README.md @@ -264,6 +264,7 @@ For example, to disable the bar on DP-1: { "enabled": true, "appearance": { + "deformScale": 1, "anim": { "durations": { "scale": 1 From 68a336249c1974be69c9270537659e52a03e6592 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:18:31 +1000 Subject: [PATCH 371/409] fix: control center outer rounding --- components/effects/InnerBorder.qml | 2 +- modules/controlcenter/ControlCenter.qml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index d5442e8f9..1b502e299 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -38,7 +38,7 @@ StyledRect { anchors.fill: parent anchors.margins: Tokens.padding.normal - radius: Tokens.rounding.small + radius: Tokens.rounding.normal } } } diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 9bf097f76..c8b435caa 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -12,7 +12,7 @@ Item { id: root required property ShellScreen screen - readonly property int rounding: floating ? 0 : Tokens.rounding.normal + readonly property int rounding: floating ? 0 : Tokens.rounding.large property alias floating: session.floating property alias active: session.active From b94ee8d41bad1ea59395d6184425036fa7121bc5 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 20 Apr 2026 17:28:21 +1000 Subject: [PATCH 372/409] fix: unstretch kuru --- modules/session/Content.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 7ff170900..bf16b1488 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -49,12 +49,12 @@ Column { width: Tokens.sizes.session.button height: Tokens.sizes.session.button sourceSize.width: width - sourceSize.height: height playing: visible asynchronous: true speed: Config.general.sessionGifSpeed source: Paths.absolutePath(Config.paths.sessionGif) + fillMode: AnimatedImage.PreserveAspectFit } SessionButton { From 03d2e985026623816c53194ed320598e8cea1cb1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 22 Apr 2026 22:47:13 +1000 Subject: [PATCH 373/409] feat: use default env + drop expensive fonts --- shell.qml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/shell.qml b/shell.qml index 5ccf61d3d..01ba0f191 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,7 @@ -//@ pragma Env QS_NO_RELOAD_POPUP=1 -//@ pragma Env QSG_RENDER_LOOP=threaded -//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 +//@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1 +//@ pragma DefaultEnv QS_DROP_EXPENSIVE_FONTS=1 +//@ pragma DefaultEnv QSG_RENDER_LOOP=threaded +//@ pragma DefaultEnv QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 import "modules" import "modules/drawers" From f6e2345c90f8bf23a7fe59951999fc8aaa9bf001 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Wed, 22 Apr 2026 21:00:27 +0300 Subject: [PATCH 374/409] feat: add monitor management pane and identifier overlay MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add MonitorsPane (control center) with two-column SplitPaneLayout mirroring the NetworkingPane design: monitor list on the left, per-monitor detail panel on the right with animated PaneTransition - Per-monitor controls: brightness slider, rotation chips (0/90/180/270°), scale slider + quick-pick chips (×1/1.25/1.5/2), and arrangement grid - Add MonitorIdentifier overlay (click or 5s auto-dismiss) showing monitor ID + name as a square rounded-corner badge on each screen - Add Monitors service singleton with batchMessage-based keyword dispatch (fixes 'Invalid dispatcher' by routing through hyprctl keyword not dispatch), findMonitorByName/ById helpers (fixes UntypedObjectModel .find() errors), rotate(), setScale(), arrange(), toggleIdentification(), stopIdentification() - Add MonitorState session object to track selected monitor in detail view - Add dashboard Monitors widget with brightness/scale/rotation/arrangement - Register monitors pane in PaneRegistry and Panes loader - Fix Monitors.qml dashboard: guard refreshRate with ?? 0 to prevent toFixed(undefined) TypeErrors - Fix MonitorIdentifier: rounding.extraLarge -> rounding.large (valid value) --- modules/MonitorIdentifier.qml | 85 ++ modules/controlcenter/PaneRegistry.qml | 6 + modules/controlcenter/Panes.qml | 1 + modules/controlcenter/Session.qml | 1 + .../controlcenter/monitors/MonitorsPane.qml | 844 ++++++++++++++++++ modules/controlcenter/state/MonitorState.qml | 8 + modules/dashboard/Content.qml | 5 + modules/dashboard/Monitors.qml | 221 +++++ modules/dashboard/Tabs.qml | 5 + services/Hyprctl.qml | 47 + services/Monitors.qml | 107 +++ shell.qml | 3 + 12 files changed, 1333 insertions(+) create mode 100644 modules/MonitorIdentifier.qml create mode 100644 modules/controlcenter/monitors/MonitorsPane.qml create mode 100644 modules/controlcenter/state/MonitorState.qml create mode 100644 modules/dashboard/Monitors.qml create mode 100644 services/Hyprctl.qml create mode 100644 services/Monitors.qml diff --git a/modules/MonitorIdentifier.qml b/modules/MonitorIdentifier.qml new file mode 100644 index 000000000..096660a42 --- /dev/null +++ b/modules/MonitorIdentifier.qml @@ -0,0 +1,85 @@ +pragma ComponentBehavior: Bound + +import qs.components +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import Quickshell.Wayland +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +Variants { + id: root + + model: Quickshell.screens + readonly property bool active: Monitors.identifying + + StyledWindow { + id: win + + required property ShellScreen modelData + readonly property var monitor: Hypr.monitorFor(modelData) + + screen: modelData + name: "monitor-identifier" + visible: root.active || identifierRect.opacity > 0 + + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.exclusionMode: ExclusionMode.Ignore + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + // Click anywhere on the overlay to dismiss + MouseArea { + anchors.fill: parent + onClicked: Monitors.stopIdentification() + } + + StyledRect { + id: identifierRect + anchors.centerIn: parent + implicitWidth: Appearance.padding.large * 14 + implicitHeight: Appearance.padding.large * 14 + radius: Appearance.rounding.large + color: Colours.tPalette.m3surfaceContainer + opacity: root.active ? 0.92 : 0 + + // Prevent the MouseArea behind from stealing this click + MouseArea { + anchors.fill: parent + onClicked: Monitors.stopIdentification() + } + + ColumnLayout { + anchors.centerIn: parent + spacing: Appearance.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: win.monitor?.id ?? "?" + font.pointSize: 96 + font.bold: true + color: Colours.palette.m3primary + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: win.monitor?.name ?? "" + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + + } + + Behavior on opacity { + Anim {} + } + } + } +} diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml index c2a0f3840..b3e19c81f 100644 --- a/modules/controlcenter/PaneRegistry.qml +++ b/modules/controlcenter/PaneRegistry.qml @@ -41,6 +41,12 @@ QtObject { readonly property string label: "launcher" readonly property string icon: "apps" readonly property string component: "launcher/LauncherPane.qml" + }, + QtObject { + readonly property string id: "monitors" + readonly property string label: "monitors" + readonly property string icon: "monitor" + readonly property string component: "monitors/MonitorsPane.qml" } ] diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 4a4460ca4..90a369a95 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -6,6 +6,7 @@ import "audio" import "appearance" import "taskbar" import "launcher" +import "monitors" import qs.components import qs.services import qs.config diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 8a8545f0f..3678093b5 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -16,6 +16,7 @@ QtObject { readonly property EthernetState ethernet: EthernetState {} readonly property LauncherState launcher: LauncherState {} readonly property VpnState vpn: VpnState {} + readonly property MonitorState monitor: MonitorState {} onActiveChanged: activeIndex = Math.max(0, panes.indexOf(active)) onActiveIndexChanged: if (panes[activeIndex]) diff --git a/modules/controlcenter/monitors/MonitorsPane.qml b/modules/controlcenter/monitors/MonitorsPane.qml new file mode 100644 index 000000000..8cdc49e9f --- /dev/null +++ b/modules/controlcenter/monitors/MonitorsPane.qml @@ -0,0 +1,844 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import "." +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config +import Quickshell +import Quickshell.Hyprland +import QtQuick +import QtQuick.Layouts + +Item { + id: root + + required property Session session + + anchors.fill: parent + + // ── Two-column split (mirrors NetworkingPane) ────────────────────── + SplitPaneLayout { + id: splitLayout + + anchors.fill: parent + + // ── LEFT: monitor list ───────────────────────────────────────── + leftContent: Component { + StyledFlickable { + id: leftFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } + + ColumnLayout { + id: leftContent + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + // Header row + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.smaller + + StyledText { + text: qsTr("Monitors") + font.pointSize: Appearance.font.size.large + font.weight: 500 + } + + Item { Layout.fillWidth: true } + + // Identify toggle — only button in the header + ToggleButton { + toggled: Monitors.identifying + icon: "tv_signin" + accent: "Secondary" + iconSize: Appearance.font.size.normal + horizontalPadding: Appearance.padding.normal + verticalPadding: Appearance.padding.smaller + tooltip: qsTr("Identify monitors") + + onClicked: Monitors.toggleIdentification() + } + } + + // Subtitle + StyledText { + Layout.fillWidth: true + text: qsTr("%1 display(s) connected").arg(Hyprland.monitors.length) + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + + // Monitor list — use Hyprland.monitors directly as model + Repeater { + model: Hyprland.monitors + + delegate: MonitorListItem { + required property var modelData + required property int index + + Layout.fillWidth: true + + monitor: modelData + active: root.session.monitor.active !== null + && root.session.monitor.active !== undefined + && root.session.monitor.active.id === modelData.id + + onClicked: root.session.monitor.active = modelData + } + } + } + } + } + + // ── RIGHT: detail / overview ─────────────────────────────────── + rightContent: Component { + Item { + id: rightPaneItem + + property var selectedMonitor: root.session.monitor.active + property string paneId: selectedMonitor + ? ("mon:" + (selectedMonitor.name ?? "")) + : "overview" + property Component targetComponent: overviewComponent + property Component nextComponent: overviewComponent + + function resolveComponent(): Component { + return selectedMonitor ? monitorDetailComponent : overviewComponent; + } + + Component.onCompleted: { + targetComponent = resolveComponent(); + nextComponent = targetComponent; + } + + Connections { + target: root.session.monitor + function onActiveChanged(): void { + rightPaneItem.nextComponent = rightPaneItem.resolveComponent(); + } + } + + Loader { + id: rightLoader + anchors.fill: parent + opacity: 1 + scale: 1 + transformOrigin: Item.Center + asynchronous: true + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ] + } + } + } + } + } + + // ── List item component ─────────────────────────────────────────── + component MonitorListItem: Item { + id: listItem + + required property var monitor + required property bool active + + signal clicked() + + implicitHeight: itemRow.implicitHeight + Appearance.padding.normal * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Qt.alpha( + Colours.tPalette.m3surfaceContainer, + listItem.active ? Colours.tPalette.m3surfaceContainer.a : 0 + ) + + StateLayer { + function onClicked(): void { listItem.clicked(); } + } + + RowLayout { + id: itemRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Appearance.padding.normal + spacing: Appearance.spacing.normal + + // Monitor icon badge + StyledRect { + implicitWidth: implicitHeight + implicitHeight: monIcon.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.normal + color: listItem.active + ? Colours.palette.m3primaryContainer + : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { + id: monIcon + anchors.centerIn: parent + text: "monitor" + font.pointSize: Appearance.font.size.large + fill: listItem.active ? 1 : 0 + color: listItem.active + ? Colours.palette.m3onPrimaryContainer + : Colours.palette.m3onSurface + } + } + + // Name + resolution + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + maximumLineCount: 1 + text: listItem.monitor?.name ?? "" + font.weight: listItem.active ? 600 : 400 + } + + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + text: { + const m = listItem.monitor; + if (!m || !m.width || !m.height) return qsTr("Unavailable"); + const rr = m.refreshRate ?? 0; + return qsTr("%1×%2 @ %3 Hz").arg(m.width).arg(m.height).arg(rr.toFixed(0)); + } + } + } + + // Focused badge + StyledRect { + visible: listItem.monitor?.focused ?? false + implicitWidth: focusedLabel.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: focusedLabel.implicitHeight + Appearance.padding.small * 2 + radius: Appearance.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, 0.9) + + StyledText { + id: focusedLabel + anchors.centerIn: parent + text: qsTr("Active") + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onPrimaryContainer + } + } + + // Chevron + MaterialIcon { + text: "chevron_right" + color: Colours.palette.m3outline + opacity: listItem.active ? 1 : 0.4 + } + } + + Behavior on color { CAnim {} } + } + } + + // ── Overview component (no monitor selected) ────────────────────── + Component { + id: overviewComponent + + StyledFlickable { + id: overviewFlickable + flickableDirection: Flickable.VerticalFlick + contentHeight: overviewInner.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: overviewFlickable + } + + ColumnLayout { + id: overviewInner + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + SettingsHeader { + icon: "monitor" + title: qsTr("Monitors") + } + + SectionHeader { + title: qsTr("Display layout") + description: qsTr("Summary of connected displays") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small + + Repeater { + model: Hyprland.monitors + + delegate: PropertyRow { + required property var modelData + required property int index + + label: qsTr("Monitor %1 – %2") + .arg(modelData.id ?? index) + .arg(modelData.name ?? "") + value: { + const m = modelData; + if (!m || !m.width || !m.height) return qsTr("No data"); + return qsTr("%1×%2 @ %3 Hz · pos %4,%5 · ×%6 scale") + .arg(m.width).arg(m.height) + .arg((m.refreshRate ?? 0).toFixed(0)) + .arg(m.x ?? 0).arg(m.y ?? 0) + .arg((m.scale ?? 1).toFixed(2)); + } + showTopMargin: index > 0 + } + } + } + + SectionHeader { + title: qsTr("Quick controls") + description: qsTr("Select a monitor on the left to configure it") + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "tv_signin" + font.pointSize: Appearance.font.size.large + color: Colours.palette.m3onSurfaceVariant + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + StyledText { + text: qsTr("Identify displays") + font.pointSize: Appearance.font.size.normal + } + StyledText { + text: qsTr("Show monitor IDs on each screen") + color: Colours.palette.m3outline + font.pointSize: Appearance.font.size.small + } + } + + ToggleButton { + toggled: Monitors.identifying + icon: Monitors.identifying ? "visibility_off" : "visibility" + accent: "Secondary" + onClicked: Monitors.toggleIdentification() + } + } + } + } + } + } + + // ── Per-monitor detail component ────────────────────────────────── + Component { + id: monitorDetailComponent + + StyledFlickable { + id: detailFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: detailInner.implicitHeight + + StyledScrollBar.vertical: StyledScrollBar { + flickable: detailFlickable + } + + readonly property var mon: root.session.monitor.active + readonly property var brightnessMon: mon ? Brightness.getMonitor(mon.name) : null + + ColumnLayout { + id: detailInner + + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + // ── Header ────────────────────────────────────────── + ConnectionHeader { + icon: "monitor" + title: detailFlickable.mon?.name ?? qsTr("Monitor") + } + + // ── Brightness ─────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true + visible: detailFlickable.brightnessMon !== null + && detailFlickable.brightnessMon !== undefined + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Brightness") + description: qsTr("Adjust display brightness") + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + MaterialIcon { + text: (detailFlickable.brightnessMon?.brightness ?? 0) > 0.5 + ? "brightness_high" : "brightness_low" + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + StyledSlider { + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + from: 0; to: 1; stepSize: 0.01 + value: detailFlickable.brightnessMon?.brightness ?? 0 + onMoved: detailFlickable.brightnessMon?.setBrightness(value) + } + + StyledText { + text: qsTr("%1%").arg( + Math.round((detailFlickable.brightnessMon?.brightness ?? 0) * 100)) + Layout.preferredWidth: 38 + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + } + } + } + } + + // ── Rotation ───────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Rotation") + description: qsTr("Rotate this display") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Repeater { + model: [ + { label: qsTr("0°"), transform: 0, angle: 0 }, + { label: qsTr("90°"), transform: 1, angle: 90 }, + { label: qsTr("180°"), transform: 2, angle: 180 }, + { label: qsTr("270°"), transform: 3, angle: 270 } + ] + + delegate: RotationChip { + required property var modelData + required property int index + + Layout.fillWidth: true + chipLabel: modelData.label + chipAngle: modelData.angle + isActive: (detailFlickable.mon?.transform ?? 0) === modelData.transform + onClicked: { + if (detailFlickable.mon) + Monitors.rotate(detailFlickable.mon.name, modelData.angle); + } + } + } + } + } + } + + // ── Scale ──────────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Scale") + description: qsTr("DPI scaling factor for this display") + } + + SectionContainer { + contentSpacing: Appearance.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + MaterialIcon { + text: "zoom_in" + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + StyledSlider { + id: scaleSlider + Layout.fillWidth: true + implicitHeight: Appearance.padding.normal * 3 + from: 0.5; to: 3.0; stepSize: 0.25 + value: detailFlickable.mon?.scale ?? 1 + + onMoved: { + if (detailFlickable.mon) + Monitors.setScale(detailFlickable.mon.name, value); + } + } + + StyledText { + text: qsTr("×%1").arg((detailFlickable.mon?.scale ?? 1).toFixed(2)) + Layout.preferredWidth: 42 + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3outline + } + } + + // Quick-pick chips: 1×, 1.25×, 1.5×, 2× + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + Repeater { + model: [1.0, 1.25, 1.5, 2.0] + + delegate: StyledRect { + required property var modelData + required property int index + + Layout.fillWidth: true + implicitHeight: scaleChipLabel.implicitHeight + Appearance.padding.normal * 2 + radius: Appearance.rounding.full + + readonly property bool isActive: + Math.abs((detailFlickable.mon?.scale ?? 1) - modelData) < 0.01 + + color: isActive + ? Colours.palette.m3secondaryContainer + : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) + + StateLayer { + color: parent.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant + function onClicked(): void { + if (detailFlickable.mon) + Monitors.setScale(detailFlickable.mon.name, modelData); + } + } + + StyledText { + id: scaleChipLabel + anchors.centerIn: parent + text: qsTr("×%1").arg(modelData.toFixed(2)) + font.pointSize: Appearance.font.size.small + color: parent.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant + } + + Behavior on color { CAnim {} } + } + } + } + } + } + + // ── Arrangement ────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true + visible: Hyprland.monitors.length > 1 + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Arrangement") + description: qsTr("Position this display relative to another") + } + + // One card per OTHER monitor — use visible to skip self + Repeater { + model: Hyprland.monitors + + delegate: SectionContainer { + required property var modelData + required property int index + + Layout.fillWidth: true + contentSpacing: Appearance.spacing.small + + // Hide the current monitor's own entry without JS filter + visible: detailFlickable.mon !== null + && detailFlickable.mon !== undefined + && modelData.id !== detailFlickable.mon.id + height: visible ? implicitHeight : 0 + + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + MaterialIcon { + text: "tv" + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Relative to Monitor %1 (%2)") + .arg(modelData.id ?? 0) + .arg(modelData.name ?? "") + font.pointSize: Appearance.font.size.normal + } + } + + GridLayout { + Layout.fillWidth: true + columns: 4 + columnSpacing: Appearance.spacing.small + rowSpacing: Appearance.spacing.small + + Repeater { + model: [ + { label: qsTr("Left"), pos: "left", icon: "arrow_back" }, + { label: qsTr("Right"), pos: "right", icon: "arrow_forward" }, + { label: qsTr("Above"), pos: "top", icon: "arrow_upward" }, + { label: qsTr("Below"), pos: "bottom", icon: "arrow_downward" } + ] + + delegate: ArrangeButton { + required property var modelData + required property int index + + Layout.fillWidth: true + btnIcon: modelData.icon + btnLabel: modelData.label + onClicked: { + if (detailFlickable.mon) + Monitors.arrange( + detailFlickable.mon.name, + modelData.pos, + parent.parent.parent.modelData.id + ); + } + } + } + } + } + } + } + + // ── Display information ─────────────────────────────── + ColumnLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.normal + + SectionHeader { + title: qsTr("Display information") + description: qsTr("Hardware and layout details") + } + + SectionContainer { + contentSpacing: Appearance.spacing.small / 2 + + PropertyRow { + label: qsTr("Name") + value: detailFlickable.mon?.name ?? qsTr("Unknown") + } + PropertyRow { + showTopMargin: true + label: qsTr("Monitor ID") + value: detailFlickable.mon != null ? String(detailFlickable.mon.id ?? "—") : "—" + } + PropertyRow { + showTopMargin: true + label: qsTr("Resolution") + value: detailFlickable.mon?.width && detailFlickable.mon?.height + ? qsTr("%1 × %2 px").arg(detailFlickable.mon.width).arg(detailFlickable.mon.height) + : qsTr("N/A") + } + PropertyRow { + showTopMargin: true + label: qsTr("Refresh rate") + value: detailFlickable.mon?.refreshRate != null + ? qsTr("%1 Hz").arg((detailFlickable.mon.refreshRate).toFixed(3)) + : qsTr("N/A") + } + PropertyRow { + showTopMargin: true + label: qsTr("Position") + value: detailFlickable.mon != null + ? qsTr("x: %1, y: %2").arg(detailFlickable.mon.x ?? 0).arg(detailFlickable.mon.y ?? 0) + : qsTr("N/A") + } + PropertyRow { + showTopMargin: true + label: qsTr("Scale") + value: detailFlickable.mon?.scale != null + ? qsTr("×%1").arg((detailFlickable.mon.scale).toFixed(2)) + : qsTr("N/A") + } + PropertyRow { + showTopMargin: true + label: qsTr("Transform") + value: { + const t = detailFlickable.mon?.transform ?? 0; + return ["Normal (0°)", "90°", "180°", "270°", + "Flipped", "Flipped 90°", "Flipped 180°", "Flipped 270°"][t] + ?? qsTr("Unknown"); + } + } + PropertyRow { + showTopMargin: true + label: qsTr("Make / Model") + value: { + const parts = [detailFlickable.mon?.make, detailFlickable.mon?.model] + .filter(v => v && v.length > 0); + return parts.length > 0 ? parts.join(" ") : qsTr("Unknown"); + } + } + PropertyRow { + showTopMargin: true + label: qsTr("Serial") + value: detailFlickable.mon?.serial || qsTr("Unknown") + } + PropertyRow { + showTopMargin: true + label: qsTr("Focused") + value: (detailFlickable.mon?.focused ?? false) ? qsTr("Yes") : qsTr("No") + } + } + } + } + } + } + + // ── Reusable sub-components ─────────────────────────────────────── + + component RotationChip: Item { + id: chip + required property string chipLabel + required property int chipAngle + required property bool isActive + signal clicked() + + implicitHeight: chipContent.implicitHeight + Appearance.padding.normal * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.full + color: chip.isActive + ? Colours.palette.m3secondaryContainer + : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) + + StateLayer { + color: chip.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant + function onClicked(): void { chip.clicked(); } + } + + ColumnLayout { + id: chipContent + anchors.centerIn: parent + spacing: 2 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "screen_rotation" + rotation: chip.chipAngle + font.pointSize: Appearance.font.size.normal + color: chip.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant + Behavior on rotation { Anim {} } + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: chip.chipLabel + font.pointSize: Appearance.font.size.small + color: chip.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant + } + } + + Behavior on color { CAnim {} } + } + } + + component ArrangeButton: Item { + id: arrangeBtn + required property string btnIcon + required property string btnLabel + signal clicked() + + implicitHeight: btnContent.implicitHeight + Appearance.padding.normal * 2 + + StyledRect { + anchors.fill: parent + radius: Appearance.rounding.normal + color: Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) + + StateLayer { + color: Colours.palette.m3onSurfaceVariant + function onClicked(): void { arrangeBtn.clicked(); } + } + + ColumnLayout { + id: btnContent + anchors.centerIn: parent + spacing: 2 + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: arrangeBtn.btnIcon + font.pointSize: Appearance.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: arrangeBtn.btnLabel + font.pointSize: Appearance.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } + } + } +} diff --git a/modules/controlcenter/state/MonitorState.qml b/modules/controlcenter/state/MonitorState.qml new file mode 100644 index 000000000..12e290b67 --- /dev/null +++ b/modules/controlcenter/state/MonitorState.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + id: root + + // The currently selected HyprlandMonitor object (null = show settings overview) + property var active: null +} diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index 1cc960a47..f95b7d782 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -111,6 +111,11 @@ Item { index: 3 sourceComponent: Weather {} } + + Pane { + index: 4 + sourceComponent: Monitors {} + } } Behavior on contentX { diff --git a/modules/dashboard/Monitors.qml b/modules/dashboard/Monitors.qml new file mode 100644 index 000000000..e1bfd4a0c --- /dev/null +++ b/modules/dashboard/Monitors.qml @@ -0,0 +1,221 @@ +import qs.components +import qs.components.controls +import qs.services +import qs.config +import Quickshell +import QtQuick +import QtQuick.Layouts + +ColumnLayout { + id: root + + spacing: Appearance.spacing.large + + RowLayout { + Layout.fillWidth: true + Layout.margins: Appearance.padding.normal + + StyledText { + text: qsTr("Monitors") + font.pointSize: Appearance.font.size.extraLarge + Layout.fillWidth: true + } + + IconTextButton { + icon: "info" + text: qsTr("Identify") + toggle: true + checked: Monitors.identifying + onClicked: Monitors.toggleIdentification() + } + } + + Flickable { + Layout.fillWidth: true + Layout.fillHeight: true + contentHeight: monitorsLayout.implicitHeight + clip: true + + ColumnLayout { + id: monitorsLayout + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.spacing.normal + + Repeater { + model: Hyprctl.monitors + + delegate: StyledRect { + id: monitorDelegate + Layout.fillWidth: true + implicitHeight: monitorContent.implicitHeight + Appearance.padding.large * 2 + color: Colours.tPalette.m3surfaceContainerHigh + radius: Appearance.rounding.large + + readonly property var mon: modelData + readonly property var brightnessMon: Brightness.getMonitor(mon.name) + + ColumnLayout { + id: monitorContent + anchors.fill: parent + anchors.margins: Appearance.padding.large + spacing: Appearance.spacing.medium + + RowLayout { + Layout.fillWidth: true + MaterialIcon { + text: "monitor" + color: Colours.palette.m3primary + } + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + StyledText { + text: `${mon.name} - ${mon.make} ${mon.model}` + font.pointSize: Appearance.font.size.large + Layout.fillWidth: true + } + StyledText { + text: `${mon.width}x${mon.height}@${(mon.refreshRate ?? 0).toFixed(2)}Hz` + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Appearance.font.size.small + } + } + StyledText { + text: `ID: ${mon.id}` + color: Colours.palette.m3onSurfaceVariant + } + } + + // Brightness + RowLayout { + Layout.fillWidth: true + visible: !!brightnessMon + + MaterialIcon { + text: "brightness_medium" + font.pointSize: Appearance.font.size.normal + } + + StyledSlider { + Layout.fillWidth: true + value: brightnessMon?.brightness ?? 0 + onMoved: if (brightnessMon) brightnessMon.setBrightness(value) + } + + StyledText { + text: `${Math.round((brightnessMon?.brightness ?? 0) * 100)}%` + Layout.preferredWidth: 40 + } + } + + // Scaling + RowLayout { + Layout.fillWidth: true + + MaterialIcon { + text: "zoom_in" + font.pointSize: Appearance.font.size.normal + } + + StyledSlider { + Layout.fillWidth: true + from: 0.5 + to: 3.0 + value: mon.scale + onMoved: Monitors.setScale(mon.name, value) + } + + StyledText { + text: `${mon.scale.toFixed(2)}x` + Layout.preferredWidth: 40 + } + } + + // Refresh Rate + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Refresh Rate") + Layout.fillWidth: true + } + + CustomSpinBox { + id: rrSelector + min: 10 + max: 1000 + step: 1 + value: mon.refreshRate + onValueModified: val => Monitors.setRefreshRate(mon.name, val) + } + } + + // Rotation + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Rotation") + Layout.fillWidth: true + } + + Repeater { + model: [ + { label: "0°", val: 0, icon: "screen_rotation" }, + { label: "90°", val: 1, icon: "screen_rotation" }, + { label: "180°", val: 2, icon: "screen_rotation" }, + { label: "270°", val: 3, icon: "screen_rotation" } + ] + + delegate: IconButton { + icon: modelData.icon + toggle: true + checked: mon.transform === modelData.val + onClicked: Monitors.rotate(mon.name, modelData.val * 90) + } + } + } + + // Arrangement + RowLayout { + Layout.fillWidth: true + spacing: Appearance.spacing.small + + StyledText { + text: qsTr("Position relative to:") + Layout.fillWidth: true + } + + CustomSpinBox { + id: targetMonSelector + min: 0 + max: Math.max(0, (Hyprctl.monitors?.length ?? 1) - 1) + value: 0 + } + + IconButton { + icon: "arrow_back" + onClicked: Monitors.arrange(mon.name, "left", targetMonSelector.value) + } + IconButton { + icon: "arrow_forward" + onClicked: Monitors.arrange(mon.name, "right", targetMonSelector.value) + } + IconButton { + icon: "arrow_upward" + onClicked: Monitors.arrange(mon.name, "top", targetMonSelector.value) + } + IconButton { + icon: "arrow_downward" + onClicked: Monitors.arrange(mon.name, "bottom", targetMonSelector.value) + } + } + } + } + } + } + } +} diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index 98ea880e5..eef01be73 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -50,6 +50,11 @@ Item { text: qsTr("Weather") } + Tab { + iconName: "monitor" + text: qsTr("Monitors") + } + // Tab { // iconName: "workspaces" // text: qsTr("Workspaces") diff --git a/services/Hyprctl.qml b/services/Hyprctl.qml new file mode 100644 index 000000000..8e49d3808 --- /dev/null +++ b/services/Hyprctl.qml @@ -0,0 +1,47 @@ +pragma Singleton + +import Quickshell +import Quickshell.Io +import QtQuick + +Singleton { + id: root + + property list monitors: [] + + function update(): void { + proc.running = true; + } + + readonly property Process proc: Process { + command: ["hyprctl", "monitors", "-j"] + stdout: StdioCollector { + onStreamFinished: { + try { + root.monitors = JSON.parse(text); + } catch (e) { + console.error("Hyprctl: failed to parse monitors JSON", e); + } + } + } + } + + Timer { + interval: 2000 + running: true + repeat: true + onTriggered: root.update() + } + + Component.onCompleted: update() + + // Refresh when Hyprland reports changes + Connections { + target: Hyprland + function onRawEvent(event: HyprlandEvent): void { + if (event.name.includes("mon")) { + root.update(); + } + } + } +} diff --git a/services/Monitors.qml b/services/Monitors.qml new file mode 100644 index 000000000..e64d14e5f --- /dev/null +++ b/services/Monitors.qml @@ -0,0 +1,107 @@ +pragma Singleton + +import qs.services +import Quickshell +import QtQuick + +Singleton { + id: root + + property bool identifying: false + + // Auto-dismiss identify overlay after 5 seconds + Timer { + id: identifyTimer + interval: 5000 + onTriggered: root.identifying = false + } + + function toggleIdentification(): void { + identifying = !identifying; + if (identifying) + identifyTimer.restart(); + else + identifyTimer.stop(); + } + + function stopIdentification(): void { + identifying = false; + identifyTimer.stop(); + } + + // Safely iterate UntypedObjectModel — .find() doesn't work on it + function findMonitorByName(name: string): var { + for (let i = 0; i < Hypr.monitors.length; i++) { + if (Hypr.monitors[i].name === name) + return Hypr.monitors[i]; + } + return null; + } + + function findMonitorById(id: int): var { + for (let i = 0; i < Hypr.monitors.length; i++) { + if (Hypr.monitors[i].id === id) + return Hypr.monitors[i]; + } + return null; + } + + // Build the monitor string Hyprland expects: + // NAME,WIDTHxHEIGHT@RATE,XxY,SCALE[,transform,N] + function monitorStr(mon: var, overrideScale: real, overrideTransform: int): string { + const scale = overrideScale >= 0 ? overrideScale : (mon.scale || 1); + const transform = overrideTransform >= 0 ? overrideTransform : (mon.transform || 0); + const rr = (mon.refreshRate || 60).toFixed(3); + let s = `${mon.name},${mon.width}x${mon.height}@${rr},${mon.x}x${mon.y},${scale}`; + if (transform !== 0) + s += `,transform,${transform}`; + return s; + } + + // Use batchMessage (hyprctl keyword), NOT dispatch (hyprctl dispatch) + // "keyword" is a config command, not a dispatcher action. + function sendKeyword(monStr: string): void { + Hypr.extras.batchMessage([`keyword monitor ${monStr}`]); + } + + function arrange(monitorName: string, pos: string, relativeToId: int): void { + const target = findMonitorById(relativeToId); + const moving = findMonitorByName(monitorName); + if (!target || !moving) return; + + let x = target.x; + let y = target.y; + + const targetW = Math.round(target.width / (target.scale || 1)); + const targetH = Math.round(target.height / (target.scale || 1)); + const movingW = Math.round(moving.width / (moving.scale || 1)); + const movingH = Math.round(moving.height / (moving.scale || 1)); + + if (pos === "left") x -= movingW; + else if (pos === "right") x += targetW; + else if (pos === "top") y -= movingH; + else if (pos === "bottom") y += targetH; + + sendKeyword(monitorStr(moving, moving.scale || 1, moving.transform || 0) + .replace(`${moving.x}x${moving.y}`, `${Math.round(x)}x${Math.round(y)}`)); + } + + function rotate(monitorName: string, angle: int): void { + const mon = findMonitorByName(monitorName); + if (!mon) return; + + let transform = 0; + if (angle === 90) transform = 1; + else if (angle === 180) transform = 2; + else if (angle === 270) transform = 3; + + sendKeyword(monitorStr(mon, mon.scale || 1, transform)); + } + + function setScale(monitorName: string, scale: real): void { + const mon = findMonitorByName(monitorName); + if (!mon) return; + const s = Math.max(0.5, Math.min(3.0, scale)); + sendKeyword(monitorStr(mon, s, mon.transform || 0)); + } +} diff --git a/shell.qml b/shell.qml index 3ce777699..e93b5c524 100644 --- a/shell.qml +++ b/shell.qml @@ -22,4 +22,7 @@ ShellRoot { IdleMonitors { lock: lock } + MonitorIdentifier { + id: monitorIdentifier + } } From 9be727dbe1fae94558f625c898659a4dbdb29ce4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:12:55 +1000 Subject: [PATCH 375/409] fix: interaction blocking at edges when fullscreen Also completely disable interaction when fullscreen --- modules/drawers/ContentWindow.qml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index dab1d606f..5fbacd416 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -61,11 +61,7 @@ StyledWindow { WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - mask: Regions { - bar: bar - panels: panels - win: root - } + mask: hasFullscreen ? emptyRegion : regions anchors.top: true anchors.bottom: true @@ -90,6 +86,18 @@ StyledWindow { } } + Region { + id: emptyRegion + } + + Regions { + id: regions + + bar: bar + panels: panels + win: root + } + HyprlandFocusGrab { id: focusGrab From 53dd959f5e07977662c7f2fc70656b30123cec01 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sun, 26 Apr 2026 15:13:28 +1000 Subject: [PATCH 376/409] feat: always expire notifs if fullscreen Also faster expire timeout when fullscreen --- plugin/src/Caelestia/Config/notifsconfig.hpp | 1 + services/NotifData.qml | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/plugin/src/Caelestia/Config/notifsconfig.hpp b/plugin/src/Caelestia/Config/notifsconfig.hpp index eef360fe4..cdac94bdf 100644 --- a/plugin/src/Caelestia/Config/notifsconfig.hpp +++ b/plugin/src/Caelestia/Config/notifsconfig.hpp @@ -13,6 +13,7 @@ class NotifsConfig : public ConfigObject { CONFIG_GLOBAL_PROPERTY(bool, expire, true) CONFIG_GLOBAL_PROPERTY(QString, fullscreen, QStringLiteral("on")) CONFIG_GLOBAL_PROPERTY(int, defaultExpireTimeout, 5000) + CONFIG_GLOBAL_PROPERTY(int, fullscreenExpireTimeout, 2000) CONFIG_PROPERTY(qreal, clearThreshold, 0.3) CONFIG_PROPERTY(int, expandThreshold, 20) CONFIG_GLOBAL_PROPERTY(bool, actionOnClick, false) diff --git a/services/NotifData.qml b/services/NotifData.qml index 337942436..3c6ae23b6 100644 --- a/services/NotifData.qml +++ b/services/NotifData.qml @@ -39,11 +39,22 @@ QtObject { property bool hasActionIcons property list actions + readonly property bool hasFullscreen: { + const monitor = Hypr.focusedMonitor; + const specialName = monitor?.lastIpcObject.specialWorkspace?.name; + if (specialName) { + const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); + return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + readonly property Timer timer: Timer { running: true - interval: notif.expireTimeout > 0 ? notif.expireTimeout : GlobalConfig.notifs.defaultExpireTimeout + interval: notif.expireTimeout > 0 ? notif.expireTimeout : notif.hasFullscreen ? GlobalConfig.notifs.fullscreenExpireTimeout : GlobalConfig.notifs.defaultExpireTimeout onTriggered: { - if (GlobalConfig.notifs.expire) + // Always expire if the active workspace has a fullscreen window + if (GlobalConfig.notifs.expire || notif.hasFullscreen) notif.popup = false; } } From 411e013ea01d64b8dc0fdbf334d2ab8589318bf2 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:04:50 +1000 Subject: [PATCH 377/409] feat: use image provider for caching --- components/images/CachingImage.qml | 24 +-- plugin/src/Caelestia/CMakeLists.txt | 1 + plugin/src/Caelestia/Images/CMakeLists.txt | 10 + .../Caelestia/Images/cachingimageprovider.cpp | 180 ++++++++++++++++++ .../Caelestia/Images/cachingimageprovider.hpp | 23 +++ plugin/src/Caelestia/Images/iutils.cpp | 39 ++++ plugin/src/Caelestia/Images/iutils.hpp | 24 +++ 7 files changed, 282 insertions(+), 19 deletions(-) create mode 100644 plugin/src/Caelestia/Images/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Images/cachingimageprovider.cpp create mode 100644 plugin/src/Caelestia/Images/cachingimageprovider.hpp create mode 100644 plugin/src/Caelestia/Images/iutils.cpp create mode 100644 plugin/src/Caelestia/Images/iutils.hpp diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index 5c5f8bbbe..167f6b9b1 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -1,28 +1,14 @@ import QtQuick -import Quickshell -import Caelestia.Internal -import qs.utils +import Caelestia.Images Image { id: root - property alias path: manager.path + property string path asynchronous: true fillMode: Image.PreserveAspectCrop - - Connections { - function onDevicePixelRatioChanged(): void { - manager.updateSource(); - } - - target: QsWindow.window - } - - CachingImageManager { - id: manager - - item: root - cacheDir: Qt.resolvedUrl(Paths.imagecache) - } + source: IUtils.urlForPath(path, fillMode) + sourceSize.width: width + sourceSize.height: height } diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index 6f4ca9804..6e73f4fe3 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -79,3 +79,4 @@ add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) add_subdirectory(Blobs) +add_subdirectory(Images) diff --git a/plugin/src/Caelestia/Images/CMakeLists.txt b/plugin/src/Caelestia/Images/CMakeLists.txt new file mode 100644 index 000000000..10707ae9d --- /dev/null +++ b/plugin/src/Caelestia/Images/CMakeLists.txt @@ -0,0 +1,10 @@ +qml_module(caelestia-images + URI Caelestia.Images + SOURCES + cachingimageprovider.cpp + iutils.cpp + LIBRARIES + Qt::Gui + Qt::Quick + Qt::Concurrent +) diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp new file mode 100644 index 000000000..ed7260186 --- /dev/null +++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp @@ -0,0 +1,180 @@ +#include "cachingimageprovider.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg) + +namespace caelestia::images { + +namespace { + +const QString& cacheDir() { + static const QString s_dir = [] { + QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); + if (cache.isEmpty()) + cache = QDir::homePath() + QStringLiteral("/.cache"); + return cache + QStringLiteral("/caelestia/imagecache"); + }(); + return s_dir; +} + +QString sha256sum(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(lcCProv).noquote() << "sha256sum: failed to open" << path; + return {}; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(&file); + file.close(); + + return hash.result().toHex(); +} + +QString fillSuffix(CachingImageProvider::FillMode fillMode) { + switch (fillMode) { + case CachingImageProvider::FillMode::Crop: + return QStringLiteral("crop"); + case CachingImageProvider::FillMode::Fit: + return QStringLiteral("fit"); + default: + return QStringLiteral("stretch"); + } +} + +class CachingImageResponse final : public QQuickImageResponse, public QRunnable { +public: + CachingImageResponse(const QString& id, const QSize& requestedSize, CachingImageProvider::FillMode fillMode) + : m_id(id) + , m_requestedSize(requestedSize) + , m_fillMode(fillMode) { + setAutoDelete(false); + } + + [[nodiscard]] QQuickTextureFactory* textureFactory() const override { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + + [[nodiscard]] QString errorString() const override { return m_error; } + + void run() override { + process(); + emit finished(); + } + +private: + void process() { + QString path = QString::fromUtf8(m_id.toUtf8().percentDecoded()); + if (!path.startsWith(QLatin1Char('/'))) + path.prepend(QLatin1Char('/')); + + if (!QFileInfo::exists(path)) { + m_error = QStringLiteral("Source file does not exist: ") + path; + qCWarning(lcCProv).noquote() << m_error; + return; + } + + // Get size from requested size, or the source's original size + QSize size = m_requestedSize; + if (size.width() <= 0 || size.height() <= 0) { + const QImageReader reader(path); + size = reader.size(); + if (!size.isValid() || size.isEmpty()) { + m_error = QStringLiteral("Could not determine size for: ") + path; + qCWarning(lcCProv).noquote() << m_error; + return; + } + } + + const QString sha = sha256sum(path); + if (sha.isEmpty()) { + m_error = QStringLiteral("Failed to hash: ") + path; + return; + } + + // clang-format off + const QString filename = QStringLiteral("%1@%2x%3-%4.png") + .arg(sha).arg(size.width()).arg(size.height()).arg(fillSuffix(m_fillMode)); + // clang-format on + const QString cache = cacheDir() + QLatin1Char('/') + filename; + + // Check cache, if it already exists, set and return + QImageReader cacheReader(cache); + if (cacheReader.canRead()) { + m_image = cacheReader.read(); + if (!m_image.isNull()) + return; + } + + QImage image(path); + if (image.isNull()) { + m_error = QStringLiteral("Failed to decode: ") + path; + qCWarning(lcCProv).noquote() << m_error; + return; + } + + image.convertTo(QImage::Format_ARGB32); + + // Scale to requested size + switch (m_fillMode) { + case CachingImageProvider::FillMode::Crop: + image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + break; + case CachingImageProvider::FillMode::Fit: + image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + break; + case CachingImageProvider::FillMode::Stretch: + image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + break; + } + + if (m_fillMode == CachingImageProvider::FillMode::Stretch) { + m_image = image; + } else { + // Crop or fit + QImage canvas(size, QImage::Format_ARGB32); + canvas.fill(Qt::transparent); + + QPainter painter(&canvas); + painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); + painter.end(); + + m_image = canvas; + } + + const QString parent = QFileInfo(cache).absolutePath(); + if (QDir().mkpath(parent) && m_image.save(cache)) + qCDebug(lcCProv).noquote() << "Saved to" << cache; + else + qCWarning(lcCProv).noquote() << "Failed to save to" << cache; + } + + QString m_id; + QSize m_requestedSize; + CachingImageProvider::FillMode m_fillMode; + QImage m_image; + QString m_error; +}; + +} // namespace + +CachingImageProvider::CachingImageProvider(FillMode fillMode) + : m_fillMode(fillMode) {} + +QQuickImageResponse* CachingImageProvider::requestImageResponse(const QString& id, const QSize& requestedSize) { + auto* const response = new CachingImageResponse(id, requestedSize, m_fillMode); + QThreadPool::globalInstance()->start(response); + return response; +} + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.hpp b/plugin/src/Caelestia/Images/cachingimageprovider.hpp new file mode 100644 index 000000000..9e9c9772c --- /dev/null +++ b/plugin/src/Caelestia/Images/cachingimageprovider.hpp @@ -0,0 +1,23 @@ +#pragma once + +#include + +namespace caelestia::images { + +class CachingImageProvider : public QQuickAsyncImageProvider { +public: + enum class FillMode { + Crop, + Fit, + Stretch + }; + + explicit CachingImageProvider(FillMode fillMode); + + QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override; + +private: + FillMode m_fillMode; +}; + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/iutils.cpp b/plugin/src/Caelestia/Images/iutils.cpp new file mode 100644 index 000000000..aeee7684a --- /dev/null +++ b/plugin/src/Caelestia/Images/iutils.cpp @@ -0,0 +1,39 @@ +#include "iutils.hpp" + +#include "cachingimageprovider.hpp" + +namespace caelestia::images { + +IUtils* IUtils::create(QQmlEngine* engine, QJSEngine* jsEngine) { + Q_UNUSED(jsEngine); + + engine->addImageProvider(QStringLiteral("ccache"), new CachingImageProvider(CachingImageProvider::FillMode::Crop)); + engine->addImageProvider(QStringLiteral("fcache"), new CachingImageProvider(CachingImageProvider::FillMode::Fit)); + engine->addImageProvider( + QStringLiteral("scache"), new CachingImageProvider(CachingImageProvider::FillMode::Stretch)); + + return new IUtils(engine); +} + +QUrl IUtils::urlForPath(const QString& path, int fillMode) { + QString prefix; + switch (fillMode) { + case 1: // Image.PreserveAspectFit + prefix = QStringLiteral("fcache"); + break; + case 2: // Image.PreserveAspectCrop + prefix = QStringLiteral("ccache"); + break; + default: // Image.Stretch or any other ones + prefix = QStringLiteral("scache"); + break; + } + + QUrl url; + url.setScheme(QStringLiteral("image")); + url.setHost(prefix); + url.setPath(path.startsWith(QLatin1Char('/')) ? path : QLatin1Char('/') + path); + return url; +} + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/iutils.hpp b/plugin/src/Caelestia/Images/iutils.hpp new file mode 100644 index 000000000..2187c8d02 --- /dev/null +++ b/plugin/src/Caelestia/Images/iutils.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::images { + +class IUtils : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + static IUtils* create(QQmlEngine* engine, QJSEngine* jsEngine); + + Q_INVOKABLE static QUrl urlForPath(const QString& path, int fillMode); + +private: + explicit IUtils(QObject* parent = nullptr) + : QObject(parent) {}; +}; + +} // namespace caelestia::images From 4f1f609b55936daa7d6a981e90da53217b1ada41 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:51:06 +1000 Subject: [PATCH 378/409] fix: use QSaveFile for atomic writes --- .../Caelestia/Images/cachingimageprovider.cpp | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp index ed7260186..8491b6410 100644 --- a/plugin/src/Caelestia/Images/cachingimageprovider.cpp +++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg) @@ -152,11 +153,20 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable m_image = canvas; } + // Save to cache const QString parent = QFileInfo(cache).absolutePath(); - if (QDir().mkpath(parent) && m_image.save(cache)) - qCDebug(lcCProv).noquote() << "Saved to" << cache; - else - qCWarning(lcCProv).noquote() << "Failed to save to" << cache; + if (!QDir().mkpath(parent)) { + qCWarning(lcCProv).noquote() << "Failed to create cache dir" << parent; + return; + } + + QSaveFile saveFile(cache); + if (!saveFile.open(QIODevice::WriteOnly) || !m_image.save(&saveFile, "PNG") || !saveFile.commit()) { + qCWarning(lcCProv).noquote() << "Failed to save to" << cache << ":" << saveFile.errorString(); + return; + } + + qCDebug(lcCProv).noquote() << "Saved to" << cache; } QString m_id; From 1f7aeec8c44e51c2856ef3c00b64284165409fb7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:51:42 +1000 Subject: [PATCH 379/409] fix: specify sourceSize as a single prop So we don't get 2 updates when width/height change --- components/images/CachingImage.qml | 3 +-- modules/launcher/items/WallpaperItem.qml | 4 +--- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index 167f6b9b1..e782b8821 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -9,6 +9,5 @@ Image { asynchronous: true fillMode: Image.PreserveAspectCrop source: IUtils.urlForPath(path, fillMode) - sourceSize.width: width - sourceSize.height: height + sourceSize: Qt.size(width, height) } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 58be068d7..3f2143aef 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -63,11 +63,9 @@ Item { } CachingImage { + anchors.fill: parent path: root.modelData.path smooth: !root.PathView.view.moving - cache: true - - anchors.fill: parent } } From b4d490a9bc18e64ab4b313af43e4591367e7a9d4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 20:56:35 +1000 Subject: [PATCH 380/409] fix: use original image if requested size is invalid --- .../Caelestia/Images/cachingimageprovider.cpp | 28 ++++++++----------- 1 file changed, 12 insertions(+), 16 deletions(-) diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp index 8491b6410..2bd1c8b8f 100644 --- a/plugin/src/Caelestia/Images/cachingimageprovider.cpp +++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp @@ -85,16 +85,11 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable return; } - // Get size from requested size, or the source's original size - QSize size = m_requestedSize; - if (size.width() <= 0 || size.height() <= 0) { - const QImageReader reader(path); - size = reader.size(); - if (!size.isValid() || size.isEmpty()) { - m_error = QStringLiteral("Could not determine size for: ") + path; - qCWarning(lcCProv).noquote() << m_error; - return; - } + // Use original image if requested size is invalid + if (m_requestedSize.width() <= 0 || m_requestedSize.height() <= 0) { + m_image = QImage(path); + qCDebug(lcCProv) << "Given source size is invalid, not caching."; + return; } const QString sha = sha256sum(path); @@ -105,7 +100,7 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable // clang-format off const QString filename = QStringLiteral("%1@%2x%3-%4.png") - .arg(sha).arg(size.width()).arg(size.height()).arg(fillSuffix(m_fillMode)); + .arg(sha).arg(m_requestedSize.width()).arg(m_requestedSize.height()).arg(fillSuffix(m_fillMode)); // clang-format on const QString cache = cacheDir() + QLatin1Char('/') + filename; @@ -129,13 +124,13 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable // Scale to requested size switch (m_fillMode) { case CachingImageProvider::FillMode::Crop: - image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); + image = image.scaled(m_requestedSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); break; case CachingImageProvider::FillMode::Fit: - image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); + image = image.scaled(m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); break; case CachingImageProvider::FillMode::Stretch: - image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); + image = image.scaled(m_requestedSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); break; } @@ -143,11 +138,12 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable m_image = image; } else { // Crop or fit - QImage canvas(size, QImage::Format_ARGB32); + QImage canvas(m_requestedSize, QImage::Format_ARGB32); canvas.fill(Qt::transparent); QPainter painter(&canvas); - painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); + painter.drawImage( + (m_requestedSize.width() - image.width()) / 2, (m_requestedSize.height() - image.height()) / 2, image); painter.end(); m_image = canvas; From 8e373ced175e948dd91c8b87bccea6cb8dedb638 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:23:56 +1000 Subject: [PATCH 381/409] feat: split caching into separate service Return original image if no cache hit instead of waiting for cache to finish --- plugin/src/Caelestia/Images/CMakeLists.txt | 1 + .../Caelestia/Images/cachingimageprovider.cpp | 132 +++------------ .../Caelestia/Images/cachingimageprovider.hpp | 8 +- plugin/src/Caelestia/Images/imagecacher.cpp | 159 ++++++++++++++++++ plugin/src/Caelestia/Images/imagecacher.hpp | 38 +++++ 5 files changed, 224 insertions(+), 114 deletions(-) create mode 100644 plugin/src/Caelestia/Images/imagecacher.cpp create mode 100644 plugin/src/Caelestia/Images/imagecacher.hpp diff --git a/plugin/src/Caelestia/Images/CMakeLists.txt b/plugin/src/Caelestia/Images/CMakeLists.txt index 10707ae9d..d869667d8 100644 --- a/plugin/src/Caelestia/Images/CMakeLists.txt +++ b/plugin/src/Caelestia/Images/CMakeLists.txt @@ -2,6 +2,7 @@ qml_module(caelestia-images URI Caelestia.Images SOURCES cachingimageprovider.cpp + imagecacher.cpp iutils.cpp LIBRARIES Qt::Gui diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp index 2bd1c8b8f..45cd77a87 100644 --- a/plugin/src/Caelestia/Images/cachingimageprovider.cpp +++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp @@ -1,15 +1,12 @@ #include "cachingimageprovider.hpp" -#include -#include -#include +#include "imagecacher.hpp" + #include #include #include #include -#include #include -#include #include Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg) @@ -18,44 +15,9 @@ namespace caelestia::images { namespace { -const QString& cacheDir() { - static const QString s_dir = [] { - QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); - if (cache.isEmpty()) - cache = QDir::homePath() + QStringLiteral("/.cache"); - return cache + QStringLiteral("/caelestia/imagecache"); - }(); - return s_dir; -} - -QString sha256sum(const QString& path) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - qCWarning(lcCProv).noquote() << "sha256sum: failed to open" << path; - return {}; - } - - QCryptographicHash hash(QCryptographicHash::Sha256); - hash.addData(&file); - file.close(); - - return hash.result().toHex(); -} - -QString fillSuffix(CachingImageProvider::FillMode fillMode) { - switch (fillMode) { - case CachingImageProvider::FillMode::Crop: - return QStringLiteral("crop"); - case CachingImageProvider::FillMode::Fit: - return QStringLiteral("fit"); - default: - return QStringLiteral("stretch"); - } -} - class CachingImageResponse final : public QQuickImageResponse, public QRunnable { public: - CachingImageResponse(const QString& id, const QSize& requestedSize, CachingImageProvider::FillMode fillMode) + CachingImageResponse(const QString& id, const QSize& requestedSize, ImageCacher::FillMode fillMode) : m_id(id) , m_requestedSize(requestedSize) , m_fillMode(fillMode) { @@ -87,87 +49,39 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable // Use original image if requested size is invalid if (m_requestedSize.width() <= 0 || m_requestedSize.height() <= 0) { + qCDebug(lcCProv).noquote() << "Given source size is invalid, returning original:" << path; m_image = QImage(path); - qCDebug(lcCProv) << "Given source size is invalid, not caching."; + if (m_image.isNull()) { + m_error = QStringLiteral("Failed to decode source: ") + path; + qCWarning(lcCProv).noquote() << m_error; + } return; } - const QString sha = sha256sum(path); - if (sha.isEmpty()) { - m_error = QStringLiteral("Failed to hash: ") + path; - return; + // Try to use cached image + const auto cachePath = ImageCacher::cachePathFor(path, m_requestedSize, m_fillMode); + if (!cachePath.isEmpty()) { + QImageReader cacheReader(cachePath); + if (cacheReader.canRead()) { + m_image = cacheReader.read(); + if (!m_image.isNull()) + return; + } } - // clang-format off - const QString filename = QStringLiteral("%1@%2x%3-%4.png") - .arg(sha).arg(m_requestedSize.width()).arg(m_requestedSize.height()).arg(fillSuffix(m_fillMode)); - // clang-format on - const QString cache = cacheDir() + QLatin1Char('/') + filename; - - // Check cache, if it already exists, set and return - QImageReader cacheReader(cache); - if (cacheReader.canRead()) { - m_image = cacheReader.read(); - if (!m_image.isNull()) - return; - } + // Schedule cache job (this call will return the original image, but later ones will use cache) + ImageCacher::instance()->schedule(path, cachePath, m_requestedSize, m_fillMode); - QImage image(path); - if (image.isNull()) { - m_error = QStringLiteral("Failed to decode: ") + path; + m_image = QImage(path); + if (m_image.isNull()) { + m_error = QStringLiteral("Failed to decode source: ") + path; qCWarning(lcCProv).noquote() << m_error; - return; } - - image.convertTo(QImage::Format_ARGB32); - - // Scale to requested size - switch (m_fillMode) { - case CachingImageProvider::FillMode::Crop: - image = image.scaled(m_requestedSize, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - break; - case CachingImageProvider::FillMode::Fit: - image = image.scaled(m_requestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation); - break; - case CachingImageProvider::FillMode::Stretch: - image = image.scaled(m_requestedSize, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - break; - } - - if (m_fillMode == CachingImageProvider::FillMode::Stretch) { - m_image = image; - } else { - // Crop or fit - QImage canvas(m_requestedSize, QImage::Format_ARGB32); - canvas.fill(Qt::transparent); - - QPainter painter(&canvas); - painter.drawImage( - (m_requestedSize.width() - image.width()) / 2, (m_requestedSize.height() - image.height()) / 2, image); - painter.end(); - - m_image = canvas; - } - - // Save to cache - const QString parent = QFileInfo(cache).absolutePath(); - if (!QDir().mkpath(parent)) { - qCWarning(lcCProv).noquote() << "Failed to create cache dir" << parent; - return; - } - - QSaveFile saveFile(cache); - if (!saveFile.open(QIODevice::WriteOnly) || !m_image.save(&saveFile, "PNG") || !saveFile.commit()) { - qCWarning(lcCProv).noquote() << "Failed to save to" << cache << ":" << saveFile.errorString(); - return; - } - - qCDebug(lcCProv).noquote() << "Saved to" << cache; } QString m_id; QSize m_requestedSize; - CachingImageProvider::FillMode m_fillMode; + ImageCacher::FillMode m_fillMode; QImage m_image; QString m_error; }; diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.hpp b/plugin/src/Caelestia/Images/cachingimageprovider.hpp index 9e9c9772c..97082c765 100644 --- a/plugin/src/Caelestia/Images/cachingimageprovider.hpp +++ b/plugin/src/Caelestia/Images/cachingimageprovider.hpp @@ -1,16 +1,14 @@ #pragma once +#include "imagecacher.hpp" + #include namespace caelestia::images { class CachingImageProvider : public QQuickAsyncImageProvider { public: - enum class FillMode { - Crop, - Fit, - Stretch - }; + using FillMode = ImageCacher::FillMode; explicit CachingImageProvider(FillMode fillMode); diff --git a/plugin/src/Caelestia/Images/imagecacher.cpp b/plugin/src/Caelestia/Images/imagecacher.cpp new file mode 100644 index 000000000..dfef8f0b8 --- /dev/null +++ b/plugin/src/Caelestia/Images/imagecacher.cpp @@ -0,0 +1,159 @@ +#include "imagecacher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcCacher, "caelestia.images.cacher", QtInfoMsg) + +namespace caelestia::images { + +namespace { + +QString sha256sum(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(lcCacher).noquote() << "sha256sum: failed to open" << path; + return {}; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(&file); + file.close(); + + return hash.result().toHex(); +} + +QString fillSuffix(ImageCacher::FillMode fillMode) { + switch (fillMode) { + case ImageCacher::FillMode::Crop: + return QStringLiteral("crop"); + case ImageCacher::FillMode::Fit: + return QStringLiteral("fit"); + default: + return QStringLiteral("stretch"); + } +} + +} // namespace + +const QString& ImageCacher::cacheDir() { + static const QString s_dir = [] { + QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); + if (cache.isEmpty()) + cache = QDir::homePath() + QStringLiteral("/.cache"); + return cache + QStringLiteral("/caelestia/imagecache"); + }(); + return s_dir; +} + +QString ImageCacher::cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode) { + const QString sha = sha256sum(sourcePath); + if (sha.isEmpty()) + return {}; + + const QString filename = + QStringLiteral("%1@%2x%3-%4.png") + .arg(sha, QString::number(size.width()), QString::number(size.height()), fillSuffix(fillMode)); + + return cacheDir() + QLatin1Char('/') + filename; +} + +ImageCacher* ImageCacher::instance() { + static ImageCacher s_instance; + return &s_instance; +} + +ImageCacher::ImageCacher(QObject* parent) + : QObject(parent) {} + +void ImageCacher::schedule(const QString& sourcePath, const QSize& size, FillMode fillMode) { + schedule(sourcePath, cachePathFor(sourcePath, size, fillMode), size, fillMode); +} + +void ImageCacher::schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { + if (cachePath.isEmpty()) + return; + + { + QMutexLocker locker(&m_mutex); + if (m_inflight.contains(cachePath)) + return; + m_inflight.insert(cachePath); + } + + QThreadPool::globalInstance()->start([this, sourcePath, cachePath, size, fillMode]() { + runJob(sourcePath, cachePath, size, fillMode); + QMutexLocker locker(&m_mutex); + m_inflight.remove(cachePath); + }); +} + +void ImageCacher::runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { + if (QFile::exists(cachePath)) { + return; + } + + QImage image(sourcePath); + if (image.isNull()) { + qCWarning(lcCacher).noquote() << "Failed to decode source" << sourcePath; + return; + } + + Qt::AspectRatioMode scaleMode; + switch (fillMode) { + case FillMode::Crop: + scaleMode = Qt::KeepAspectRatioByExpanding; + break; + case FillMode::Fit: + scaleMode = Qt::KeepAspectRatio; + break; + case FillMode::Stretch: + scaleMode = Qt::IgnoreAspectRatio; + break; + } + + image.convertTo(QImage::Format_ARGB32); + image = image.scaled(size, scaleMode, Qt::SmoothTransformation); + + if (image.isNull()) { + qCWarning(lcCacher).noquote() << "Failed to scale" << sourcePath; + return; + } + + QImage canvas; + if (fillMode == FillMode::Stretch) { + canvas = image; + } else { + canvas = QImage(size, QImage::Format_ARGB32); + canvas.fill(Qt::transparent); + + QPainter painter(&canvas); + painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); + painter.end(); + } + + const QString parent = QFileInfo(cachePath).absolutePath(); + if (!QDir().mkpath(parent)) { + qCWarning(lcCacher).noquote() << "Failed to create cache dir" << parent; + return; + } + + QSaveFile saveFile(cachePath); + if (!saveFile.open(QIODevice::WriteOnly) || !canvas.save(&saveFile, "PNG") || !saveFile.commit()) { + qCWarning( + lcCacher, "Failed to save to %s: %s", qUtf8Printable(cachePath), qUtf8Printable(saveFile.errorString())); + return; + } + + qCDebug(lcCacher).noquote() << "Saved to" << cachePath; +} + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/imagecacher.hpp b/plugin/src/Caelestia/Images/imagecacher.hpp new file mode 100644 index 000000000..796084193 --- /dev/null +++ b/plugin/src/Caelestia/Images/imagecacher.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::images { + +class ImageCacher : public QObject { + Q_OBJECT + +public: + enum class FillMode { + Crop, + Fit, + Stretch, + }; + + static ImageCacher* instance(); + + static const QString& cacheDir(); + static QString cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode); + + void schedule(const QString& sourcePath, const QSize& size, FillMode fillMode); + void schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); + +private: + explicit ImageCacher(QObject* parent = nullptr); + + static void runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); + + QMutex m_mutex; + QSet m_inflight; +}; + +} // namespace caelestia::images From 5b88995b39ce80785c822cc6f79d4b4fe80561d4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:30:53 +1000 Subject: [PATCH 382/409] fix: initial 0 size on launcher wallpaper item --- modules/launcher/items/WallpaperItem.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 3f2143aef..ddf0d61af 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -66,6 +66,7 @@ Item { anchors.fill: parent path: root.modelData.path smooth: !root.PathView.view.moving + sourceSize: Qt.size(image.implicitWidth, image.implicitHeight) } } From f843250c9b92af3174d8a905d752301003571692 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:29:16 +1000 Subject: [PATCH 383/409] fix: blob exclusion being ignored at corner blends --- plugin/src/Caelestia/Blobs/CMakeLists.txt | 1 + plugin/src/Caelestia/Blobs/blobmaterial.cpp | 8 ++++- plugin/src/Caelestia/Blobs/blobmaterial.hpp | 3 ++ plugin/src/Caelestia/Blobs/blobshape.cpp | 24 +++++++++++++ plugin/src/Caelestia/Blobs/shaders/blob.frag | 36 +++++++++++++++++--- 5 files changed, 67 insertions(+), 5 deletions(-) diff --git a/plugin/src/Caelestia/Blobs/CMakeLists.txt b/plugin/src/Caelestia/Blobs/CMakeLists.txt index f44be0080..9506f7f44 100644 --- a/plugin/src/Caelestia/Blobs/CMakeLists.txt +++ b/plugin/src/Caelestia/Blobs/CMakeLists.txt @@ -12,6 +12,7 @@ qml_module(caelestia-blobs qt_add_shaders(caelestia-blobs "blob_shaders" BATCHABLE OPTIMIZED NOHLSL NOMSL + GLSL "300es,330" PREFIX "/" FILES shaders/blob.frag diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp index f71ad1bde..721a2532a 100644 --- a/plugin/src/Caelestia/Blobs/blobmaterial.cpp +++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp @@ -2,6 +2,9 @@ #include +static_assert(sizeof(decltype(BlobRectData::excludeMask)) == sizeof(float), + "BlobMaterial packs excludeMask into a float slot via memcpy"); + QSGMaterialType* BlobMaterial::type() const { static QSGMaterialType s_type; return &s_type; @@ -82,8 +85,11 @@ bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newM for (int i = 0; i < count; ++i) { const auto& r = mat->m_rects[i]; const int base = 160 + i * 80; + // Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt) + float maskAsFloat; + memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; - const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig }; + const float d1[4] = { maskAsFloat, r.offsetX, r.offsetY, r.minEig }; const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; memcpy(buf->data() + base, d0, 16); memcpy(buf->data() + base + 16, d1, 16); diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp index 85dbedaa0..bf1eda756 100644 --- a/plugin/src/Caelestia/Blobs/blobmaterial.hpp +++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp @@ -14,6 +14,9 @@ struct BlobRectData { float screenHalfX = 0, screenHalfY = 0; // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU float radius[4] = { 0, 0, 0, 0 }; + // Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. + // Used by the shader to skip smin between excluded pairs. + int excludeMask = 0; }; class BlobMaterial : public QSGMaterial { diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp index 9fcb622b6..cfa11c44c 100644 --- a/plugin/src/Caelestia/Blobs/blobshape.cpp +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -149,6 +149,10 @@ void BlobShape::updatePolish() { const QRectF myPadded(static_cast(m_cachedPaddedX), static_cast(m_cachedPaddedY), static_cast(m_cachedPaddedW), static_cast(m_cachedPaddedH)); + // Track shape pointers parallel to m_cachedRects for pairwise exclusion lookups + QVector rectShapes; + rectShapes.reserve(m_group->shapes().size()); + for (BlobShape* other : m_group->shapes()) { if (other->isInvertedRect()) continue; @@ -210,12 +214,29 @@ void BlobShape::updatePolish() { r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; m_cachedRects.append(r); + rectShapes.append(other); } } if (isInvertedRect()) m_cachedMyIndex = -1; + // Compute pairwise exclude masks. Bit j in entry i is set iff rect i excludes rect j + // or rect j excludes rect i. The shader uses this to avoid smin between excluded pairs. + const auto cachedCount = m_cachedRects.size(); + for (qsizetype i = 0; i < cachedCount; ++i) { + int mask = 0; + BlobShape* si = rectShapes[i]; + for (qsizetype j = 0; j < cachedCount; ++j) { + if (j == i) + continue; + BlobShape* sj = rectShapes[j]; + if (si->isExcluded(sj) || sj->isExcluded(si)) + mask |= (1 << j); + } + m_cachedRects[i].excludeMask = mask; + } + // Cache inverted rect data m_cachedHasInverted = false; m_cachedInvertedRadius = 0; @@ -270,6 +291,7 @@ void BlobShape::updatePolish() { const auto rectCount = m_cachedRects.size(); for (qsizetype i = 0; i < rectCount; ++i) { auto& ri = m_cachedRects[i]; + const int riExcludeMask = ri.excludeMask; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; @@ -280,6 +302,8 @@ void BlobShape::updatePolish() { for (qsizetype j = 0; j < rectCount; ++j) { if (j == i) continue; + if (riExcludeMask & (1 << j)) + continue; const auto& rj = m_cachedRects[j]; fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag index e78531b65..c002fd259 100644 --- a/plugin/src/Caelestia/Blobs/shaders/blob.frag +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -63,13 +63,15 @@ float smaxSharpA(float a, float b, float k) { void main() { vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); - float mergedSdf = 1e10; + // Phase 1: compute per-rect SDF, track owner. We can't smin yet because excluded + // pairs need to skip the smooth blend, which requires pairwise pass below. + float dArr[16]; int owner = -2; float minDist = 1e10; for (int i = 0; i < rectCount; i++) { vec4 rect = rectData[i * 5]; // cx, cy, hw, hh - vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig + vec4 props = rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) @@ -79,8 +81,10 @@ void main() { // AABB early-out: skip rects far from this pixel vec2 extent = sh.xy + vec2(smoothFactor * 1.5); - if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) + if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) { + dArr[i] = 1e10; continue; + } // Apply pre-computed inverse deformation to the evaluation point mat2 invDeform = mat2(invDm.xy, invDm.zw); @@ -135,13 +139,37 @@ void main() { d *= scale; } - mergedSdf = smin(mergedSdf, d, smoothFactor); + dArr[i] = d; if (d < smoothFactor && d < minDist) { minDist = d; owner = i; } } + // Phase 2: hard-min baseline over all rects. + float mergedSdf = 1e10; + for (int i = 0; i < rectCount; i++) { + mergedSdf = min(mergedSdf, dArr[i]); + } + + // Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin <= min, + // so taking the min over all non-excluded pair smins gives the smoothly-merged SDF. + for (int i = 0; i < rectCount; i++) { + if (dArr[i] >= 1e9) + continue; + int excludeMask = floatBitsToInt(rectData[i * 5 + 1].x); + for (int j = i + 1; j < rectCount; j++) { + if (dArr[j] >= 1e9) + continue; + if ((excludeMask & (1 << j)) != 0) + continue; + // smin only deviates from min within smoothFactor + if (abs(dArr[i] - dArr[j]) >= smoothFactor) + continue; + mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); + } + } + if (hasInverted != 0) { float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); From 6777ad0a2d9a1df586647ebc2c60db4ec96993c8 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:33:06 +1000 Subject: [PATCH 384/409] chore: remove old CachingImageManager --- plugin/src/Caelestia/Internal/CMakeLists.txt | 18 +- .../Internal/cachingimagemanager.cpp | 213 ------------------ .../Internal/cachingimagemanager.hpp | 65 ------ 3 files changed, 8 insertions(+), 288 deletions(-) delete mode 100644 plugin/src/Caelestia/Internal/cachingimagemanager.cpp delete mode 100644 plugin/src/Caelestia/Internal/cachingimagemanager.hpp diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index bc4a6948d..f4bbc5fdc 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -1,19 +1,17 @@ qml_module(caelestia-internal URI Caelestia.Internal SOURCES - arcgauge.hpp arcgauge.cpp - cachingimagemanager.hpp cachingimagemanager.cpp - circularbuffer.hpp circularbuffer.cpp - circularindicatormanager.hpp circularindicatormanager.cpp - hyprdevices.hpp hyprdevices.cpp - hyprextras.hpp hyprextras.cpp - logindmanager.hpp logindmanager.cpp - sparklineitem.hpp sparklineitem.cpp - visualiserbars.hpp visualiserbars.cpp + arcgauge.cpp + circularbuffer.cpp + circularindicatormanager.cpp + hyprdevices.cpp + hyprextras.cpp + logindmanager.cpp + sparklineitem.cpp + visualiserbars.cpp LIBRARIES Qt::Gui Qt::Quick - Qt::Concurrent Qt::Network Qt::DBus ) diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp deleted file mode 100644 index 46152d2a9..000000000 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +++ /dev/null @@ -1,213 +0,0 @@ -#include "cachingimagemanager.hpp" - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -Q_LOGGING_CATEGORY(lcCim, "caelestia.internal.cim", QtInfoMsg) - -namespace caelestia::internal { - -qreal CachingImageManager::effectiveScale() const { - if (m_item && m_item->window()) { - return m_item->window()->devicePixelRatio(); - } - - return 1.0; -} - -QSize CachingImageManager::effectiveSize() const { - if (!m_item) { - return QSize(); - } - - const qreal scale = effectiveScale(); - const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); - m_item->setProperty("sourceSize", size); - return size; -} - -QQuickItem* CachingImageManager::item() const { - return m_item; -} - -void CachingImageManager::setItem(QQuickItem* item) { - if (m_item == item) { - return; - } - - if (m_widthConn) { - disconnect(m_widthConn); - } - if (m_heightConn) { - disconnect(m_heightConn); - } - - m_item = item; - emit itemChanged(); - - if (item) { - m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { - updateSource(); - }); - m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { - updateSource(); - }); - updateSource(); - } -} - -QUrl CachingImageManager::cacheDir() const { - return m_cacheDir; -} - -void CachingImageManager::setCacheDir(const QUrl& cacheDir) { - if (m_cacheDir == cacheDir) { - return; - } - - m_cacheDir = cacheDir; - if (!m_cacheDir.path().endsWith("/")) { - m_cacheDir.setPath(m_cacheDir.path() + "/"); - } - emit cacheDirChanged(); -} - -QString CachingImageManager::path() const { - return m_path; -} - -void CachingImageManager::setPath(const QString& path) { - if (m_path == path) { - return; - } - - m_path = path; - emit pathChanged(); - - if (!path.isEmpty()) { - updateSource(path); - } -} - -void CachingImageManager::updateSource() { - updateSource(m_path); -} - -void CachingImageManager::updateSource(const QString& path) { - if (path.isEmpty() || path == m_shaPath) { - // Path is empty or already calculating sha for path - return; - } - - m_shaPath = path; - - QtConcurrent::run(&CachingImageManager::sha256sum, path).then(this, [path, this](const QString& sha) { - if (m_path != path) { - return; - } - - const QSize size = effectiveSize(); - - if (!m_item || !size.width() || !size.height()) { - return; - } - - const QString fillMode = m_item->property("fillMode").toString(); - // clang-format off - const QString filename = QString("%1@%2x%3-%4.png") - .arg(sha).arg(size.width()).arg(size.height()) - .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); - // clang-format on - - const QUrl cache = m_cacheDir.resolved(QUrl(filename)); - if (m_cachePath == cache) { - return; - } - - m_cachePath = cache; - emit cachePathChanged(); - - if (!cache.isLocalFile()) { - qCWarning(lcCim) << "updateSource: cachePath" << cache << "is not a local file"; - return; - } - - const QImageReader reader(cache.toLocalFile()); - if (reader.canRead()) { - m_item->setProperty("source", cache); - } else { - m_item->setProperty("source", QUrl::fromLocalFile(path)); - createCache(path, cache.toLocalFile(), fillMode, size); - } - - // Clear current running sha if same - if (m_shaPath == path) { - m_shaPath = QString(); - } - }); -} - -QUrl CachingImageManager::cachePath() const { - return m_cachePath; -} - -void CachingImageManager::createCache( - const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { - QThreadPool::globalInstance()->start([path, cache, fillMode, size] { - QImage image(path); - - if (image.isNull()) { - qCWarning(lcCim) << "createCache: failed to read" << path; - return; - } - - image.convertTo(QImage::Format_ARGB32); - - if (fillMode == "PreserveAspectCrop") { - image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); - } else if (fillMode == "PreserveAspectFit") { - image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); - } else { - image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); - } - - if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") { - QImage canvas(size, QImage::Format_ARGB32); - canvas.fill(Qt::transparent); - - QPainter painter(&canvas); - painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); - painter.end(); - - image = canvas; - } - - const QString parent = QFileInfo(cache).absolutePath(); - if (!QDir().mkpath(parent) || !image.save(cache)) { - qCWarning(lcCim) << "createCache: failed to save to" << cache; - } - }); -} - -QString CachingImageManager::sha256sum(const QString& path) { - QFile file(path); - if (!file.open(QIODevice::ReadOnly)) { - qCWarning(lcCim) << "sha256sum: failed to open" << path; - return ""; - } - - QCryptographicHash hash(QCryptographicHash::Sha256); - hash.addData(&file); - file.close(); - - return hash.result().toHex(); -} - -} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp deleted file mode 100644 index 1b707414d..000000000 --- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +++ /dev/null @@ -1,65 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -namespace caelestia::internal { - -class CachingImageManager : public QObject { - Q_OBJECT - QML_ELEMENT - - Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) - Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) - - Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) - Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) - -public: - explicit CachingImageManager(QObject* parent = nullptr) - : QObject(parent) {} - - [[nodiscard]] QQuickItem* item() const; - void setItem(QQuickItem* item); - - [[nodiscard]] QUrl cacheDir() const; - void setCacheDir(const QUrl& cacheDir); - - [[nodiscard]] QString path() const; - void setPath(const QString& path); - - [[nodiscard]] QUrl cachePath() const; - - Q_INVOKABLE void updateSource(); - Q_INVOKABLE void updateSource(const QString& path); - -signals: - void itemChanged(); - void cacheDirChanged(); - - void pathChanged(); - void cachePathChanged(); - void usingCacheChanged(); - -private: - QString m_shaPath; - - QPointer m_item; - QUrl m_cacheDir; - - QString m_path; - QUrl m_cachePath; - - QMetaObject::Connection m_widthConn; - QMetaObject::Connection m_heightConn; - - [[nodiscard]] qreal effectiveScale() const; - [[nodiscard]] QSize effectiveSize() const; - - void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; - [[nodiscard]] static QString sha256sum(const QString& path); -}; - -} // namespace caelestia::internal From dea8efcc97162267db7bbd44515294a298525eb1 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:49:21 +1000 Subject: [PATCH 385/409] fix: handle hidpi properly --- components/images/CachingImage.qml | 6 +++++- modules/launcher/items/WallpaperItem.qml | 6 +++++- plugin/src/Caelestia/Images/iutils.cpp | 3 +++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index e782b8821..1e932dbad 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Caelestia.Images Image { @@ -9,5 +10,8 @@ Image { asynchronous: true fillMode: Image.PreserveAspectCrop source: IUtils.urlForPath(path, fillMode) - sourceSize: Qt.size(width, height) + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index ddf0d61af..d09a894c6 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -1,4 +1,5 @@ import QtQuick +import Quickshell import Caelestia.Config import Caelestia.Models import qs.components @@ -66,7 +67,10 @@ Item { anchors.fill: parent path: root.modelData.path smooth: !root.PathView.view.moving - sourceSize: Qt.size(image.implicitWidth, image.implicitHeight) + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr); + } } } diff --git a/plugin/src/Caelestia/Images/iutils.cpp b/plugin/src/Caelestia/Images/iutils.cpp index aeee7684a..996f93dd1 100644 --- a/plugin/src/Caelestia/Images/iutils.cpp +++ b/plugin/src/Caelestia/Images/iutils.cpp @@ -16,6 +16,9 @@ IUtils* IUtils::create(QQmlEngine* engine, QJSEngine* jsEngine) { } QUrl IUtils::urlForPath(const QString& path, int fillMode) { + if (path.isEmpty()) + return QUrl(); + QString prefix; switch (fillMode) { case 1: // Image.PreserveAspectFit From 775d0a81015da00120b14ff10d3aa5bfebdd5558 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 21:57:30 +1000 Subject: [PATCH 386/409] fix: handle hidpi for rest of sourceSize uses --- modules/dashboard/Media.qml | 6 ++++-- modules/dashboard/dash/Media.qml | 7 +++++-- modules/lock/Media.qml | 7 +++++-- modules/lock/NotifDock.qml | 2 +- modules/lock/NotifGroup.qml | 6 ++++-- modules/notifications/Notification.qml | 6 ++++-- modules/session/Content.qml | 2 +- modules/sidebar/NotifDock.qml | 3 ++- modules/sidebar/NotifGroup.qml | 6 ++++-- 9 files changed, 30 insertions(+), 15 deletions(-) diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index ccb5f0c4b..367d9e98a 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -184,8 +184,10 @@ Item { source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } MouseArea { anchors.fill: parent diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index b8e78abde..a14e5e389 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,5 +1,6 @@ import QtQuick import QtQuick.Shapes +import Quickshell import Caelestia.Config import Caelestia.Services import qs.components @@ -109,8 +110,10 @@ Item { source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } } } diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index 05c9daaf2..c55333c7f 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Quickshell import Caelestia.Config import qs.components import qs.components.effects @@ -22,8 +23,10 @@ Item { asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } layer.enabled: true layer.effect: OpacityMask { diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index bf827579e..2254dfe0b 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -52,7 +52,7 @@ ColumnLayout { asynchronous: true source: Paths.absolutePath(Config.paths.lockNoNotifsPic) fillMode: Image.PreserveAspectFit - sourceSize.width: clipRect.width * 0.8 + sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) layer.enabled: true layer.effect: Colouriser { diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 0a5b6d738..971b921a0 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -73,8 +73,10 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: TokenConfig.sizes.notifs.image - sourceSize.height: TokenConfig.sizes.notifs.image + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true width: TokenConfig.sizes.notifs.image diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 70f58af08..fe6fd0567 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -126,8 +126,10 @@ StyledRect { anchors.fill: parent source: Qt.resolvedUrl(root.modelData.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: TokenConfig.sizes.notifs.image - sourceSize.height: TokenConfig.sizes.notifs.image + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true } diff --git a/modules/session/Content.qml b/modules/session/Content.qml index bf16b1488..e1ba28098 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -48,7 +48,7 @@ Column { AnimatedImage { width: Tokens.sizes.session.button height: Tokens.sizes.session.button - sourceSize.width: width + sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) playing: visible asynchronous: true diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index bf978ec0b..4509ce26b 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Layouts +import Quickshell import Quickshell.Widgets import Caelestia.Config import qs.components @@ -98,7 +99,7 @@ Item { asynchronous: true source: Paths.absolutePath(Config.paths.noNotifsPic) fillMode: Image.PreserveAspectFit - sourceSize.width: clipRect.width * 0.8 + sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) layer.enabled: true layer.effect: Colouriser { diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 57ee06b4f..2920af743 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -87,8 +87,10 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop - sourceSize.width: TokenConfig.sizes.notifs.image - sourceSize.height: TokenConfig.sizes.notifs.image + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true width: TokenConfig.sizes.notifs.image From 665d784186ddded833cf158edcafe67111242e48 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:07:11 +1000 Subject: [PATCH 387/409] feat: calculate size from source when single dim requested --- .../Caelestia/Images/cachingimageprovider.cpp | 28 ++++++++++++++++--- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp index 45cd77a87..768e05dae 100644 --- a/plugin/src/Caelestia/Images/cachingimageprovider.cpp +++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp @@ -47,8 +47,12 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable return; } - // Use original image if requested size is invalid - if (m_requestedSize.width() <= 0 || m_requestedSize.height() <= 0) { + QSize size = m_requestedSize; + const bool needsW = size.width() <= 0; + const bool needsH = size.height() <= 0; + + // If both dimensions are missing, return the original directly + if (needsW && needsH) { qCDebug(lcCProv).noquote() << "Given source size is invalid, returning original:" << path; m_image = QImage(path); if (m_image.isNull()) { @@ -58,8 +62,24 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable return; } + // If one dimension is missing, derive it from the source aspect ratio + if (needsW || needsH) { + const QImageReader sourceReader(path); + const QSize sourceSize = sourceReader.size(); + if (!sourceSize.isValid() || sourceSize.isEmpty()) { + m_error = QStringLiteral("Could not determine source size for: ") + path; + qCWarning(lcCProv).noquote() << m_error; + return; + } + + if (needsW) + size.setWidth(qRound(size.height() * sourceSize.width() / static_cast(sourceSize.height()))); + else + size.setHeight(qRound(size.width() * sourceSize.height() / static_cast(sourceSize.width()))); + } + // Try to use cached image - const auto cachePath = ImageCacher::cachePathFor(path, m_requestedSize, m_fillMode); + const auto cachePath = ImageCacher::cachePathFor(path, size, m_fillMode); if (!cachePath.isEmpty()) { QImageReader cacheReader(cachePath); if (cacheReader.canRead()) { @@ -70,7 +90,7 @@ class CachingImageResponse final : public QQuickImageResponse, public QRunnable } // Schedule cache job (this call will return the original image, but later ones will use cache) - ImageCacher::instance()->schedule(path, cachePath, m_requestedSize, m_fillMode); + ImageCacher::instance()->schedule(path, cachePath, size, m_fillMode); m_image = QImage(path); if (m_image.isNull()) { From d4b98dec7759072f9c1bfaaede3b284ca4589dd4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 30 Apr 2026 21:49:44 +1000 Subject: [PATCH 388/409] fix: slight border showing when fullscreen --- modules/drawers/ContentWindow.qml | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 5fbacd416..d1b6ad8ea 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -31,6 +31,8 @@ StyledWindow { } return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; } + + property real sdfBorderOffset: hasFullscreen ? 2 : 0 // SDFs joins are not exact, so offset by 2px to ensure nothing shows property real borderThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness property real borderRounding: hasFullscreen ? 0 : contentItem.Config.border.rounding @@ -68,6 +70,10 @@ StyledWindow { anchors.left: true anchors.right: true + Behavior on sdfBorderOffset { + Anim {} + } + Behavior on borderThickness { Anim { type: Anim.DefaultSpatial @@ -149,10 +155,10 @@ StyledWindow { anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers group: blobGroup radius: root.borderRounding - borderLeft: bar.implicitWidth - anchors.margins - borderRight: root.borderThickness - anchors.margins - borderTop: root.borderThickness - anchors.margins - borderBottom: root.borderThickness - anchors.margins + borderLeft: bar.implicitWidth - anchors.margins - root.sdfBorderOffset + borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset + borderTop: root.borderThickness - anchors.margins - root.sdfBorderOffset + borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset } PanelBg { From bcd7f6f161ae52e725b837335019525ffc221d8f Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:01:27 +1000 Subject: [PATCH 389/409] feat: display on overlay layer only when fs --- modules/drawers/ContentWindow.qml | 31 +++++++------------------------ 1 file changed, 7 insertions(+), 24 deletions(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index d1b6ad8ea..c92b78ad0 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -32,11 +32,12 @@ StyledWindow { return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; } - property real sdfBorderOffset: hasFullscreen ? 2 : 0 // SDFs joins are not exact, so offset by 2px to ensure nothing shows - property real borderThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness + property real fsTransitionProg: hasFullscreen ? 1 : 0 + readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows + readonly property real borderThickness: contentItem.Config.border.thickness * (1 - fsTransitionProg) + readonly property real borderRounding: contentItem.Config.border.rounding * (1 - fsTransitionProg) + readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness - property real borderRounding: hasFullscreen ? 0 : contentItem.Config.border.rounding - property real shadowOpacity: hasFullscreen ? 0 : 0.7 readonly property int dragMaskPadding: { if (focusGrab.active || panels.popouts.isDetached) @@ -60,7 +61,7 @@ StyledWindow { name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.layer: fsTransitionProg > 0 ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: hasFullscreen ? emptyRegion : regions @@ -70,28 +71,10 @@ StyledWindow { anchors.left: true anchors.right: true - Behavior on sdfBorderOffset { + Behavior on fsTransitionProg { Anim {} } - Behavior on borderThickness { - Anim { - type: Anim.DefaultSpatial - } - } - - Behavior on borderRounding { - Anim { - type: Anim.DefaultSpatial - } - } - - Behavior on shadowOpacity { - Anim { - type: Anim.DefaultSpatial - } - } - Region { id: emptyRegion } From 87a452851e51127bc695c5f764ef6f8394ffebb4 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:05:12 +1000 Subject: [PATCH 390/409] fix: allow interacting with notifs and osd in fs --- modules/drawers/ContentWindow.qml | 12 ++++++++++++ modules/drawers/Interactions.qml | 7 ++++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index c92b78ad0..0849f65af 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -77,6 +77,18 @@ StyledWindow { Region { id: emptyRegion + + x: panels.notifications.x + bar.implicitWidth + y: panels.notifications.y + root.borderThickness + width: panels.notifications.width + height: panels.notifications.height + + Region { + x: root.width - width + y: panels.osdWrapper.y + root.borderThickness + width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness + height: panels.osd.height + } } Regions { diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index bef8f4b8e..b89cb0077 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -61,7 +61,7 @@ CustomMouseArea { anchors.fill: parent acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons - hoverEnabled: !fullscreen + hoverEnabled: true onPressed: event => dragStart = Qt.point(event.x, event.y) onContainsMouseChanged: { @@ -97,6 +97,11 @@ CustomMouseArea { const dragX = x - dragStart.x; const dragY = y - dragStart.y; + if (fullscreen) { + root.panels.osd.hovered = inRightPanel(panels.osdWrapper, x, y); + return; + } + // Show bar in non-exclusive mode on hover if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) bar.isHovered = true; From a06fa385220c5f5735eb2597509db8171304193e Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:21:47 +1000 Subject: [PATCH 391/409] fix: close detached popout on fs --- modules/drawers/ContentWindow.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 0849f65af..60bbe39fe 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -57,6 +57,7 @@ StyledWindow { visibilities.launcher = false; visibilities.session = false; visibilities.dashboard = false; + panels.popouts.close(); } name: "drawers" From dfbd0829e7495faf02f4a863d7a20a6c76b58b75 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:26:52 +1000 Subject: [PATCH 392/409] feat: add showOverFullscreen option + disable by default Fixes #1377 --- README.md | 1 + modules/drawers/ContentWindow.qml | 2 +- plugin/src/Caelestia/Config/generalconfig.hpp | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 41c74df95..13b5ec8a2 100644 --- a/README.md +++ b/README.md @@ -298,6 +298,7 @@ For example, to disable the bar on DP-1: }, "general": { "logo": "caelestia", + "showOverFullscreen": false, "mediaGifSpeedAdjustment": 300, "sessionGifSpeed": 0.7, "apps": { diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml index 60bbe39fe..677c84a09 100644 --- a/modules/drawers/ContentWindow.qml +++ b/modules/drawers/ContentWindow.qml @@ -62,7 +62,7 @@ StyledWindow { name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: fsTransitionProg > 0 ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.layer: fsTransitionProg > 0 && contentItem.Config.general.showOverFullscreen ? WlrLayer.Overlay : WlrLayer.Top WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: hasFullscreen ? emptyRegion : regions diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp index 73a4915d1..859302fef 100644 --- a/plugin/src/Caelestia/Config/generalconfig.hpp +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -90,6 +90,7 @@ class GeneralConfig : public ConfigObject { QML_ANONYMOUS CONFIG_GLOBAL_PROPERTY(QString, logo) + CONFIG_PROPERTY(bool, showOverFullscreen, false) CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) CONFIG_SUBOBJECT(GeneralApps, apps) From efc08759ceaeddc2c571d868c623995270ac365d Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Thu, 30 Apr 2026 22:47:42 +1000 Subject: [PATCH 393/409] fix: allow non-ascii chars in passwords Fixes #1424 --- modules/lock/Pam.qml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 94832b206..f4c617473 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -31,8 +31,8 @@ Scope { } else { buffer = buffer.slice(0, -1); } - } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { - // No illegal characters (you are insane if you use unicode in your password) + } else if (/^[^\x00-\x1F\x7F-\x9F]+$/.test(event.text)) { + // Allow anything except control characters buffer += event.text; } } From e872bc54b1c2fb56cdb25fcbfaf7822ad6540fa7 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 2 May 2026 12:03:55 +1000 Subject: [PATCH 394/409] ci: fix update flake workflow --- .github/workflows/update-flake-inputs.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml index c90822157..9b5a37f9e 100644 --- a/.github/workflows/update-flake-inputs.yml +++ b/.github/workflows/update-flake-inputs.yml @@ -14,13 +14,12 @@ jobs: id: app-token uses: actions/create-github-app-token@v3 with: - app-id: ${{ secrets.APP_ID }} + client-id: ${{ secrets.CLIENT_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} - persist-credentials: false - name: Install Nix uses: nixbuild/nix-quick-install-action@v31 From a3940125e894fc52c6ddecaa261c721e533ee1c1 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sat, 2 May 2026 02:14:56 +0000 Subject: [PATCH 395/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 9f1b2ea0e..640590d06 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1772764582, - "narHash": "sha256-hSwjmpXHFqzSXrndVekA0IheKrbC7wi0IbfZTYwlmXw=", + "lastModified": 1777470967, + "narHash": "sha256-u8QP1TYolV6BR0qsK2NHY1qZ/PdNAgrVxUcSDZdl35Q=", "owner": "caelestia-dots", "repo": "cli", - "rev": "4bcd42f482d038b98145b0b03388244b68b7d35d", + "rev": "5c9ce66c031788df50bbfbea195bf773ddbf92bc", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1772773019, - "narHash": "sha256-E1bxHxNKfDoQUuvriG71+f+s/NT0qWkImXsYZNFFfCs=", + "lastModified": 1777268161, + "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "aca4d95fce4914b3892661bcb80b8087293536c6", + "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1772925576, - "narHash": "sha256-mMoiXABDtkSJxCYDrkhJ/TrrJf5M46oUfIlJvv2gkZ0=", + "lastModified": 1777341401, + "narHash": "sha256-QEAVYeXxvTamsYJVBq8+qSJV9ml2MxqRaZvkobfuPWA=", "ref": "refs/heads/master", - "rev": "15a84097653593dd15fad59a56befc2b7bdc270d", - "revCount": 750, + "rev": "0baa81aa03559ca315668e5a306364cddf1a6f49", + "revCount": 812, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 4e9e1f4b723f7e3a87cb280d67a25ee92c87fbff Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Sat, 2 May 2026 12:17:49 +1000 Subject: [PATCH 396/409] ci: update action versions --- .github/workflows/update-flake-inputs.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml index 9b5a37f9e..7f24c7978 100644 --- a/.github/workflows/update-flake-inputs.yml +++ b/.github/workflows/update-flake-inputs.yml @@ -22,14 +22,14 @@ jobs: token: ${{ steps.app-token.outputs.token }} - name: Install Nix - uses: nixbuild/nix-quick-install-action@v31 + uses: nixbuild/nix-quick-install-action@v34 with: nix_conf: | keep-env-derivations = true keep-outputs = true - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 + uses: nix-community/cache-nix-action@v7 with: # restore and save a cache using this key primary-key: nix-${{ hashFiles('**/*.nix', '**/flake.lock') }} @@ -84,7 +84,7 @@ jobs: - name: Commit and push changes if: steps.check.outputs.modified == 'true' - uses: EndBug/add-and-commit@v9 + uses: EndBug/add-and-commit@v10 with: github_token: ${{ steps.app-token.outputs.token }} add: flake.lock From e056d7bc258eb150f1f932441eca4572d6e53bf4 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 3 May 2026 02:41:22 +0000 Subject: [PATCH 397/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 640590d06..225792b13 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1777470967, - "narHash": "sha256-u8QP1TYolV6BR0qsK2NHY1qZ/PdNAgrVxUcSDZdl35Q=", + "lastModified": 1777726295, + "narHash": "sha256-Db9B3uTLDSj7zjQ4L3tIk9RcxHXbWRKw5fw8JozikyI=", "owner": "caelestia-dots", "repo": "cli", - "rev": "5c9ce66c031788df50bbfbea195bf773ddbf92bc", + "rev": "a00e71d6b7572a8dbbb7945a45b79acf2401ad56", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1777268161, - "narHash": "sha256-bxrdOn8SCOv8tN4JbTF/TXq7kjo9ag4M+C8yzzIRYbE=", + "lastModified": 1777578337, + "narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "1c3fe55ad329cbcb28471bb30f05c9827f724c76", + "rev": "15f4ee454b1dce334612fa6843b3e05cf546efab", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1777341401, - "narHash": "sha256-QEAVYeXxvTamsYJVBq8+qSJV9ml2MxqRaZvkobfuPWA=", + "lastModified": 1777706159, + "narHash": "sha256-TuD8hw9lkRCEb+6v93iB7HDvcmvH8R0qyD6wBmPGfv8=", "ref": "refs/heads/master", - "rev": "0baa81aa03559ca315668e5a306364cddf1a6f49", - "revCount": 812, + "rev": "8db8ca1fecfcce8def1f9265fa1742baa0e0c271", + "revCount": 813, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 97ec5985732d6f117f574f29c8164b759c162724 Mon Sep 17 00:00:00 2001 From: dark3txr <227404939+dark3txr@users.noreply.github.com> Date: Mon, 4 May 2026 09:07:02 +0100 Subject: [PATCH 398/409] fix: add Global to app properties (#1447) --- modules/launcher/items/CalcItem.qml | 2 +- modules/launcher/services/Apps.qml | 2 +- modules/utilities/cards/RecordingList.qml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index e7f11babe..f746f96c6 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -78,7 +78,7 @@ Item { id: stateLayer onClicked: { - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); + Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); root.list.visibilities.launcher = false; } diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index 5c80513e9..168d9209d 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -13,7 +13,7 @@ Searcher { if (entry.runInTerminal) Quickshell.execDetached({ - command: ["app2unit", "--", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], + command: ["app2unit", "--", ...GlobalConfig.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], workingDirectory: entry.workingDirectory }); else diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index a0ec101b3..951f7f09e 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -104,7 +104,7 @@ ColumnLayout { onClicked: { root.visibilities.utilities = false; root.visibilities.sidebar = false; - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]); + Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.playback, recording.modelData.path]); } } @@ -114,7 +114,7 @@ ColumnLayout { onClicked: { root.visibilities.utilities = false; root.visibilities.sidebar = false; - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]); + Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.explorer, recording.modelData.path]); } } From 5d1fa5c60da77500f73ee86e2fcb6b0d9aeb5d2c Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 4 May 2026 20:34:23 +1000 Subject: [PATCH 399/409] chore: add crash issue template --- .github/ISSUE_TEMPLATE/crash.yml | 57 ++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/crash.yml diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml new file mode 100644 index 000000000..e37198c91 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -0,0 +1,57 @@ +name: Crash report +description: Report a crash +labels: ["bug", "crash"] +type: "Bug" +title: "[CRASH] " +body: + - type: textarea + attributes: + label: What caused the crash + description: | + Any information likely to help debug the crash. What were you doing when the crash occurred, + what changes did you make, can you get it to happen again? + + - type: upload + attributes: + label: Report file + description: Attach `report.txt` here. + validations: + required: true + + - type: upload + attributes: + label: Log file + description: | + Attach `log.qslog.log` here. If it is too big to upload, compress it. + + You can preview the log if you'd like using `qs log -r '*=true'`. + validations: + required: true + + - type: textarea + attributes: + label: "Version info" + description: Run `caelestia -v` and paste the result below. + value: | +
Version info + + ``` + + ``` + +
+ validations: + required: true + + - type: checkboxes + attributes: + label: Reminder + options: + - label: I've successfully updated to the latest versions following the [updating guide](https://github.com/caelestia-dots/caelestia?tab=readme-ov-file#updating). + required: false + - label: I've successfully updated the system packages to the latest. + required: false + - label: I agree that it's usually impossible for others to help me without my logs. + required: true + - label: I've ticked the checkboxes without reading their contents + required: false From 54cdd80c1b7671deeb057cc554f83e436765596a Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Mon, 4 May 2026 20:38:23 +1000 Subject: [PATCH 400/409] chore: set crash report url to ours Sorry for putting this off for so long foxxed T_T --- shell.qml | 1 + 1 file changed, 1 insertion(+) diff --git a/shell.qml b/shell.qml index 01ba0f191..bc2bb1648 100644 --- a/shell.qml +++ b/shell.qml @@ -1,3 +1,4 @@ +//@ pragma Env QS_CRASHREPORT_URL=https://github.com/caelestia-dots/shell/issues/new?template=crash.yml //@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1 //@ pragma DefaultEnv QS_DROP_EXPENSIVE_FONTS=1 //@ pragma DefaultEnv QSG_RENDER_LOOP=threaded From 4763a690cd41ba8c13e69d89a0d2d655332d1e89 Mon Sep 17 00:00:00 2001 From: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> Date: Wed, 6 May 2026 21:04:43 +1000 Subject: [PATCH 401/409] fix: lock screencopy not working --- modules/lock/Lock.qml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index 5d70266a0..f852cb7ff 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -1,5 +1,6 @@ pragma ComponentBehavior: Bound +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland @@ -25,6 +26,20 @@ Scope { lock: lock } + Loader { + asynchronous: true + active: true + onLoaded: active = false + + // Force a load of a screencopy so the one in the lock works + // My guess is the ICC backend loads async on first request, which if the lock is + // the first request it fails to capture (because it's async and the compositor + // refuses capture when locked) + sourceComponent: ScreencopyView { + captureSource: Quickshell.screens[0] + } + } + // qmllint disable unresolved-type CustomShortcut { // qmllint enable unresolved-type From 859c76ad7c615a38876123d0483d8d32052e71a5 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 10 May 2026 04:10:32 +0300 Subject: [PATCH 402/409] Fixed closeAnim.start() so it only runs after the file is saved and swappy is launched. --- modules/areapicker/Picker.qml | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 35b35a2e4..3be2bf308 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -71,20 +71,20 @@ MouseArea { } } - function save(): void { - const tmpfile = Qt.resolvedUrl(`/tmp/caelestia-picker-${Quickshell.processId}-${Date.now()}.png`); - CUtils.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => { - if (root.loader.clipboardOnly) { - Quickshell.execDetached(["sh", "-c", "wl-copy --type image/png < " + path]); - Quickshell.execDetached(["notify-send", "-a", "caelestia-cli", "-i", path, "Screenshot taken", "Screenshot copied to clipboard"]); - } else { - Quickshell.execDetached(["swappy", "-f", path]); - } - }); - closeAnim.start(); - } + function save(): void { + const tmpfile = Qt.resolvedUrl(`/tmp/caelestia-picker-${Quickshell.processId}-${Date.now()}.png`); + CUtils.saveItem(screencopy, tmpfile, Qt.rect(Math.ceil(rsx), Math.ceil(rsy), Math.floor(sw), Math.floor(sh)), path => { + if (root.loader.clipboardOnly) { + Quickshell.execDetached(["sh", "-c", "wl-copy --type image/png < " + path]); + Quickshell.execDetached(["notify-send", "-a", "caelestia-cli", "-i", path, "Screenshot taken", "Screenshot copied to clipboard"]); + } else { + Quickshell.execDetached(["swappy", "-f", path]); + } + closeAnim.start(); + }) +} - onClientsChanged: checkClientRects(mouseX, mouseY) +onClientsChanged: checkClientRects(mouseX, mouseY) anchors.fill: parent opacity: 0 From 2ca4ad4a434e91e73504debd5225e66dc5ebb2b6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 10 May 2026 02:43:24 +0000 Subject: [PATCH 403/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 225792b13..32e7be601 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1777726295, - "narHash": "sha256-Db9B3uTLDSj7zjQ4L3tIk9RcxHXbWRKw5fw8JozikyI=", + "lastModified": 1778125502, + "narHash": "sha256-QAAO9RCR6byVJi50l8RMVJWzrsNYbXonfR6tqU93vIQ=", "owner": "caelestia-dots", "repo": "cli", - "rev": "a00e71d6b7572a8dbbb7945a45b79acf2401ad56", + "rev": "7b8a4281aa8b2b12745de531cce0c65d87aea2e5", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1777578337, - "narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=", + "lastModified": 1777954456, + "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", "owner": "nixos", "repo": "nixpkgs", - "rev": "15f4ee454b1dce334612fa6843b3e05cf546efab", + "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1777706159, - "narHash": "sha256-TuD8hw9lkRCEb+6v93iB7HDvcmvH8R0qyD6wBmPGfv8=", + "lastModified": 1778222427, + "narHash": "sha256-6GFiP611nEJvtm+m03sMyfaVIJ9QOCi//hS+PPKyyPA=", "ref": "refs/heads/master", - "rev": "8db8ca1fecfcce8def1f9265fa1742baa0e0c271", - "revCount": 813, + "rev": "d1760ed1f31c02a95b37a9bf4084129c829ebe7f", + "revCount": 817, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From cf18cea3dad28ddda2f151b1b42a66f2fba1f84a Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 17 May 2026 02:50:49 +0000 Subject: [PATCH 404/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 32e7be601..0721599d2 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1778125502, - "narHash": "sha256-QAAO9RCR6byVJi50l8RMVJWzrsNYbXonfR6tqU93vIQ=", + "lastModified": 1778644647, + "narHash": "sha256-iQIu4b5by8B3qKeigungSIgRoJg9HS1NiIZwqIbCGYk=", "owner": "caelestia-dots", "repo": "cli", - "rev": "7b8a4281aa8b2b12745de531cce0c65d87aea2e5", + "rev": "2ce6213698d1cfa15b4b067d35c3cda634f443dd", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1777954456, - "narHash": "sha256-hGdgeU2Nk87RAuZyYjyDjFL6LK7dAZN5RE9+hrDTkDU=", + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "549bd84d6279f9852cae6225e372cc67fb91a4c1", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1778222427, - "narHash": "sha256-6GFiP611nEJvtm+m03sMyfaVIJ9QOCi//hS+PPKyyPA=", + "lastModified": 1778488696, + "narHash": "sha256-QSWgYuZUCNUJ/cxmaq83WkcT7lHQDDfsPVgH+96kIl0=", "ref": "refs/heads/master", - "rev": "d1760ed1f31c02a95b37a9bf4084129c829ebe7f", - "revCount": 817, + "rev": "7d1c9a9c6721606b129829134d6f614f015621e2", + "revCount": 818, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 2f7ab5e4140d9188185f6c3164e428c1eeda1fe6 Mon Sep 17 00:00:00 2001 From: github-actions <41898282+github-actions[bot]@users.noreply.github.com> Date: Sun, 24 May 2026 02:59:47 +0000 Subject: [PATCH 405/409] [CI] chore: update flake --- flake.lock | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/flake.lock b/flake.lock index 0721599d2..1199fd146 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1778644647, - "narHash": "sha256-iQIu4b5by8B3qKeigungSIgRoJg9HS1NiIZwqIbCGYk=", + "lastModified": 1779508871, + "narHash": "sha256-SFl2je8b423f/tIy0RTgCrWQUJiyFsAZp1Y0cccXqio=", "owner": "caelestia-dots", "repo": "cli", - "rev": "2ce6213698d1cfa15b4b067d35c3cda634f443dd", + "rev": "7f300626704b7a86fe838ab58df654fa99779613", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1778869304, - "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "lastModified": 1779508470, + "narHash": "sha256-Ap9KJX+5xHIn3bPIpfNgT6MEXdAECECwo4/rmlQD74M=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "rev": "29916453413845e54a65b8a1cf996842300cd299", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1778488696, - "narHash": "sha256-QSWgYuZUCNUJ/cxmaq83WkcT7lHQDDfsPVgH+96kIl0=", + "lastModified": 1779430452, + "narHash": "sha256-zTslhsxLqUlRTML506iougTGzyR38Fzhzn7t4KDEuuE=", "ref": "refs/heads/master", - "rev": "7d1c9a9c6721606b129829134d6f614f015621e2", - "revCount": 818, + "rev": "4b4fca3224ab977dc515ac0bb78d00b3dfa71e00", + "revCount": 819, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, From 922d4a7b5c1dc932785ac26a953dfecd85cd07a6 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 24 May 2026 13:53:11 +0300 Subject: [PATCH 406/409] recover local changes --- .envrc | 5 +- .github/ISSUE_TEMPLATE/crash.yml | 57 + .github/workflows/check-format.yml | 38 + .github/workflows/lint.yml | 43 + .github/workflows/release.yml | 9 +- .github/workflows/update-flake-inputs.yml | 23 +- .github/workflows/update-image.yml | 46 + .gitignore | 1 + CMakeLists.txt | 10 +- README.md | 144 +- all_changes.patch | 51777 ++++++++++++++++ assets/logo.svg | 121 +- assets/shaders/fade.frag | 26 + assets/shaders/fade.frag.qsb | Bin 0 -> 1659 bytes clean-snapshot.tar | 0 components/AnchorAnim.qml | 51 + components/Anim.qml | 48 +- components/CAnim.qml | 7 +- components/ConnectionHeader.qml | 11 +- components/ConnectionInfoSection.qml | 23 +- components/DashboardState.qml | 6 + components/DrawerVisibilities.qml | 11 + components/Logo.qml | 70 + components/MaterialIcon.qml | 6 +- components/PropertyRow.qml | 12 +- components/SectionContainer.qml | 15 +- components/SectionHeader.qml | 10 +- components/StateLayer.qml | 213 +- components/StyledClippingRect.qml | 2 +- components/StyledText.qml | 15 +- components/containers/StyledFlickable.qml | 2 +- components/containers/StyledListView.qml | 2 +- components/containers/StyledWindow.qml | 6 + components/controls/CircularIndicator.qml | 18 +- components/controls/CircularProgress.qml | 18 +- components/controls/CollapsibleSection.qml | 72 +- components/controls/CustomSpinBox.qml | 48 +- components/controls/FilledSlider.qml | 24 +- components/controls/IconButton.qml | 13 +- components/controls/IconTextButton.qml | 17 +- components/controls/Menu.qml | 225 +- components/controls/SpinBoxRow.qml | 18 +- components/controls/SplitButton.qml | 60 +- components/controls/SplitButtonRow.qml | 19 +- components/controls/StyledInputField.qml | 31 +- components/controls/StyledRadioButton.qml | 21 +- components/controls/StyledScrollBar.qml | 78 +- components/controls/StyledSlider.qml | 12 +- components/controls/StyledSwitch.qml | 29 +- components/controls/StyledTextField.qml | 18 +- components/controls/SwitchRow.qml | 24 +- components/controls/TextButton.qml | 15 +- components/controls/ToggleButton.qml | 58 +- components/controls/ToggleRow.qml | 9 +- components/controls/Tooltip.qml | 169 +- components/effects/ColouredIcon.qml | 4 +- components/effects/Colouriser.qml | 2 +- components/effects/Elevation.qml | 4 +- components/effects/InnerBorder.qml | 10 +- components/effects/OpacityMask.qml | 4 +- components/filedialog/CurrentItem.qml | 20 +- components/filedialog/DialogButtons.qml | 41 +- components/filedialog/FileDialog.qml | 6 +- components/filedialog/FolderContents.qml | 193 +- components/filedialog/HeaderBar.qml | 49 +- components/filedialog/Sidebar.qml | 38 +- components/images/CachingIconImage.qml | 8 +- components/images/CachingImage.qml | 25 +- components/misc/CustomShortcut.qml | 2 + components/widgets/ExtraIndicator.qml | 19 +- extras/version.cpp | 3 +- flake.lock | 20 +- flake.nix | 1 - last-50-commits.txt | 50 + modules/BatteryMonitor.qml | 22 +- modules/ConfigToasts.qml | 39 + modules/IdleMonitors.qml | 12 +- modules/MonitorIdentifier.qml | 12 +- modules/Shortcuts.qml | 70 +- modules/areapicker/AreaPicker.qml | 21 +- modules/areapicker/Picker.qml | 45 +- modules/background/Background.qml | 254 +- modules/background/DesktopClock.qml | 59 +- modules/background/Visualiser.qml | 105 +- modules/background/Wallpaper.qml | 38 +- modules/bar/Bar.qml | 54 +- modules/bar/BarWrapper.qml | 43 +- modules/bar/components/ActiveWindow.qml | 58 +- modules/bar/components/Clock.qml | 73 +- modules/bar/components/OsIcon.qml | 35 +- modules/bar/components/Power.qml | 21 +- modules/bar/components/StatusIcons.qml | 43 +- modules/bar/components/Tray.qml | 38 +- modules/bar/components/TrayItem.qml | 10 +- .../components/workspaces/ActiveIndicator.qml | 30 +- .../bar/components/workspaces/OccupiedBg.qml | 26 +- .../workspaces/SpecialWorkspaces.qml | 319 +- .../bar/components/workspaces/Workspace.qml | 29 +- .../bar/components/workspaces/Workspaces.qml | 47 +- modules/bar/popouts/ActiveWindow.qml | 42 +- modules/bar/popouts/Audio.qml | 34 +- modules/bar/popouts/Battery.qml | 54 +- modules/bar/popouts/Bluetooth.qml | 89 +- modules/bar/popouts/ClipWrapper.qml | 67 + modules/bar/popouts/Content.qml | 81 +- modules/bar/popouts/LockStatus.qml | 6 +- modules/bar/popouts/Network.qml | 102 +- modules/bar/popouts/PopoutState.qml | 8 + modules/bar/popouts/TrayMenu.qml | 68 +- modules/bar/popouts/WirelessPassword.qml | 272 +- modules/bar/popouts/Wrapper.qml | 104 +- modules/bar/popouts/kblayout/KbLayout.qml | 85 +- .../bar/popouts/kblayout/KbLayoutModel.qml | 177 +- modules/controlcenter/ControlCenter.qml | 25 +- modules/controlcenter/NavRail.qml | 75 +- modules/controlcenter/PaneRegistry.qml | 18 +- modules/controlcenter/Panes.qml | 44 +- modules/controlcenter/Session.qml | 2 +- modules/controlcenter/WindowFactory.qml | 9 +- modules/controlcenter/WindowTitle.qml | 20 +- .../appearance/AppearancePane.qml | 136 +- .../appearance/sections/AnimationsSection.qml | 10 +- .../appearance/sections/BackgroundSection.qml | 69 +- .../appearance/sections/BorderSection.qml | 14 +- .../sections/ColorSchemeSection.qml | 38 +- .../sections/ColorVariantSection.qml | 28 +- .../appearance/sections/FontsSection.qml | 119 +- .../appearance/sections/ScalesSection.qml | 14 +- .../appearance/sections/ThemeModeSection.qml | 6 +- .../sections/TransparencySection.qml | 12 +- modules/controlcenter/audio/AudioPane.qml | 184 +- modules/controlcenter/bluetooth/BtPane.qml | 11 +- modules/controlcenter/bluetooth/Details.qml | 183 +- .../controlcenter/bluetooth/DeviceList.qml | 76 +- modules/controlcenter/bluetooth/Settings.qml | 154 +- .../components/ConnectedButtonGroup.qml | 114 + .../components/DeviceDetails.qml | 14 +- .../controlcenter/components/DeviceList.qml | 23 +- .../components/PaneTransition.qml | 22 +- .../components/ReadonlySlider.qml | 67 + .../components/SettingsHeader.qml | 10 +- .../controlcenter/components/SliderInput.qml | 52 +- .../components/SplitPaneLayout.qml | 36 +- .../components/SplitPaneWithDetails.qml | 22 +- .../components/WallpaperGrid.qml | 57 +- .../controlcenter/dashboard/DashboardPane.qml | 527 + .../dashboard/GeneralSection.qml | 128 + .../dashboard/PerformanceSection.qml | 106 + .../controlcenter/launcher/LauncherPane.qml | 310 +- modules/controlcenter/launcher/Settings.qml | 83 +- .../controlcenter/monitors/MonitorsPane.qml | 279 +- .../controlcenter/network/EthernetDetails.qml | 16 +- .../controlcenter/network/EthernetList.qml | 48 +- .../controlcenter/network/EthernetPane.qml | 6 +- .../network/EthernetSettings.qml | 26 +- .../controlcenter/network/NetworkSettings.qml | 47 +- .../controlcenter/network/NetworkingPane.qml | 61 +- modules/controlcenter/network/VpnDetails.qml | 263 +- modules/controlcenter/network/VpnList.qml | 472 +- modules/controlcenter/network/VpnSettings.qml | 117 +- .../controlcenter/network/WirelessDetails.qml | 130 +- .../controlcenter/network/WirelessList.qml | 73 +- .../controlcenter/network/WirelessPane.qml | 6 +- .../network/WirelessPasswordDialog.qml | 178 +- .../network/WirelessSettings.qml | 14 +- .../notifications/NotificationsPane.qml | 607 + .../controlcenter/state/BluetoothState.qml | 2 +- modules/controlcenter/taskbar/TaskbarPane.qml | 1283 +- modules/dashboard/Content.qml | 184 +- modules/dashboard/Dash.qml | 34 +- modules/dashboard/LyricMenu.qml | 412 + modules/dashboard/LyricsView.qml | 112 + modules/dashboard/Media.qml | 341 +- modules/dashboard/MediaWrapper.qml | 13 + modules/dashboard/Monitors.qml | 32 +- modules/dashboard/Performance.qml | 893 +- modules/dashboard/Tabs.qml | 104 +- modules/dashboard/WeatherTab.qml | 279 + modules/dashboard/Wrapper.qml | 84 +- modules/dashboard/dash/Calendar.qml | 95 +- modules/dashboard/dash/DateTime.qml | 27 +- modules/dashboard/dash/Media.qml | 117 +- modules/dashboard/dash/Resources.qml | 20 +- modules/dashboard/dash/SmallWeather.qml | 56 + modules/dashboard/dash/User.qml | 53 +- modules/drawers/ContentWindow.qml | 318 + modules/drawers/Drawers.qml | 179 +- modules/drawers/Exclusions.qml | 11 +- modules/drawers/Interactions.qml | 69 +- modules/drawers/Panels.qml | 111 +- modules/drawers/Regions.qml | 84 + modules/launcher/AppList.qml | 43 +- modules/launcher/Content.qml | 43 +- modules/launcher/ContentList.qml | 40 +- modules/launcher/WallpaperList.qml | 12 +- modules/launcher/Wrapper.qml | 108 +- modules/launcher/items/ActionItem.qml | 30 +- modules/launcher/items/AppItem.qml | 52 +- modules/launcher/items/CalcItem.qml | 60 +- modules/launcher/items/SchemeItem.qml | 36 +- modules/launcher/items/VariantItem.qml | 34 +- modules/launcher/items/WallpaperItem.qml | 43 +- modules/launcher/services/Actions.qml | 14 +- modules/launcher/services/Apps.qml | 15 +- modules/launcher/services/M3Variants.qml | 10 +- modules/launcher/services/Schemes.qml | 12 +- modules/lock/Center.qml | 93 +- modules/lock/Content.qml | 30 +- modules/lock/Fetch.qml | 75 +- modules/lock/InputField.qml | 25 +- modules/lock/Lock.qml | 25 +- modules/lock/LockSurface.qml | 65 +- modules/lock/Media.qml | 72 +- modules/lock/NotifDock.qml | 49 +- modules/lock/NotifGroup.qml | 116 +- modules/lock/Pam.qml | 22 +- modules/lock/Resources.qml | 28 +- modules/lock/WeatherInfo.qml | 54 +- modules/notifications/Content.qml | 205 +- modules/notifications/Notification.qml | 180 +- modules/notifications/Wrapper.qml | 31 +- modules/osd/Content.qml | 45 +- modules/osd/Wrapper.qml | 63 +- modules/session/Content.qml | 51 +- modules/session/Wrapper.qml | 53 +- modules/sidebar/Content.qml | 14 +- modules/sidebar/Notif.qml | 144 +- modules/sidebar/NotifActionList.qml | 35 +- modules/sidebar/NotifDock.qml | 87 +- modules/sidebar/NotifDockList.qml | 168 +- modules/sidebar/NotifGroup.qml | 130 +- modules/sidebar/NotifGroupList.qml | 174 +- modules/sidebar/Wrapper.qml | 54 +- modules/utilities/Background.qml | 7 +- modules/utilities/Content.qml | 12 +- modules/utilities/RecordingDeleteModal.qml | 92 +- modules/utilities/Wrapper.qml | 74 +- modules/utilities/cards/IdleInhibit.qml | 49 +- modules/utilities/cards/Record.qml | 113 +- modules/utilities/cards/RecordingList.qml | 47 +- modules/utilities/cards/Toggles.qml | 199 +- modules/utilities/toasts/ToastItem.qml | 28 +- modules/utilities/toasts/Toasts.qml | 33 +- modules/windowinfo/Buttons.qml | 93 +- modules/windowinfo/Details.qml | 36 +- modules/windowinfo/Preview.qml | 31 +- modules/windowinfo/WindowInfo.qml | 24 +- nix/default.nix | 2 - plugin/src/Caelestia/Blobs/CMakeLists.txt | 20 + plugin/src/Caelestia/Blobs/blobgroup.cpp | 104 + plugin/src/Caelestia/Blobs/blobgroup.hpp | 53 + .../src/Caelestia/Blobs/blobinvertedrect.cpp | 184 + .../src/Caelestia/Blobs/blobinvertedrect.hpp | 54 + plugin/src/Caelestia/Blobs/blobmaterial.cpp | 102 + plugin/src/Caelestia/Blobs/blobmaterial.hpp | 47 + plugin/src/Caelestia/Blobs/blobrect.cpp | 245 + plugin/src/Caelestia/Blobs/blobrect.hpp | 126 + plugin/src/Caelestia/Blobs/blobshape.cpp | 392 + plugin/src/Caelestia/Blobs/blobshape.hpp | 79 + plugin/src/Caelestia/Blobs/shaders/blob.frag | 251 + plugin/src/Caelestia/Blobs/shaders/blob.vert | 29 + plugin/src/Caelestia/CMakeLists.txt | 31 +- .../src/Caelestia/Components/CMakeLists.txt | 7 + .../src/Caelestia/Components/lazylistview.cpp | 1108 + .../src/Caelestia/Components/lazylistview.hpp | 243 + plugin/src/Caelestia/Config/CMakeLists.txt | 32 + plugin/src/Caelestia/Config/anim.cpp | 117 + plugin/src/Caelestia/Config/anim.hpp | 78 + .../src/Caelestia/Config/appearanceconfig.cpp | 183 + .../src/Caelestia/Config/appearanceconfig.hpp | 259 + .../src/Caelestia/Config/backgroundconfig.hpp | 84 + plugin/src/Caelestia/Config/barconfig.hpp | 166 + plugin/src/Caelestia/Config/borderconfig.hpp | 29 + plugin/src/Caelestia/Config/config.cpp | 120 + plugin/src/Caelestia/Config/config.hpp | 86 + .../src/Caelestia/Config/configattached.cpp | 96 + .../src/Caelestia/Config/configattached.hpp | 98 + plugin/src/Caelestia/Config/configobject.cpp | 316 + plugin/src/Caelestia/Config/configobject.hpp | 143 + .../Caelestia/Config/controlcenterconfig.hpp | 18 + .../src/Caelestia/Config/dashboardconfig.hpp | 44 + plugin/src/Caelestia/Config/generalconfig.hpp | 108 + .../src/Caelestia/Config/launcherconfig.hpp | 135 + plugin/src/Caelestia/Config/lockconfig.hpp | 21 + .../Caelestia/Config/monitorconfigmanager.cpp | 64 + .../Caelestia/Config/monitorconfigmanager.hpp | 35 + plugin/src/Caelestia/Config/notifsconfig.hpp | 28 + plugin/src/Caelestia/Config/osdconfig.hpp | 21 + plugin/src/Caelestia/Config/rootconfig.cpp | 258 + plugin/src/Caelestia/Config/rootconfig.hpp | 59 + plugin/src/Caelestia/Config/serviceconfig.hpp | 43 + plugin/src/Caelestia/Config/sessionconfig.hpp | 57 + plugin/src/Caelestia/Config/sidebarconfig.hpp | 19 + plugin/src/Caelestia/Config/tokens.cpp | 54 + plugin/src/Caelestia/Config/tokens.hpp | 351 + .../src/Caelestia/Config/tokensattached.cpp | 118 + .../src/Caelestia/Config/tokensattached.hpp | 78 + plugin/src/Caelestia/Config/userpaths.hpp | 30 + .../src/Caelestia/Config/utilitiesconfig.hpp | 88 + plugin/src/Caelestia/Config/winfoconfig.hpp | 18 + plugin/src/Caelestia/Images/CMakeLists.txt | 11 + .../Caelestia/Images/cachingimageprovider.cpp | 120 + .../Caelestia/Images/cachingimageprovider.hpp | 21 + plugin/src/Caelestia/Images/imagecacher.cpp | 159 + plugin/src/Caelestia/Images/imagecacher.hpp | 38 + plugin/src/Caelestia/Images/iutils.cpp | 42 + plugin/src/Caelestia/Images/iutils.hpp | 24 + plugin/src/Caelestia/Internal/CMakeLists.txt | 14 +- plugin/src/Caelestia/Internal/arcgauge.cpp | 119 + plugin/src/Caelestia/Internal/arcgauge.hpp | 61 + .../src/Caelestia/Internal/circularbuffer.cpp | 94 + .../src/Caelestia/Internal/circularbuffer.hpp | 44 + .../Internal/circularindicatormanager.cpp | 12 +- plugin/src/Caelestia/Internal/hyprextras.cpp | 30 +- plugin/src/Caelestia/Internal/hyprextras.hpp | 6 +- .../src/Caelestia/Internal/logindmanager.cpp | 14 +- .../src/Caelestia/Internal/sparklineitem.cpp | 216 + .../src/Caelestia/Internal/sparklineitem.hpp | 91 + .../src/Caelestia/Internal/visualiserbars.cpp | 198 + .../src/Caelestia/Internal/visualiserbars.hpp | 72 + .../src/Caelestia/Models/filesystemmodel.cpp | 44 +- .../src/Caelestia/Models/filesystemmodel.hpp | 2 +- .../src/Caelestia/Services/audiocollector.cpp | 24 +- .../src/Caelestia/Services/audioprovider.cpp | 7 +- .../src/Caelestia/Services/audioprovider.hpp | 2 +- plugin/src/Caelestia/Services/beattracker.cpp | 4 +- .../src/Caelestia/Services/cavaprovider.cpp | 9 +- plugin/src/Caelestia/Services/service.cpp | 1 - plugin/src/Caelestia/appdb.cpp | 85 +- plugin/src/Caelestia/appdb.hpp | 10 + plugin/src/Caelestia/cutils.cpp | 34 +- plugin/src/Caelestia/imageanalyser.cpp | 18 +- plugin/src/Caelestia/imageanalyser.hpp | 3 +- plugin/src/Caelestia/qalculator.cpp | 98 +- plugin/src/Caelestia/qalculator.hpp | 23 + plugin/src/Caelestia/requests.cpp | 29 +- plugin/src/Caelestia/requests.hpp | 4 +- plugin/src/Caelestia/toaster.cpp | 1 - scripts/qml-lint-conventions.py | 648 + services/Audio.qml | 96 +- services/Brightness.qml | 45 +- services/Colours.qml | 40 +- services/GameMode.qml | 22 +- services/Hypr.qml | 89 +- services/IdleInhibitor.qml | 4 +- services/LyricsService.qml | 403 + services/Monitors.qml | 41 +- services/Network.qml | 130 +- services/NetworkUsage.qml | 229 + services/Nmcli.qml | 599 +- services/NotifData.qml | 243 + services/Notifs.qml | 238 +- services/Players.qml | 47 +- services/Recorder.qml | 69 +- services/Screens.qml | 14 + services/SystemUsage.qml | 176 +- services/Time.qml | 7 +- services/VPN.qml | 353 +- services/Visibilities.qml | 6 +- services/Wallpapers.qml | 17 +- services/Weather.qml | 71 +- shell.qml | 11 +- utils/Icons.qml | 56 +- utils/NetworkConnection.qml | 2 +- utils/Paths.qml | 7 +- utils/Searcher.qml | 3 +- utils/Strings.qml | 26 + utils/SysInfo.qml | 16 +- utils/scripts/lrcparser.js | 62 + 369 files changed, 74974 insertions(+), 7660 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/crash.yml create mode 100644 .github/workflows/check-format.yml create mode 100644 .github/workflows/lint.yml create mode 100644 .github/workflows/update-image.yml create mode 100644 all_changes.patch create mode 100644 assets/shaders/fade.frag create mode 100644 assets/shaders/fade.frag.qsb create mode 100644 clean-snapshot.tar create mode 100644 components/AnchorAnim.qml create mode 100644 components/DashboardState.qml create mode 100644 components/DrawerVisibilities.qml create mode 100644 components/Logo.qml create mode 100644 last-50-commits.txt create mode 100644 modules/ConfigToasts.qml create mode 100644 modules/bar/popouts/ClipWrapper.qml create mode 100644 modules/bar/popouts/PopoutState.qml create mode 100644 modules/controlcenter/components/ConnectedButtonGroup.qml create mode 100644 modules/controlcenter/components/ReadonlySlider.qml create mode 100644 modules/controlcenter/dashboard/DashboardPane.qml create mode 100644 modules/controlcenter/dashboard/GeneralSection.qml create mode 100644 modules/controlcenter/dashboard/PerformanceSection.qml create mode 100644 modules/controlcenter/notifications/NotificationsPane.qml create mode 100644 modules/dashboard/LyricMenu.qml create mode 100644 modules/dashboard/LyricsView.qml create mode 100644 modules/dashboard/MediaWrapper.qml create mode 100644 modules/dashboard/WeatherTab.qml create mode 100644 modules/dashboard/dash/SmallWeather.qml create mode 100644 modules/drawers/ContentWindow.qml create mode 100644 modules/drawers/Regions.qml create mode 100644 plugin/src/Caelestia/Blobs/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Blobs/blobgroup.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobgroup.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobinvertedrect.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobinvertedrect.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobmaterial.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobmaterial.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobrect.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobrect.hpp create mode 100644 plugin/src/Caelestia/Blobs/blobshape.cpp create mode 100644 plugin/src/Caelestia/Blobs/blobshape.hpp create mode 100644 plugin/src/Caelestia/Blobs/shaders/blob.frag create mode 100644 plugin/src/Caelestia/Blobs/shaders/blob.vert create mode 100644 plugin/src/Caelestia/Components/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Components/lazylistview.cpp create mode 100644 plugin/src/Caelestia/Components/lazylistview.hpp create mode 100644 plugin/src/Caelestia/Config/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Config/anim.cpp create mode 100644 plugin/src/Caelestia/Config/anim.hpp create mode 100644 plugin/src/Caelestia/Config/appearanceconfig.cpp create mode 100644 plugin/src/Caelestia/Config/appearanceconfig.hpp create mode 100644 plugin/src/Caelestia/Config/backgroundconfig.hpp create mode 100644 plugin/src/Caelestia/Config/barconfig.hpp create mode 100644 plugin/src/Caelestia/Config/borderconfig.hpp create mode 100644 plugin/src/Caelestia/Config/config.cpp create mode 100644 plugin/src/Caelestia/Config/config.hpp create mode 100644 plugin/src/Caelestia/Config/configattached.cpp create mode 100644 plugin/src/Caelestia/Config/configattached.hpp create mode 100644 plugin/src/Caelestia/Config/configobject.cpp create mode 100644 plugin/src/Caelestia/Config/configobject.hpp create mode 100644 plugin/src/Caelestia/Config/controlcenterconfig.hpp create mode 100644 plugin/src/Caelestia/Config/dashboardconfig.hpp create mode 100644 plugin/src/Caelestia/Config/generalconfig.hpp create mode 100644 plugin/src/Caelestia/Config/launcherconfig.hpp create mode 100644 plugin/src/Caelestia/Config/lockconfig.hpp create mode 100644 plugin/src/Caelestia/Config/monitorconfigmanager.cpp create mode 100644 plugin/src/Caelestia/Config/monitorconfigmanager.hpp create mode 100644 plugin/src/Caelestia/Config/notifsconfig.hpp create mode 100644 plugin/src/Caelestia/Config/osdconfig.hpp create mode 100644 plugin/src/Caelestia/Config/rootconfig.cpp create mode 100644 plugin/src/Caelestia/Config/rootconfig.hpp create mode 100644 plugin/src/Caelestia/Config/serviceconfig.hpp create mode 100644 plugin/src/Caelestia/Config/sessionconfig.hpp create mode 100644 plugin/src/Caelestia/Config/sidebarconfig.hpp create mode 100644 plugin/src/Caelestia/Config/tokens.cpp create mode 100644 plugin/src/Caelestia/Config/tokens.hpp create mode 100644 plugin/src/Caelestia/Config/tokensattached.cpp create mode 100644 plugin/src/Caelestia/Config/tokensattached.hpp create mode 100644 plugin/src/Caelestia/Config/userpaths.hpp create mode 100644 plugin/src/Caelestia/Config/utilitiesconfig.hpp create mode 100644 plugin/src/Caelestia/Config/winfoconfig.hpp create mode 100644 plugin/src/Caelestia/Images/CMakeLists.txt create mode 100644 plugin/src/Caelestia/Images/cachingimageprovider.cpp create mode 100644 plugin/src/Caelestia/Images/cachingimageprovider.hpp create mode 100644 plugin/src/Caelestia/Images/imagecacher.cpp create mode 100644 plugin/src/Caelestia/Images/imagecacher.hpp create mode 100644 plugin/src/Caelestia/Images/iutils.cpp create mode 100644 plugin/src/Caelestia/Images/iutils.hpp create mode 100644 plugin/src/Caelestia/Internal/arcgauge.cpp create mode 100644 plugin/src/Caelestia/Internal/arcgauge.hpp create mode 100644 plugin/src/Caelestia/Internal/circularbuffer.cpp create mode 100644 plugin/src/Caelestia/Internal/circularbuffer.hpp create mode 100644 plugin/src/Caelestia/Internal/sparklineitem.cpp create mode 100644 plugin/src/Caelestia/Internal/sparklineitem.hpp create mode 100644 plugin/src/Caelestia/Internal/visualiserbars.cpp create mode 100644 plugin/src/Caelestia/Internal/visualiserbars.hpp create mode 100755 scripts/qml-lint-conventions.py create mode 100644 services/LyricsService.qml create mode 100644 services/NetworkUsage.qml create mode 100644 services/NotifData.qml create mode 100644 services/Screens.qml create mode 100644 utils/Strings.qml create mode 100644 utils/scripts/lrcparser.js diff --git a/.envrc b/.envrc index c90b500c9..5a259560a 100644 --- a/.envrc +++ b/.envrc @@ -3,10 +3,11 @@ if has nix; then fi shopt -s globstar -watch_file assets/cpp/**/*.cpp -watch_file assets/cpp/**/*.hpp watch_file plugin/**/*.cpp watch_file plugin/**/*.hpp +watch_file plugin/**/*.qml +watch_file plugin/**/*.vert +watch_file plugin/**/*.frag watch_file **/CMakeLists.txt cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml new file mode 100644 index 000000000..e37198c91 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/crash.yml @@ -0,0 +1,57 @@ +name: Crash report +description: Report a crash +labels: ["bug", "crash"] +type: "Bug" +title: "[CRASH] " +body: + - type: textarea + attributes: + label: What caused the crash + description: | + Any information likely to help debug the crash. What were you doing when the crash occurred, + what changes did you make, can you get it to happen again? + + - type: upload + attributes: + label: Report file + description: Attach `report.txt` here. + validations: + required: true + + - type: upload + attributes: + label: Log file + description: | + Attach `log.qslog.log` here. If it is too big to upload, compress it. + + You can preview the log if you'd like using `qs log -r '*=true'`. + validations: + required: true + + - type: textarea + attributes: + label: "Version info" + description: Run `caelestia -v` and paste the result below. + value: | +
Version info + + ``` + + ``` + +
+ validations: + required: true + + - type: checkboxes + attributes: + label: Reminder + options: + - label: I've successfully updated to the latest versions following the [updating guide](https://github.com/caelestia-dots/caelestia?tab=readme-ov-file#updating). + required: false + - label: I've successfully updated the system packages to the latest. + required: false + - label: I agree that it's usually impossible for others to help me without my logs. + required: true + - label: I've ticked the checkboxes without reading their contents + required: false diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml new file mode 100644 index 000000000..ea5fe9ef0 --- /dev/null +++ b/.github/workflows/check-format.yml @@ -0,0 +1,38 @@ +name: Check formatting + +on: + push: + branches: + - main + pull_request: + +jobs: + check-qml: + runs-on: ubuntu-latest + container: + image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest + + steps: + - uses: actions/checkout@v6 + + - name: Check QML format + shell: fish {0} + run: | + for file in (string match -v 'build/*' **.qml) + /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 + end + python3 scripts/qml-lint-conventions.py + + check-cpp: + runs-on: ubuntu-latest + container: + image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest + + steps: + - uses: actions/checkout@v6 + + - name: Check C++ format + shell: fish {0} + run: | + find plugin extras -name '*.cpp' -o -name '*.hpp' \ + | xargs clang-format --dry-run --Werror diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..7962963ca --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,43 @@ +name: Lint code + +on: + push: + branches: + - main + pull_request: + +jobs: + lint: + runs-on: ubuntu-latest + + container: + image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest + + steps: + - uses: actions/checkout@v6 + + - name: Build + run: | + # Set version and git rev for CI build so we don't call git + cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror -DVERSION= -DGIT_REVISION= + cmake --build build + + - name: Lint QML + shell: fish {0} + run: | + # Generate tooling + touch .qmlls.ini + QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p . + + # Construct linter args + set -l build_dir (grep -oP "(?<=buildDir=\")(.*)(?=\")" .qmlls.ini) + set -l import_paths (grep -oP "(?<=importPaths=\")(.*)(?=\")" .qmlls.ini | string split :) + set -l args -I $build_dir + for path in $import_paths + set -a args -I $path + end + set -l qml_files (string match -vr '(build|modules/controlcenter)/.*' **.qml) + + # Lint + set -l lint_out (/usr/lib/qt6/bin/qmllint --import disable $args $qml_files 2>&1 | tee /dev/stderr) + test -z "$lint_out" || exit 1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ac4377231..29e704a96 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,7 +3,7 @@ name: Create release on: push: tags: - - 'v*' + - "v*" jobs: build-and-release: @@ -13,7 +13,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Create packages run: | @@ -35,3 +35,8 @@ jobs: caelestia-shell-${{ github.ref_name }}.tar.gz caelestia-shell-latest.tar.gz generate_release_notes: true + + - name: Update stable branch + run: | + git branch -f stable "$GITHUB_SHA" + git push origin stable --force diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml index 1a8bd071f..7f24c7978 100644 --- a/.github/workflows/update-flake-inputs.yml +++ b/.github/workflows/update-flake-inputs.yml @@ -3,27 +3,33 @@ name: Update flake inputs on: workflow_dispatch: schedule: - - cron: '0 0 * * 0' + - cron: "0 0 * * 0" jobs: update-flake: runs-on: ubuntu-latest - permissions: - contents: write - steps: - - uses: actions/checkout@v4 + - name: Generate app token + id: app-token + uses: actions/create-github-app-token@v3 + with: + client-id: ${{ secrets.CLIENT_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - uses: actions/checkout@v6 + with: + token: ${{ steps.app-token.outputs.token }} - name: Install Nix - uses: nixbuild/nix-quick-install-action@v31 + uses: nixbuild/nix-quick-install-action@v34 with: nix_conf: | keep-env-derivations = true keep-outputs = true - name: Restore and save Nix store - uses: nix-community/cache-nix-action@v6 + uses: nix-community/cache-nix-action@v7 with: # restore and save a cache using this key primary-key: nix-${{ hashFiles('**/*.nix', '**/flake.lock') }} @@ -78,8 +84,9 @@ jobs: - name: Commit and push changes if: steps.check.outputs.modified == 'true' - uses: EndBug/add-and-commit@v9 + uses: EndBug/add-and-commit@v10 with: + github_token: ${{ steps.app-token.outputs.token }} add: flake.lock default_author: github_actions message: "[CI] chore: update flake" diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml new file mode 100644 index 000000000..74fe5435c --- /dev/null +++ b/.github/workflows/update-image.yml @@ -0,0 +1,46 @@ +name: Update Docker CI image + +on: + workflow_dispatch: + schedule: + - cron: "0 0 * * 0" + +permissions: + packages: write + +jobs: + build-image: + runs-on: ubuntu-latest + + steps: + - uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Write Dockerfile + run: | + cat > /tmp/Dockerfile <> /etc/sudoers && \ + sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ + cd /home/builder/yay-bin && \ + sudo -u builder makepkg -si --noconfirm && \ + sudo -u builder yay -S --noconfirm quickshell-git libcava && \ + sudo -u builder yay -Yc --noconfirm && \ + pacman -Rns --noconfirm yay-bin && \ + sed -i '/builder ALL=(ALL) NOPASSWD:ALL/d' /etc/sudoers && \ + userdel -r builder && \ + pacman -Scc --noconfirm + EOF + + - name: Build and push + uses: docker/build-push-action@v7 + with: + context: . + file: /tmp/Dockerfile + push: true + tags: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest diff --git a/.gitignore b/.gitignore index a114b1bcd..c30a6d926 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ /.qmlls.ini build/ .cache/ +logs \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 7b95855b5..b23f7b485 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -60,8 +60,14 @@ if("plugin" IN_LIST ENABLE_MODULES) endif() if("shell" IN_LIST ENABLE_MODULES) - foreach(dir assets components config modules services utils) + foreach(dir assets components modules services utils) install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") endforeach() - install(FILES shell.qml LICENSE DESTINATION "${INSTALL_QSCONFDIR}") + + file(READ shell.qml SHELL_QML) + string(REPLACE "settings.watchFiles: true" "settings.watchFiles: false" SHELL_QML "${SHELL_QML}") + file(WRITE "${CMAKE_BINARY_DIR}/qml/shell.qml" "${SHELL_QML}") + install(FILES "${CMAKE_BINARY_DIR}/qml/shell.qml" DESTINATION "${INSTALL_QSCONFDIR}") + + install(FILES LICENSE DESTINATION "${INSTALL_QSCONFDIR}") endif() diff --git a/README.md b/README.md index 6bbc8db9c..13b5ec8a2 100644 --- a/README.md +++ b/README.md @@ -205,20 +205,66 @@ git pull ## Configuring All configuration options should be put in `~/.config/caelestia/shell.json`. This file is _not_ created by -default, you must create it manually. +default, you must create it manually. Options that you omit from the config file will use their default +values. + +### Per-monitor configuration + +You can configure options per-monitor in `~/.config/caelestia/monitors//shell.json`. Options +set in this file will **override** the respective options in the global config. Otherwise, the options will +use their values from the global config. + +For example, to disable the bar on DP-1: + +**`~/.config/caelestia/monitors/DP-1/shell.json`** + +```json +{ + "bar": { + "persistent": false + } +} +``` + +> [!NOTE] +> Not all options are respect per-monitor overrides. Most notably, the following options will only read +> from the global config, and ignore the respective option in per-monitor config files. +> +>
Ignored options +> +> - `appearance` (`anim`, `transparency`) +> - `general` (`logo`, `apps`, `idle`, `battery`) +> - `bar.workspaces` (`perMonitorWorkspaces`, `specialWorkspaceIcons`, `windowIcons`) +> - `bar.tray` (`iconSubs`, `hiddenIcons`) +> - `dashboard` (`mediaUpdateInterval`, `resourceUpdateInterval`) +> - `launcher` (`specialPrefix`, `actionPrefix`, `enableDangerousActions`, `vimKeybinds`, +> `favouriteApps`, `hiddenApps`, `actions`) +> - `launcher.useFuzzy` (`apps`, `actions`, `schemes`, `variants`, `wallpapers`) +> - `notifs` (`expire`, `fullscreen`, `defaultExpireTimeout`, `actionOnClick`) +> - `lock` (`enableFprint`, `maxFprintTries`) +> - `utilities` (`toasts`, `vpn`) +> - `services` (`weatherLocation`, `useFahrenheit`, `useFahrenheitPerformance`, `useTwelveHourClock`, +> `gpuType`, `visualiserBars`, `audioIncrement`, `brightnessIncrement`, `maxVolume`, `smartScheme`, +> `defaultPlayer`, `playerAliases`, `showLyrics`, `lyricsBackend`) +> - `paths` (`wallpaperDir`, `lyricsDir`) +> +>
### Example configuration > [!NOTE] -> The example configuration only includes recommended configuration options. For more advanced customisation -> such as modifying the size of individual items or changing constants in the code, there are some other -> options which can be found in the source files in the `config` directory. +> The example configuration includes ALL configuration options in `shell.json`. You are +> **not** recommended to copy and paste this entire configuration into `shell.json`. +> This is meant to serve as a reference of all the available options, and you should +> only add the ones you want to change to `shell.json`.
Example ```json { + "enabled": true, "appearance": { + "deformScale": 1, "anim": { "durations": { "scale": 1 @@ -251,6 +297,10 @@ default, you must create it manually. } }, "general": { + "logo": "caelestia", + "showOverFullscreen": false, + "mediaGifSpeedAdjustment": 300, + "sessionGifSpeed": 0.7, "apps": { "terminal": ["foot"], "audio": ["pavucontrol"], @@ -328,7 +378,14 @@ default, you must create it manually. } }, "bar": { + "activeWindow": { + "compact": false, + "inverted": false, + "showOnHover": true + }, "clock": { + "background": false, + "showDate": false, "showIcon": true }, "dragThreshold": 20, @@ -413,6 +470,12 @@ default, you must create it manually. "name": "steam", "icon": "sports_esports" } + ], + "windowIcons": [ + { + "regex": "steam(_app_(default|[0-9]+))?", + "icon": "sports_esports" + } ] }, "excludedScreens": [""], @@ -422,13 +485,18 @@ default, you must create it manually. }, "border": { "rounding": 25, + "smoothing": 32, "thickness": 10 }, "dashboard": { "enabled": true, + "showOnHover": true, + "showDashboard": true, + "showMedia": true, + "showPerformance": true, + "showWeather": true, "dragThreshold": 50, - "mediaUpdateInterval": 500, - "showOnHover": true + "mediaUpdateInterval": 500 }, "launcher": { "actionPrefix": ">", @@ -560,10 +628,12 @@ default, you must create it manually. "wallpapers": false }, "showOnHover": false, + "favouriteApps": [], "hiddenApps": [] }, "lock": { - "recolourLogo": false + "recolourLogo": false, + "hideNotifs": false }, "notifs": { "actionOnClick": false, @@ -582,7 +652,10 @@ default, you must create it manually. "paths": { "mediaGif": "root:/assets/bongocat.gif", "sessionGif": "root:/assets/kurukuru.gif", - "wallpaperDir": "~/Pictures/Wallpapers" + "noNotifsPic": "root:/assets/dino.png", + "lockNoNotifsPic": "root:/assets/dino.png", + "wallpaperDir": "~/Pictures/Wallpapers", + "lyricsDir": "~/Music/lyrics" }, "services": { "audioIncrement": 0.1, @@ -593,6 +666,7 @@ default, you must create it manually. "playerAliases": [{ "from": "com.github.th_ch.youtube_music", "to": "YT Music" }], "weatherLocation": "", "useFahrenheit": false, + "useFahrenheitPerformance": false, "useTwelveHourClock": false, "smartScheme": true, "visualiserBars": 45 @@ -601,6 +675,12 @@ default, you must create it manually. "dragThreshold": 30, "enabled": true, "vimKeybinds": false, + "icons": { + "logout": "logout", + "shutdown": "power_settings_new", + "hibernate": "downloading", + "reboot": "cached" + }, "commands": { "logout": ["loginctl", "terminate-user", ""], "shutdown": ["systemctl", "poweroff"], @@ -639,13 +719,59 @@ default, you must create it manually. "enabled": false } ] - } + }, + "quickToggles": [ + { + "id": "wifi", + "enabled": true + }, + { + "id": "bluetooth", + "enabled": true + }, + { + "id": "mic", + "enabled": true + }, + { + "enabled": true, + "id": "settings" + }, + { + "id": "gameMode", + "enabled": true + }, + { + "id": "dnd", + "enabled": true + }, + { + "id": "vpn", + "enabled": true + } + ] } } ```
+### Advanced configuration + +> [!WARNING] +> Do NOT change any of these options if you do not know what you are doing. These options control the +> tokens used internally within the shell, and can cause visual issues if changed. The existence of +> the options are also not guaranteed across versions, and may change or be removed without notice. + +A separate `~/.config/caelestia/shell-tokens.json` file allows editing the internal tokens without +touching the source code of the shell. These tokens affect, for example, individual rounding, +spacing, padding, font size, animation duration and easing curves tokens, and the sizes of certain +components. The appearance scale values in `shell.json` are multiplied against these base +token values to produce the final computed values. + +Per-monitor token overrides are also available at +`~/.config/caelestia/monitors//shell-tokens.json`. + ### Home Manager Module For NixOS users, a home manager module is also available. diff --git a/all_changes.patch b/all_changes.patch new file mode 100644 index 000000000..2d737a143 --- /dev/null +++ b/all_changes.patch @@ -0,0 +1,51777 @@ +diff --git a/.envrc b/.envrc +index c90b500c..5a259560 100644 +--- a/.envrc ++++ b/.envrc +@@ -3,10 +3,11 @@ if has nix; then + fi + + shopt -s globstar +-watch_file assets/cpp/**/*.cpp +-watch_file assets/cpp/**/*.hpp + watch_file plugin/**/*.cpp + watch_file plugin/**/*.hpp ++watch_file plugin/**/*.qml ++watch_file plugin/**/*.vert ++watch_file plugin/**/*.frag + watch_file **/CMakeLists.txt + + cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv +diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml +new file mode 100644 +index 00000000..e37198c9 +--- /dev/null ++++ b/.github/ISSUE_TEMPLATE/crash.yml +@@ -0,0 +1,57 @@ ++name: Crash report ++description: Report a crash ++labels: ["bug", "crash"] ++type: "Bug" ++title: "[CRASH] " ++body: ++ - type: textarea ++ attributes: ++ label: What caused the crash ++ description: | ++ Any information likely to help debug the crash. What were you doing when the crash occurred, ++ what changes did you make, can you get it to happen again? ++ ++ - type: upload ++ attributes: ++ label: Report file ++ description: Attach `report.txt` here. ++ validations: ++ required: true ++ ++ - type: upload ++ attributes: ++ label: Log file ++ description: | ++ Attach `log.qslog.log` here. If it is too big to upload, compress it. ++ ++ You can preview the log if you'd like using `qs log -r '*=true'`. ++ validations: ++ required: true ++ ++ - type: textarea ++ attributes: ++ label: "Version info" ++ description: Run `caelestia -v` and paste the result below. ++ value: | ++
Version info ++ ++ ``` ++ ++ ``` ++ ++
++ validations: ++ required: true ++ ++ - type: checkboxes ++ attributes: ++ label: Reminder ++ options: ++ - label: I've successfully updated to the latest versions following the [updating guide](https://github.com/caelestia-dots/caelestia?tab=readme-ov-file#updating). ++ required: false ++ - label: I've successfully updated the system packages to the latest. ++ required: false ++ - label: I agree that it's usually impossible for others to help me without my logs. ++ required: true ++ - label: I've ticked the checkboxes without reading their contents ++ required: false +diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml +new file mode 100644 +index 00000000..ea5fe9ef +--- /dev/null ++++ b/.github/workflows/check-format.yml +@@ -0,0 +1,38 @@ ++name: Check formatting ++ ++on: ++ push: ++ branches: ++ - main ++ pull_request: ++ ++jobs: ++ check-qml: ++ runs-on: ubuntu-latest ++ container: ++ image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest ++ ++ steps: ++ - uses: actions/checkout@v6 ++ ++ - name: Check QML format ++ shell: fish {0} ++ run: | ++ for file in (string match -v 'build/*' **.qml) ++ /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 ++ end ++ python3 scripts/qml-lint-conventions.py ++ ++ check-cpp: ++ runs-on: ubuntu-latest ++ container: ++ image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest ++ ++ steps: ++ - uses: actions/checkout@v6 ++ ++ - name: Check C++ format ++ shell: fish {0} ++ run: | ++ find plugin extras -name '*.cpp' -o -name '*.hpp' \ ++ | xargs clang-format --dry-run --Werror +diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml +new file mode 100644 +index 00000000..7962963c +--- /dev/null ++++ b/.github/workflows/lint.yml +@@ -0,0 +1,43 @@ ++name: Lint code ++ ++on: ++ push: ++ branches: ++ - main ++ pull_request: ++ ++jobs: ++ lint: ++ runs-on: ubuntu-latest ++ ++ container: ++ image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest ++ ++ steps: ++ - uses: actions/checkout@v6 ++ ++ - name: Build ++ run: | ++ # Set version and git rev for CI build so we don't call git ++ cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror -DVERSION= -DGIT_REVISION= ++ cmake --build build ++ ++ - name: Lint QML ++ shell: fish {0} ++ run: | ++ # Generate tooling ++ touch .qmlls.ini ++ QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p . ++ ++ # Construct linter args ++ set -l build_dir (grep -oP "(?<=buildDir=\")(.*)(?=\")" .qmlls.ini) ++ set -l import_paths (grep -oP "(?<=importPaths=\")(.*)(?=\")" .qmlls.ini | string split :) ++ set -l args -I $build_dir ++ for path in $import_paths ++ set -a args -I $path ++ end ++ set -l qml_files (string match -vr '(build|modules/controlcenter)/.*' **.qml) ++ ++ # Lint ++ set -l lint_out (/usr/lib/qt6/bin/qmllint --import disable $args $qml_files 2>&1 | tee /dev/stderr) ++ test -z "$lint_out" || exit 1 +diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml +index ac437723..29e704a9 100644 +--- a/.github/workflows/release.yml ++++ b/.github/workflows/release.yml +@@ -3,7 +3,7 @@ name: Create release + on: + push: + tags: +- - 'v*' ++ - "v*" + + jobs: + build-and-release: +@@ -13,7 +13,7 @@ jobs: + contents: write + + steps: +- - uses: actions/checkout@v4 ++ - uses: actions/checkout@v6 + + - name: Create packages + run: | +@@ -35,3 +35,8 @@ jobs: + caelestia-shell-${{ github.ref_name }}.tar.gz + caelestia-shell-latest.tar.gz + generate_release_notes: true ++ ++ - name: Update stable branch ++ run: | ++ git branch -f stable "$GITHUB_SHA" ++ git push origin stable --force +diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml +index 1a8bd071..7f24c797 100644 +--- a/.github/workflows/update-flake-inputs.yml ++++ b/.github/workflows/update-flake-inputs.yml +@@ -3,27 +3,33 @@ name: Update flake inputs + on: + workflow_dispatch: + schedule: +- - cron: '0 0 * * 0' ++ - cron: "0 0 * * 0" + + jobs: + update-flake: + runs-on: ubuntu-latest + +- permissions: +- contents: write +- + steps: +- - uses: actions/checkout@v4 ++ - name: Generate app token ++ id: app-token ++ uses: actions/create-github-app-token@v3 ++ with: ++ client-id: ${{ secrets.CLIENT_ID }} ++ private-key: ${{ secrets.APP_PRIVATE_KEY }} ++ ++ - uses: actions/checkout@v6 ++ with: ++ token: ${{ steps.app-token.outputs.token }} + + - name: Install Nix +- uses: nixbuild/nix-quick-install-action@v31 ++ uses: nixbuild/nix-quick-install-action@v34 + with: + nix_conf: | + keep-env-derivations = true + keep-outputs = true + + - name: Restore and save Nix store +- uses: nix-community/cache-nix-action@v6 ++ uses: nix-community/cache-nix-action@v7 + with: + # restore and save a cache using this key + primary-key: nix-${{ hashFiles('**/*.nix', '**/flake.lock') }} +@@ -78,8 +84,9 @@ jobs: + + - name: Commit and push changes + if: steps.check.outputs.modified == 'true' +- uses: EndBug/add-and-commit@v9 ++ uses: EndBug/add-and-commit@v10 + with: ++ github_token: ${{ steps.app-token.outputs.token }} + add: flake.lock + default_author: github_actions + message: "[CI] chore: update flake" +diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml +new file mode 100644 +index 00000000..74fe5435 +--- /dev/null ++++ b/.github/workflows/update-image.yml +@@ -0,0 +1,46 @@ ++name: Update Docker CI image ++ ++on: ++ workflow_dispatch: ++ schedule: ++ - cron: "0 0 * * 0" ++ ++permissions: ++ packages: write ++ ++jobs: ++ build-image: ++ runs-on: ubuntu-latest ++ ++ steps: ++ - uses: docker/login-action@v4 ++ with: ++ registry: ghcr.io ++ username: ${{ github.actor }} ++ password: ${{ secrets.GITHUB_TOKEN }} ++ ++ - name: Write Dockerfile ++ run: | ++ cat > /tmp/Dockerfile <> /etc/sudoers && \ ++ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ ++ cd /home/builder/yay-bin && \ ++ sudo -u builder makepkg -si --noconfirm && \ ++ sudo -u builder yay -S --noconfirm quickshell-git libcava && \ ++ sudo -u builder yay -Yc --noconfirm && \ ++ pacman -Rns --noconfirm yay-bin && \ ++ sed -i '/builder ALL=(ALL) NOPASSWD:ALL/d' /etc/sudoers && \ ++ userdel -r builder && \ ++ pacman -Scc --noconfirm ++ EOF ++ ++ - name: Build and push ++ uses: docker/build-push-action@v7 ++ with: ++ context: . ++ file: /tmp/Dockerfile ++ push: true ++ tags: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest +diff --git a/.gitignore b/.gitignore +index a114b1bc..c30a6d92 100644 +--- a/.gitignore ++++ b/.gitignore +@@ -3,3 +3,4 @@ + /.qmlls.ini + build/ + .cache/ ++logs +\ No newline at end of file +diff --git a/CMakeLists.txt b/CMakeLists.txt +index 7b95855b..b23f7b48 100644 +--- a/CMakeLists.txt ++++ b/CMakeLists.txt +@@ -60,8 +60,14 @@ if("plugin" IN_LIST ENABLE_MODULES) + endif() + + if("shell" IN_LIST ENABLE_MODULES) +- foreach(dir assets components config modules services utils) ++ foreach(dir assets components modules services utils) + install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") + endforeach() +- install(FILES shell.qml LICENSE DESTINATION "${INSTALL_QSCONFDIR}") ++ ++ file(READ shell.qml SHELL_QML) ++ string(REPLACE "settings.watchFiles: true" "settings.watchFiles: false" SHELL_QML "${SHELL_QML}") ++ file(WRITE "${CMAKE_BINARY_DIR}/qml/shell.qml" "${SHELL_QML}") ++ install(FILES "${CMAKE_BINARY_DIR}/qml/shell.qml" DESTINATION "${INSTALL_QSCONFDIR}") ++ ++ install(FILES LICENSE DESTINATION "${INSTALL_QSCONFDIR}") + endif() +diff --git a/README.md b/README.md +index 6bbc8db9..13b5ec8a 100644 +--- a/README.md ++++ b/README.md +@@ -205,20 +205,66 @@ git pull + ## Configuring + + All configuration options should be put in `~/.config/caelestia/shell.json`. This file is _not_ created by +-default, you must create it manually. ++default, you must create it manually. Options that you omit from the config file will use their default ++values. ++ ++### Per-monitor configuration ++ ++You can configure options per-monitor in `~/.config/caelestia/monitors//shell.json`. Options ++set in this file will **override** the respective options in the global config. Otherwise, the options will ++use their values from the global config. ++ ++For example, to disable the bar on DP-1: ++ ++**`~/.config/caelestia/monitors/DP-1/shell.json`** ++ ++```json ++{ ++ "bar": { ++ "persistent": false ++ } ++} ++``` ++ ++> [!NOTE] ++> Not all options are respect per-monitor overrides. Most notably, the following options will only read ++> from the global config, and ignore the respective option in per-monitor config files. ++> ++>
Ignored options ++> ++> - `appearance` (`anim`, `transparency`) ++> - `general` (`logo`, `apps`, `idle`, `battery`) ++> - `bar.workspaces` (`perMonitorWorkspaces`, `specialWorkspaceIcons`, `windowIcons`) ++> - `bar.tray` (`iconSubs`, `hiddenIcons`) ++> - `dashboard` (`mediaUpdateInterval`, `resourceUpdateInterval`) ++> - `launcher` (`specialPrefix`, `actionPrefix`, `enableDangerousActions`, `vimKeybinds`, ++> `favouriteApps`, `hiddenApps`, `actions`) ++> - `launcher.useFuzzy` (`apps`, `actions`, `schemes`, `variants`, `wallpapers`) ++> - `notifs` (`expire`, `fullscreen`, `defaultExpireTimeout`, `actionOnClick`) ++> - `lock` (`enableFprint`, `maxFprintTries`) ++> - `utilities` (`toasts`, `vpn`) ++> - `services` (`weatherLocation`, `useFahrenheit`, `useFahrenheitPerformance`, `useTwelveHourClock`, ++> `gpuType`, `visualiserBars`, `audioIncrement`, `brightnessIncrement`, `maxVolume`, `smartScheme`, ++> `defaultPlayer`, `playerAliases`, `showLyrics`, `lyricsBackend`) ++> - `paths` (`wallpaperDir`, `lyricsDir`) ++> ++>
+ + ### Example configuration + + > [!NOTE] +-> The example configuration only includes recommended configuration options. For more advanced customisation +-> such as modifying the size of individual items or changing constants in the code, there are some other +-> options which can be found in the source files in the `config` directory. ++> The example configuration includes ALL configuration options in `shell.json`. You are ++> **not** recommended to copy and paste this entire configuration into `shell.json`. ++> This is meant to serve as a reference of all the available options, and you should ++> only add the ones you want to change to `shell.json`. + +
Example + + ```json + { ++ "enabled": true, + "appearance": { ++ "deformScale": 1, + "anim": { + "durations": { + "scale": 1 +@@ -251,6 +297,10 @@ default, you must create it manually. + } + }, + "general": { ++ "logo": "caelestia", ++ "showOverFullscreen": false, ++ "mediaGifSpeedAdjustment": 300, ++ "sessionGifSpeed": 0.7, + "apps": { + "terminal": ["foot"], + "audio": ["pavucontrol"], +@@ -328,7 +378,14 @@ default, you must create it manually. + } + }, + "bar": { ++ "activeWindow": { ++ "compact": false, ++ "inverted": false, ++ "showOnHover": true ++ }, + "clock": { ++ "background": false, ++ "showDate": false, + "showIcon": true + }, + "dragThreshold": 20, +@@ -413,6 +470,12 @@ default, you must create it manually. + "name": "steam", + "icon": "sports_esports" + } ++ ], ++ "windowIcons": [ ++ { ++ "regex": "steam(_app_(default|[0-9]+))?", ++ "icon": "sports_esports" ++ } + ] + }, + "excludedScreens": [""], +@@ -422,13 +485,18 @@ default, you must create it manually. + }, + "border": { + "rounding": 25, ++ "smoothing": 32, + "thickness": 10 + }, + "dashboard": { + "enabled": true, ++ "showOnHover": true, ++ "showDashboard": true, ++ "showMedia": true, ++ "showPerformance": true, ++ "showWeather": true, + "dragThreshold": 50, +- "mediaUpdateInterval": 500, +- "showOnHover": true ++ "mediaUpdateInterval": 500 + }, + "launcher": { + "actionPrefix": ">", +@@ -560,10 +628,12 @@ default, you must create it manually. + "wallpapers": false + }, + "showOnHover": false, ++ "favouriteApps": [], + "hiddenApps": [] + }, + "lock": { +- "recolourLogo": false ++ "recolourLogo": false, ++ "hideNotifs": false + }, + "notifs": { + "actionOnClick": false, +@@ -582,7 +652,10 @@ default, you must create it manually. + "paths": { + "mediaGif": "root:/assets/bongocat.gif", + "sessionGif": "root:/assets/kurukuru.gif", +- "wallpaperDir": "~/Pictures/Wallpapers" ++ "noNotifsPic": "root:/assets/dino.png", ++ "lockNoNotifsPic": "root:/assets/dino.png", ++ "wallpaperDir": "~/Pictures/Wallpapers", ++ "lyricsDir": "~/Music/lyrics" + }, + "services": { + "audioIncrement": 0.1, +@@ -593,6 +666,7 @@ default, you must create it manually. + "playerAliases": [{ "from": "com.github.th_ch.youtube_music", "to": "YT Music" }], + "weatherLocation": "", + "useFahrenheit": false, ++ "useFahrenheitPerformance": false, + "useTwelveHourClock": false, + "smartScheme": true, + "visualiserBars": 45 +@@ -601,6 +675,12 @@ default, you must create it manually. + "dragThreshold": 30, + "enabled": true, + "vimKeybinds": false, ++ "icons": { ++ "logout": "logout", ++ "shutdown": "power_settings_new", ++ "hibernate": "downloading", ++ "reboot": "cached" ++ }, + "commands": { + "logout": ["loginctl", "terminate-user", ""], + "shutdown": ["systemctl", "poweroff"], +@@ -639,13 +719,59 @@ default, you must create it manually. + "enabled": false + } + ] +- } ++ }, ++ "quickToggles": [ ++ { ++ "id": "wifi", ++ "enabled": true ++ }, ++ { ++ "id": "bluetooth", ++ "enabled": true ++ }, ++ { ++ "id": "mic", ++ "enabled": true ++ }, ++ { ++ "enabled": true, ++ "id": "settings" ++ }, ++ { ++ "id": "gameMode", ++ "enabled": true ++ }, ++ { ++ "id": "dnd", ++ "enabled": true ++ }, ++ { ++ "id": "vpn", ++ "enabled": true ++ } ++ ] + } + } + ``` + +
+ ++### Advanced configuration ++ ++> [!WARNING] ++> Do NOT change any of these options if you do not know what you are doing. These options control the ++> tokens used internally within the shell, and can cause visual issues if changed. The existence of ++> the options are also not guaranteed across versions, and may change or be removed without notice. ++ ++A separate `~/.config/caelestia/shell-tokens.json` file allows editing the internal tokens without ++touching the source code of the shell. These tokens affect, for example, individual rounding, ++spacing, padding, font size, animation duration and easing curves tokens, and the sizes of certain ++components. The appearance scale values in `shell.json` are multiplied against these base ++token values to produce the final computed values. ++ ++Per-monitor token overrides are also available at ++`~/.config/caelestia/monitors//shell-tokens.json`. ++ + ### Home Manager Module + + For NixOS users, a home manager module is also available. +diff --git a/assets/logo.svg b/assets/logo.svg +index 712d1e36..6879c92b 100644 +--- a/assets/logo.svg ++++ b/assets/logo.svg +@@ -1,21 +1,19 @@ + +- +- + + +- +- ++ inkscape:zoom="1.28" ++ inkscape:cx="43.359375" ++ inkscape:cy="183.98438" ++ inkscape:window-width="1824" ++ inkscape:window-height="1034" ++ inkscape:window-x="0" ++ inkscape:window-y="0" ++ inkscape:window-maximized="1" ++ inkscape:current-layer="Layer_2" /> + ++ id="defs1"> ++ ++ + +- +- +- +- +- +- +- +- +- +- ++ id="dark-4" ++ transform="matrix(0.08939103,0,0,0.08939103,-3.899948e-4,18.839439)"> ++ ++ ++ ++ ++ + + +diff --git a/assets/shaders/fade.frag b/assets/shaders/fade.frag +new file mode 100644 +index 00000000..a6cdf70d +--- /dev/null ++++ b/assets/shaders/fade.frag +@@ -0,0 +1,26 @@ ++#version 440 ++ ++layout(location = 0) in vec2 qt_TexCoord0; ++layout(location = 0) out vec4 fragColor; ++ ++layout(std140, binding = 0) uniform buf { ++ mat4 qt_Matrix; ++ float qt_Opacity; ++ float fadeMargin; ++}; ++ ++layout(binding = 1) uniform sampler2D source; ++ ++void main() { ++ vec4 tex = texture(source, qt_TexCoord0); ++ float factor = 1.0; ++ float margin = 0.1; ++ ++ if (qt_TexCoord0.y < margin) { ++ factor = qt_TexCoord0.y / margin; ++ } else if (qt_TexCoord0.y > (1.0 - margin)) { ++ factor = (1.0 - qt_TexCoord0.y) / margin; ++ } ++ ++ fragColor = tex * factor * qt_Opacity; ++} +diff --git a/assets/shaders/fade.frag.qsb b/assets/shaders/fade.frag.qsb +new file mode 100644 +index 00000000..888e4c10 +Binary files /dev/null and b/assets/shaders/fade.frag.qsb differ +diff --git a/components/AnchorAnim.qml b/components/AnchorAnim.qml +new file mode 100644 +index 00000000..dd62dc36 +--- /dev/null ++++ b/components/AnchorAnim.qml +@@ -0,0 +1,51 @@ ++import QtQuick ++import Caelestia.Config ++ ++AnchorAnimation { ++ enum Type { ++ StandardSmall = 0, ++ Standard, ++ StandardLarge, ++ StandardExtraLarge, ++ EmphasizedSmall, ++ Emphasized, ++ EmphasizedLarge, ++ EmphasizedExtraLarge, ++ FastSpatial, ++ DefaultSpatial, ++ SlowSpatial ++ } ++ ++ property int type: AnchorAnim.DefaultSpatial ++ ++ duration: { ++ if (type < AnchorAnim.StandardSmall || type > AnchorAnim.SlowSpatial) ++ return Tokens.anim.durations.expressiveDefaultSpatial; ++ ++ if (type == AnchorAnim.FastSpatial) ++ return Tokens.anim.durations.expressiveFastSpatial; ++ if (type == AnchorAnim.DefaultSpatial) ++ return Tokens.anim.durations.expressiveDefaultSpatial; ++ if (type == AnchorAnim.SlowSpatial) ++ return Tokens.anim.durations.expressiveSlowSpatial; ++ ++ const types = ["small", "normal", "large", "extraLarge"]; ++ const idx = type % 4; // 0-7 are the 4 standard types ++ return Tokens.anim.durations[types[idx]]; ++ } ++ easing: { ++ if (type == AnchorAnim.FastSpatial) ++ return Tokens.anim.expressiveFastSpatial; ++ if (type == AnchorAnim.DefaultSpatial) ++ return Tokens.anim.expressiveDefaultSpatial; ++ if (type == AnchorAnim.SlowSpatial) ++ return Tokens.anim.expressiveSlowSpatial; ++ ++ if (type >= AnchorAnim.StandardSmall && type <= AnchorAnim.StandardExtraLarge) ++ return Tokens.anim.standard; ++ if (type >= AnchorAnim.EmphasizedSmall && type <= AnchorAnim.EmphasizedExtraLarge) ++ return Tokens.anim.emphasized; ++ ++ return Tokens.anim.expressiveDefaultSpatial; ++ } ++} +diff --git a/components/Anim.qml b/components/Anim.qml +index 6883a798..8bc4468a 100644 +--- a/components/Anim.qml ++++ b/components/Anim.qml +@@ -1,8 +1,48 @@ +-import qs.config + import QtQuick ++import Caelestia.Config + + NumberAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard ++ enum Type { ++ StandardSmall = 0, ++ Standard, ++ StandardLarge, ++ StandardExtraLarge, ++ EmphasizedSmall, ++ Emphasized, ++ EmphasizedLarge, ++ EmphasizedExtraLarge, ++ FastSpatial, ++ DefaultSpatial, ++ SlowSpatial ++ } ++ ++ property int type: Anim.Standard ++ ++ duration: { ++ if (type < Anim.StandardSmall || type > Anim.SlowSpatial) ++ return Tokens.anim.durations.normal; ++ ++ if (type == Anim.FastSpatial) ++ return Tokens.anim.durations.expressiveFastSpatial; ++ if (type == Anim.DefaultSpatial) ++ return Tokens.anim.durations.expressiveDefaultSpatial; ++ if (type == Anim.SlowSpatial) ++ return Tokens.anim.durations.expressiveSlowSpatial; ++ ++ const types = ["small", "normal", "large", "extraLarge"]; ++ const idx = type % 4; // 0-7 are the 4 standard types ++ return Tokens.anim.durations[types[idx]]; ++ } ++ easing: { ++ if (type == Anim.FastSpatial) ++ return Tokens.anim.expressiveFastSpatial; ++ if (type == Anim.DefaultSpatial) ++ return Tokens.anim.expressiveDefaultSpatial; ++ if (type == Anim.SlowSpatial) ++ return Tokens.anim.expressiveSlowSpatial; ++ ++ if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge) ++ return Tokens.anim.emphasized; ++ return Tokens.anim.standard; ++ } + } +diff --git a/components/CAnim.qml b/components/CAnim.qml +index 49484b78..b86fcce1 100644 +--- a/components/CAnim.qml ++++ b/components/CAnim.qml +@@ -1,8 +1,7 @@ +-import qs.config + import QtQuick ++import Caelestia.Config + + ColorAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard ++ duration: Tokens.anim.durations.normal ++ easing: Tokens.anim.standard + } +diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml +index 12b42764..691fd3c5 100644 +--- a/components/ConnectionHeader.qml ++++ b/components/ConnectionHeader.qml +@@ -1,8 +1,7 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components + + ColumnLayout { + id: root +@@ -10,14 +9,14 @@ ColumnLayout { + required property string icon + required property string title + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + Layout.alignment: Qt.AlignHCenter + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.icon +- font.pointSize: Appearance.font.size.extraLarge * 3 ++ font.pointSize: Tokens.font.size.extraLarge * 3 + font.bold: true + } + +@@ -25,7 +24,7 @@ ColumnLayout { + Layout.alignment: Qt.AlignHCenter + animate: true + text: root.title +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.bold: true + } + } +diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml +index 927ef287..d94c3a6e 100644 +--- a/components/ConnectionInfoSection.qml ++++ b/components/ConnectionInfoSection.qml +@@ -1,16 +1,15 @@ +-import qs.components +-import qs.components.effects +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root + + required property var deviceDetails + +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { + text: qsTr("IP Address") +@@ -19,40 +18,40 @@ ColumnLayout { + StyledText { + text: root.deviceDetails?.ipAddress || qsTr("Not available") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Subnet Mask") + } + + StyledText { + text: root.deviceDetails?.subnet || qsTr("Not available") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Gateway") + } + + StyledText { + text: root.deviceDetails?.gateway || qsTr("Not available") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("DNS Servers") + } + + StyledText { + text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + wrapMode: Text.Wrap + Layout.maximumWidth: parent.width + } +diff --git a/components/DashboardState.qml b/components/DashboardState.qml +new file mode 100644 +index 00000000..b4355cd7 +--- /dev/null ++++ b/components/DashboardState.qml +@@ -0,0 +1,6 @@ ++import Quickshell ++ ++PersistentProperties { ++ property int currentTab ++ property date currentDate: new Date() ++} +diff --git a/components/DrawerVisibilities.qml b/components/DrawerVisibilities.qml +new file mode 100644 +index 00000000..3286e319 +--- /dev/null ++++ b/components/DrawerVisibilities.qml +@@ -0,0 +1,11 @@ ++import Quickshell ++ ++PersistentProperties { ++ property bool bar ++ property bool osd ++ property bool session ++ property bool launcher ++ property bool dashboard ++ property bool utilities ++ property bool sidebar ++} +diff --git a/components/Logo.qml b/components/Logo.qml +new file mode 100644 +index 00000000..7cd41e17 +--- /dev/null ++++ b/components/Logo.qml +@@ -0,0 +1,70 @@ ++import QtQuick ++import QtQuick.Shapes ++import qs.services ++ ++Item { ++ id: root ++ ++ readonly property real designWidth: 128 ++ readonly property real designHeight: 90.38 ++ ++ property color topColour: Colours.palette.m3primary ++ property color bottomColour: Colours.palette.m3onSurface ++ ++ implicitWidth: designWidth ++ implicitHeight: designHeight ++ ++ Shape { ++ anchors.centerIn: parent ++ width: root.designWidth ++ height: root.designHeight ++ scale: Math.min(root.width / width, root.height / height) ++ transformOrigin: Item.Center ++ preferredRendererType: Shape.CurveRenderer ++ ++ ShapePath { ++ fillColor: root.topColour ++ strokeColor: "transparent" ++ ++ PathSvg { ++ path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" ++ } ++ } ++ ++ ShapePath { ++ fillColor: root.bottomColour ++ strokeColor: "transparent" ++ ++ PathSvg { ++ path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" ++ } ++ } ++ ++ ShapePath { ++ fillColor: root.topColour ++ strokeColor: "transparent" ++ ++ PathSvg { ++ path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" ++ } ++ } ++ ++ ShapePath { ++ fillColor: root.topColour ++ strokeColor: "transparent" ++ ++ PathSvg { ++ path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" ++ } ++ } ++ ++ ShapePath { ++ fillColor: root.topColour ++ strokeColor: "transparent" ++ ++ PathSvg { ++ path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" ++ } ++ } ++ } ++} +diff --git a/components/MaterialIcon.qml b/components/MaterialIcon.qml +index a1d19d3c..739c50ba 100644 +--- a/components/MaterialIcon.qml ++++ b/components/MaterialIcon.qml +@@ -1,12 +1,12 @@ ++import Caelestia.Config + import qs.services +-import qs.config + + StyledText { + property real fill + property int grade: Colours.light ? 0 : -25 + +- font.family: Appearance.font.family.material +- font.pointSize: Appearance.font.size.larger ++ font.family: Tokens.font.family.material ++ font.pointSize: Tokens.font.size.larger + font.variableAxes: ({ + FILL: fill.toFixed(1), + GRAD: grade, +diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml +index 640d5f74..4d302509 100644 +--- a/components/PropertyRow.qml ++++ b/components/PropertyRow.qml +@@ -1,8 +1,8 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root +@@ -11,16 +11,16 @@ ColumnLayout { + required property string value + property bool showTopMargin: false + +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { +- Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 ++ Layout.topMargin: root.showTopMargin ? Tokens.spacing.normal : 0 + text: root.label + } + + StyledText { + text: root.value + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } +diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml +index 2b653a5d..7aa3f13f 100644 +--- a/components/SectionContainer.qml ++++ b/components/SectionContainer.qml +@@ -1,21 +1,20 @@ +-import qs.components +-import qs.components.effects +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root + + default property alias content: contentColumn.data +- property real contentSpacing: Appearance.spacing.larger ++ property real contentSpacing: Tokens.spacing.larger + property bool alignTop: false + + Layout.fillWidth: true +- implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: contentColumn.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh + + ColumnLayout { +@@ -25,7 +24,7 @@ StyledRect { + anchors.right: parent.right + anchors.top: root.alignTop ? parent.top : undefined + anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + + spacing: root.contentSpacing + } +diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml +index 502e9189..09364143 100644 +--- a/components/SectionHeader.qml ++++ b/components/SectionHeader.qml +@@ -1,8 +1,8 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root +@@ -13,9 +13,9 @@ ColumnLayout { + spacing: 0 + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: root.title +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +diff --git a/components/StateLayer.qml b/components/StateLayer.qml +index a20e2661..2ce8c500 100644 +--- a/components/StateLayer.qml ++++ b/components/StateLayer.qml +@@ -1,95 +1,186 @@ +-import qs.services +-import qs.config + import QtQuick ++import QtQuick.Shapes ++import Caelestia.Config ++import qs.services + + MouseArea { + id: root + + property bool disabled + property bool showHoverBackground: true +- property color color: Colours.palette.m3onSurface +- property real radius: parent?.radius ?? 0 +- property alias rect: hoverLayer ++ readonly property alias rect: base ++ ++ property bool shapeMorph ++ property real stateOpacity: pressed ? 0.1 : containsMouse ? 0.08 : 0 ++ ++ property real pressX: width / 2 ++ property real pressY: height / 2 ++ property real circleRadius ++ ++ property alias color: base.color ++ property alias radius: base.radius ++ property alias topLeftRadius: base.topLeftRadius ++ property alias topRightRadius: base.topRightRadius ++ property alias bottomLeftRadius: base.bottomLeftRadius ++ property alias bottomRightRadius: base.bottomRightRadius ++ ++ readonly property real endRadius: { ++ const d1 = distSq(0, 0); ++ const d2 = distSq(width, 0); ++ const d3 = distSq(0, height); ++ const d4 = distSq(width, height); ++ return Math.sqrt(Math.max(d1, d2, d3, d4)) * (shapeMorph ? 1.16 : 1); ++ } ++ property real endRadiusAtPress + +- function onClicked(): void { ++ function distSq(x: real, y: real): real { ++ return (pressX - x) ** 2 + (pressY - y) ** 2; + } + +- anchors.fill: parent ++ function clamp(r: real): real { ++ return Math.max(0, Math.min(r, width / 2, height / 2)); ++ } + ++ function press(x: real, y: real): void { ++ pressX = x; ++ pressY = y; ++ fadeAnim.complete(); ++ circleRadius = 0; ++ circle.opacity = 0.1; ++ rippleAnim.restart(); ++ endRadiusAtPress = endRadius; ++ } ++ ++ anchors.fill: parent + enabled: !disabled + cursorShape: disabled ? undefined : Qt.PointingHandCursor + hoverEnabled: true + +- onPressed: event => { +- if (disabled) +- return; ++ onPressed: e => press(e.x, e.y) + +- rippleAnim.x = event.x; +- rippleAnim.y = event.y; +- +- const dist = (ox, oy) => ox * ox + oy * oy; +- rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); +- +- rippleAnim.restart(); ++ onPressedChanged: { ++ if (!pressed && !rippleAnim.running && circle.opacity > 0) ++ fadeAnim.start(); + } + +- onClicked: event => !disabled && onClicked(event) ++ onCircleRadiusChanged: { ++ if (!pressed && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) ++ fadeAnim.start(); ++ } + +- SequentialAnimation { ++ Anim { + id: rippleAnim + +- property real x +- property real y +- property real radius ++ alwaysRunToEnd: true ++ target: root ++ property: "circleRadius" ++ to: root.endRadius ++ easing: Tokens.anim.expressiveSlowEffects ++ duration: Tokens.anim.durations.expressiveSlowEffects * 2 ++ } + +- PropertyAction { +- target: ripple +- property: "x" +- value: rippleAnim.x +- } +- PropertyAction { +- target: ripple +- property: "y" +- value: rippleAnim.y +- } +- PropertyAction { +- target: ripple +- property: "opacity" +- value: 0.08 +- } +- Anim { +- target: ripple +- properties: "implicitWidth,implicitHeight" +- from: 0 +- to: rippleAnim.radius * 2 +- easing.bezierCurve: Appearance.anim.curves.standardDecel +- } +- Anim { +- target: ripple +- property: "opacity" +- to: 0 +- } ++ Anim { ++ id: fadeAnim ++ ++ target: circle ++ property: "opacity" ++ to: 0 ++ easing: Tokens.anim.expressiveSlowEffects ++ duration: Tokens.anim.durations.expressiveSlowEffects + } + +- StyledClippingRect { +- id: hoverLayer ++ StyledRect { ++ id: base + + anchors.fill: parent ++ opacity: root.stateOpacity ++ color: Colours.palette.m3onSurface ++ // Pick up radius from parent if it has one (parent can be anything with a radius property) ++ radius: root.parent?.radius ?? 0 // qmllint disable missing-property ++ } + +- color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) +- radius: root.radius ++ Shape { ++ id: circle + +- StyledRect { +- id: ripple ++ anchors.fill: parent ++ opacity: 0 ++ preferredRendererType: Shape.CurveRenderer ++ ++ ShapePath { ++ strokeWidth: 0 ++ strokeColor: "transparent" ++ fillColor: base.color ++ fillGradient: RadialGradient { ++ centerX: root.pressX ++ centerY: root.pressY ++ centerRadius: root.circleRadius ++ focalX: centerX ++ focalY: centerY ++ ++ GradientStop { ++ position: 0 ++ color: Qt.alpha(base.color, 1) ++ } ++ GradientStop { ++ position: 0.99 ++ color: Qt.alpha(base.color, 1) ++ } ++ GradientStop { ++ position: 1 ++ color: Qt.alpha(base.color, 0) ++ } ++ } + +- radius: Appearance.rounding.full +- color: root.color +- opacity: 0 ++ startX: root.clamp(base.topLeftRadius) ++ startY: 0 + +- transform: Translate { +- x: -ripple.width / 2 +- y: -ripple.height / 2 ++ PathLine { ++ x: root.width - root.clamp(base.topLeftRadius) ++ y: 0 ++ } ++ PathArc { ++ relativeX: root.clamp(base.topLeftRadius) ++ relativeY: root.clamp(base.topLeftRadius) ++ radiusX: root.clamp(base.topLeftRadius) ++ radiusY: root.clamp(base.topLeftRadius) ++ } ++ PathLine { ++ x: root.width ++ y: root.height - root.clamp(base.bottomRightRadius) + } ++ PathArc { ++ relativeX: -root.clamp(base.bottomRightRadius) ++ relativeY: root.clamp(base.bottomRightRadius) ++ radiusX: root.clamp(base.bottomRightRadius) ++ radiusY: root.clamp(base.bottomRightRadius) ++ } ++ PathLine { ++ x: root.clamp(base.bottomLeftRadius) ++ y: root.height ++ } ++ PathArc { ++ relativeX: -root.clamp(base.bottomLeftRadius) ++ relativeY: -root.clamp(base.bottomLeftRadius) ++ radiusX: root.clamp(base.bottomLeftRadius) ++ radiusY: root.clamp(base.bottomLeftRadius) ++ } ++ PathLine { ++ x: 0 ++ y: root.clamp(base.topLeftRadius) ++ } ++ PathArc { ++ x: root.clamp(base.topLeftRadius) ++ y: 0 ++ radiusX: root.clamp(base.topLeftRadius) ++ radiusY: root.clamp(base.topLeftRadius) ++ } ++ } ++ } ++ ++ Behavior on stateOpacity { ++ Anim { ++ easing: Tokens.anim.expressiveDefaultEffects ++ duration: Tokens.anim.durations.expressiveDefaultEffects + } + } + } +diff --git a/components/StyledClippingRect.qml b/components/StyledClippingRect.qml +index 8f2630c1..6484e0e6 100644 +--- a/components/StyledClippingRect.qml ++++ b/components/StyledClippingRect.qml +@@ -1,5 +1,5 @@ +-import Quickshell.Widgets + import QtQuick ++import Quickshell.Widgets + + ClippingRectangle { + id: root +diff --git a/components/StyledText.qml b/components/StyledText.qml +index ed961d26..d3624c58 100644 +--- a/components/StyledText.qml ++++ b/components/StyledText.qml +@@ -1,8 +1,8 @@ + pragma ComponentBehavior: Bound + +-import qs.services +-import qs.config + import QtQuick ++import Caelestia.Config ++import qs.services + + Text { + id: root +@@ -11,13 +11,13 @@ Text { + property string animateProp: "scale" + property real animateFrom: 0 + property real animateTo: 1 +- property int animateDuration: Appearance.anim.durations.normal ++ property int animateDuration: Tokens.anim.durations.normal + + renderType: Text.NativeRendering + textFormat: Text.PlainText + color: Colours.palette.m3onSurface +- font.family: Appearance.font.family.sans +- font.pointSize: Appearance.font.size.smaller ++ font.family: Tokens.font.family.sans ++ font.pointSize: Tokens.font.size.smaller + + Behavior on color { + CAnim {} +@@ -29,12 +29,12 @@ Text { + SequentialAnimation { + Anim { + to: root.animateFrom +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ easing: Tokens.anim.standardAccel + } + PropertyAction {} + Anim { + to: root.animateTo +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + } + } +@@ -43,6 +43,5 @@ Text { + target: root + property: root.animateProp + duration: root.animateDuration / 2 +- easing.type: Easing.BezierSpline + } + } +diff --git a/components/containers/StyledFlickable.qml b/components/containers/StyledFlickable.qml +index bc6ae0f6..b792e583 100644 +--- a/components/containers/StyledFlickable.qml ++++ b/components/containers/StyledFlickable.qml +@@ -1,5 +1,5 @@ +-import ".." + import QtQuick ++import qs.components + + Flickable { + id: root +diff --git a/components/containers/StyledListView.qml b/components/containers/StyledListView.qml +index 626d2063..8b5a4401 100644 +--- a/components/containers/StyledListView.qml ++++ b/components/containers/StyledListView.qml +@@ -1,5 +1,5 @@ +-import ".." + import QtQuick ++import qs.components + + ListView { + id: root +diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml +index 8c6e39fc..72bbed6f 100644 +--- a/components/containers/StyledWindow.qml ++++ b/components/containers/StyledWindow.qml +@@ -1,9 +1,15 @@ + import Quickshell + import Quickshell.Wayland ++import Caelestia.Config + ++// qmllint disable uncreatable-type + PanelWindow { ++ // qmllint enable uncreatable-type + required property string name + + WlrLayershell.namespace: `caelestia-${name}` + color: "transparent" ++ ++ contentItem.Config.screen: screen.name ++ contentItem.Tokens.screen: screen.name + } +diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml +index 957899e5..aabda13a 100644 +--- a/components/controls/CircularIndicator.qml ++++ b/components/controls/CircularIndicator.qml +@@ -1,9 +1,9 @@ +-import ".." +-import qs.services +-import qs.config +-import Caelestia.Internal + import QtQuick + import QtQuick.Templates ++import Caelestia.Config ++import Caelestia.Internal ++import qs.components ++import qs.services + + BusyIndicator { + id: root +@@ -19,8 +19,8 @@ BusyIndicator { + Completing + } + +- property real implicitSize: Appearance.font.size.normal * 3 +- property real strokeWidth: Appearance.padding.small * 0.8 ++ property real implicitSize: Tokens.font.size.normal * 3 ++ property real strokeWidth: Tokens.padding.small * 0.8 + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + +@@ -64,7 +64,7 @@ BusyIndicator { + transitions: Transition { + Anim { + properties: "opacity,internalStrokeWidth" +- duration: manager.completeEndDuration * Appearance.anim.durations.scale ++ duration: manager.completeEndDuration * Tokens.anim.durations.scale + } + } + +@@ -90,7 +90,7 @@ BusyIndicator { + property: "progress" + from: 0 + to: 1 +- duration: manager.duration * Appearance.anim.durations.scale ++ duration: manager.duration * Tokens.anim.durations.scale + } + + NumberAnimation { +@@ -99,7 +99,7 @@ BusyIndicator { + property: "completeEndProgress" + from: 0 + to: 1 +- duration: manager.completeEndDuration * Appearance.anim.durations.scale ++ duration: manager.completeEndDuration * Tokens.anim.durations.scale + onFinished: { + if (root.animState === CircularIndicator.Completing) + root.animState = CircularIndicator.Stopped; +diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml +index a15cd900..2372c86c 100644 +--- a/components/controls/CircularProgress.qml ++++ b/components/controls/CircularProgress.qml +@@ -1,17 +1,17 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Shapes ++import Caelestia.Config ++import qs.components ++import qs.services + + Shape { + id: root + + property real value + property int startAngle: -90 +- property int strokeWidth: Appearance.padding.smaller ++ property int strokeWidth: Tokens.padding.smaller + property int padding: 0 +- property int spacing: Appearance.spacing.small ++ property int spacing: Tokens.spacing.small + property color fgColour: Colours.palette.m3primary + property color bgColour: Colours.palette.m3secondaryContainer + +@@ -27,7 +27,7 @@ Shape { + fillColor: "transparent" + strokeColor: root.bgColour + strokeWidth: root.strokeWidth +- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap ++ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + startAngle: root.startAngle + 360 * root.vValue + root.gapAngle +@@ -40,7 +40,7 @@ Shape { + + Behavior on strokeColor { + CAnim { +- duration: Appearance.anim.durations.large ++ duration: Tokens.anim.durations.large + } + } + } +@@ -49,7 +49,7 @@ Shape { + fillColor: "transparent" + strokeColor: root.fgColour + strokeWidth: root.strokeWidth +- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap ++ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + startAngle: root.startAngle +@@ -62,7 +62,7 @@ Shape { + + Behavior on strokeColor { + CAnim { +- duration: Appearance.anim.durations.large ++ duration: Tokens.anim.durations.large + } + } + } +diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml +index e3d8eefd..80ea8fe8 100644 +--- a/components/controls/CollapsibleSection.qml ++++ b/components/controls/CollapsibleSection.qml +@@ -1,10 +1,8 @@ +-import ".." +-import qs.components +-import qs.components.effects +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root +@@ -15,28 +13,32 @@ ColumnLayout { + property bool showBackground: false + property bool nested: false + ++ default property alias content: contentColumn.data ++ + signal toggleRequested + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + Layout.fillWidth: true + + Item { + id: sectionHeaderItem ++ + Layout.fillWidth: true +- Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) ++ Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.normal * 2, 48) + + RowLayout { + id: titleRow ++ + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: Appearance.padding.normal +- anchors.rightMargin: Appearance.padding.normal +- spacing: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.padding.normal ++ anchors.rightMargin: Tokens.padding.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: root.title +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -48,11 +50,11 @@ ColumnLayout { + text: "expand_more" + rotation: root.expanded ? 180 : 0 + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal ++ + Behavior on rotation { + Anim { +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.standard ++ type: Anim.StandardSmall + } + } + } +@@ -61,70 +63,66 @@ ColumnLayout { + StateLayer { + anchors.fill: parent + color: Colours.palette.m3onSurface +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + showHoverBackground: false +- function onClicked(): void { ++ onClicked: { + root.toggleRequested(); + root.expanded = !root.expanded; + } + } + } + +- default property alias content: contentColumn.data +- + Item { + id: contentWrapper ++ + Layout.fillWidth: true +- Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 ++ Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.small * 2) : 0 + clip: true + + Behavior on Layout.preferredHeight { +- Anim { +- easing.bezierCurve: Appearance.anim.curves.standard +- } ++ Anim {} + } + + StyledRect { + id: backgroundRect ++ + anchors.fill: parent +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) + opacity: root.showBackground && root.expanded ? 1.0 : 0.0 + visible: root.showBackground + + Behavior on opacity { +- Anim { +- easing.bezierCurve: Appearance.anim.curves.standard +- } ++ Anim {} + } + } + + ColumnLayout { + id: contentColumn ++ + anchors.left: parent.left + anchors.right: parent.right +- y: Appearance.spacing.small +- anchors.leftMargin: Appearance.padding.normal +- anchors.rightMargin: Appearance.padding.normal +- anchors.bottomMargin: Appearance.spacing.small +- spacing: Appearance.spacing.small ++ y: Tokens.spacing.small ++ anchors.leftMargin: Tokens.padding.normal ++ anchors.rightMargin: Tokens.padding.normal ++ anchors.bottomMargin: Tokens.spacing.small ++ spacing: Tokens.spacing.small + opacity: root.expanded ? 1.0 : 0.0 + + Behavior on opacity { +- Anim { +- easing.bezierCurve: Appearance.anim.curves.standard +- } ++ Anim {} + } + + StyledText { + id: descriptionText ++ + Layout.fillWidth: true +- Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 +- Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 ++ Layout.topMargin: root.description !== "" ? Tokens.spacing.smaller : 0 ++ Layout.bottomMargin: root.description !== "" ? Tokens.spacing.small : 0 + visible: root.description !== "" + text: root.description + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + wrapMode: Text.Wrap + } + } +diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml +index 438dc080..d7885c30 100644 +--- a/components/controls/CustomSpinBox.qml ++++ b/components/controls/CustomSpinBox.qml +@@ -1,10 +1,10 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + RowLayout { + id: root +@@ -15,13 +15,13 @@ RowLayout { + property real step: 1 + property alias repeatRate: timer.interval + +- signal valueModified(value: real) +- +- spacing: Appearance.spacing.small +- + property bool isEditing: false + property string displayText: root.value.toString() + ++ signal valueModified(value: real) ++ ++ spacing: Tokens.spacing.small ++ + onValueChanged: { + if (!root.isEditing) { + root.displayText = root.value.toString(); +@@ -73,23 +73,23 @@ RowLayout { + root.isEditing = false; + } + +- padding: Appearance.padding.small +- leftPadding: Appearance.padding.normal +- rightPadding: Appearance.padding.normal ++ padding: Tokens.padding.small ++ leftPadding: Tokens.padding.normal ++ rightPadding: Tokens.padding.normal + + background: StyledRect { + implicitWidth: 100 +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainerHigh + } + } + + StyledRect { +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight +- implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 + + StateLayer { + id: upState +@@ -99,7 +99,7 @@ RowLayout { + onPressAndHold: timer.start() + onReleased: timer.stop() + +- function onClicked(): void { ++ onClicked: { + let newValue = Math.min(root.max, root.value + root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; +@@ -120,21 +120,16 @@ RowLayout { + } + + StyledRect { +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.palette.m3primary + + implicitWidth: implicitHeight +- implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: downIcon.implicitHeight + Tokens.padding.small * 2 + + StateLayer { + id: downState + +- color: Colours.palette.m3onPrimary +- +- onPressAndHold: timer.start() +- onReleased: timer.stop() +- +- function onClicked(): void { ++ onClicked: { + let newValue = Math.max(root.min, root.value - root.step); + // Round to avoid floating point precision errors + const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; +@@ -143,6 +138,11 @@ RowLayout { + root.displayText = newValue.toString(); + root.valueModified(newValue); + } ++ ++ color: Colours.palette.m3onPrimary ++ ++ onPressAndHold: timer.start() ++ onReleased: timer.stop() + } + + MaterialIcon { +@@ -162,9 +162,9 @@ RowLayout { + triggeredOnStart: true + onTriggered: { + if (upState.pressed) +- upState.onClicked(); ++ upState.clicked(); + else if (downState.pressed) +- downState.onClicked(); ++ downState.clicked(); + } + } + } +diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml +index 80dd44c5..1d8c85a9 100644 +--- a/components/controls/FilledSlider.qml ++++ b/components/controls/FilledSlider.qml +@@ -1,9 +1,9 @@ +-import ".." + import "../effects" +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Templates ++import Caelestia.Config ++import qs.components ++import qs.services + + Slider { + id: root +@@ -16,7 +16,7 @@ Slider { + + background: StyledRect { + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + StyledRect { + anchors.left: parent.left +@@ -51,7 +51,7 @@ Slider { + anchors.fill: parent + + color: Colours.palette.m3inverseSurface +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + MouseArea { + id: handleInteraction +@@ -70,8 +70,8 @@ Slider { + function update(): void { + animate = !moving; + binding.when = moving; +- font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; +- font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; ++ font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger; ++ font.family = moving ? Tokens.font.family.sans : Tokens.font.family.material; + } + + text: root.icon +@@ -96,8 +96,8 @@ Slider { + target: icon + property: "scale" + to: 0 +- duration: Appearance.anim.durations.normal / 2 +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ duration: Tokens.anim.durations.normal / 2 ++ easing: Tokens.anim.standardAccel + } + ScriptAction { + script: icon.update() +@@ -106,8 +106,8 @@ Slider { + target: icon + property: "scale" + to: 1 +- duration: Appearance.anim.durations.normal / 2 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ duration: Tokens.anim.durations.normal / 2 ++ easing: Tokens.anim.standardDecel + } + } + } +@@ -140,7 +140,7 @@ Slider { + + Behavior on value { + Anim { +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + } + } +diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml +index ffb1d066..58af806a 100644 +--- a/components/controls/IconButton.qml ++++ b/components/controls/IconButton.qml +@@ -1,7 +1,7 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root +@@ -15,7 +15,7 @@ StyledRect { + property alias icon: label.text + property bool checked + property bool toggle +- property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller ++ property real padding: type === IconButton.Text ? Tokens.padding.small / 2 : Tokens.padding.smaller + property alias font: label.font + property int type: IconButton.Filled + property bool disabled +@@ -44,7 +44,7 @@ StyledRect { + + onCheckedChanged: internalChecked = checked + +- radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) ++ radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour + + implicitWidth: implicitHeight +@@ -55,8 +55,7 @@ StyledRect { + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour + disabled: root.disabled +- +- function onClicked(): void { ++ onClicked: { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); +diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml +index b2bb96cc..2a1dacec 100644 +--- a/components/controls/IconTextButton.qml ++++ b/components/controls/IconTextButton.qml +@@ -1,8 +1,8 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root +@@ -17,8 +17,8 @@ StyledRect { + property alias text: label.text + property bool checked + property bool toggle +- property real horizontalPadding: Appearance.padding.normal +- property real verticalPadding: Appearance.padding.smaller ++ property real horizontalPadding: Tokens.padding.normal ++ property real verticalPadding: Tokens.padding.smaller + property alias font: label.font + property int type: IconTextButton.Filled + +@@ -36,7 +36,7 @@ StyledRect { + + onCheckedChanged: internalChecked = checked + +- radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) ++ radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + + implicitWidth: row.implicitWidth + horizontalPadding * 2 +@@ -46,8 +46,7 @@ StyledRect { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour +- +- function onClicked(): void { ++ onClicked: { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); +@@ -58,7 +57,7 @@ StyledRect { + id: row + + anchors.centerIn: parent +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + MaterialIcon { + id: iconLabel +diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml +index c763b54a..482a4d40 100644 +--- a/components/controls/Menu.qml ++++ b/components/controls/Menu.qml +@@ -1,95 +1,187 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import "../effects" +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.components.effects ++import qs.services ++import qs.modules.drawers + +-Elevation { ++MouseArea { + id: root + ++ enum Side { ++ Top, ++ Bottom, ++ Left, ++ Right ++ } ++ ++ required property Item attachTo ++ property int attachSideX: Menu.Right ++ property int attachSideY: Menu.Bottom ++ property int thisSideX: Menu.Right ++ property int thisSideY: Menu.Top ++ property real marginX ++ property real marginY ++ + property list items + property MenuItem active: items[0] ?? null + property bool expanded + + signal itemSelected(item: MenuItem) + +- radius: Appearance.rounding.small / 2 +- level: 2 ++ parent: { ++ const win = QsWindow.window; ++ const contentWin = win as ContentWindow; // If inside the drawer content window, put it inside the interaction wrapper so hover works ++ return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem; ++ } ++ anchors.fill: parent + +- implicitWidth: Math.max(200, column.implicitWidth) +- implicitHeight: root.expanded ? column.implicitHeight : 0 +- opacity: root.expanded ? 1 : 0 ++ enabled: expanded ++ onClicked: expanded = false + +- StyledClippingRect { +- anchors.fill: parent +- radius: parent.radius +- color: Colours.palette.m3surfaceContainer ++ opacity: expanded ? 1 : 0 ++ layer.enabled: opacity < 1 + +- ColumnLayout { +- id: column ++ Behavior on opacity { ++ Anim { ++ duration: Tokens.anim.durations.small ++ } ++ } + +- anchors.left: parent.left +- anchors.right: parent.right +- spacing: 0 ++ TransformWatcher { ++ id: watcher + +- Repeater { +- model: root.items ++ a: root.parent ++ b: root.attachTo ++ } + +- StyledRect { +- id: item ++ Elevation { ++ id: menu + +- required property int index +- required property MenuItem modelData +- readonly property bool active: modelData === root.active ++ x: { ++ watcher.transform; // mapToItem is not reactive so this forces updates ++ const item = root.attachTo; ++ let off = root.attachSideX === Menu.Left ? 0 : item.width; ++ if (root.thisSideX === Menu.Right) ++ off -= width; ++ return item.mapToItem(root.parent, off, 0).x + root.marginX; ++ } ++ y: { ++ watcher.transform; // mapToItem is not reactive so this forces updates ++ const item = root.attachTo; ++ let off = root.attachSideY === Menu.Top ? 0 : item.height; ++ if (root.thisSideY === Menu.Bottom) ++ off -= height; ++ return item.mapToItem(root.parent, 0, off).y + root.marginY; ++ } + +- Layout.fillWidth: true +- implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 ++ radius: Tokens.rounding.normal ++ level: 2 + +- color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) ++ implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2) ++ implicitHeight: column.implicitHeight + column.anchors.margins * 2 + +- StateLayer { +- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- disabled: !root.expanded ++ transform: Scale { ++ yScale: root.expanded ? 1 : 0.1 ++ origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0 + +- function onClicked(): void { +- root.itemSelected(item.modelData); +- root.active = item.modelData; +- root.expanded = false; +- } +- } ++ Behavior on yScale { ++ Anim { ++ type: Anim.DefaultSpatial ++ } ++ } ++ } ++ ++ StyledRect { ++ anchors.fill: parent ++ radius: parent.radius ++ color: Colours.palette.m3surfaceContainerLow ++ ++ ColumnLayout { ++ id: column ++ ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.small ++ spacing: 0 + +- RowLayout { +- id: menuOptionRow ++ Repeater { ++ id: repeater + +- anchors.fill: parent +- anchors.margins: Appearance.padding.normal +- spacing: Appearance.spacing.small ++ model: root.items + +- MaterialIcon { +- Layout.alignment: Qt.AlignVCenter +- text: item.modelData.icon +- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant ++ StyledRect { ++ id: item ++ ++ required property int index ++ required property MenuItem modelData ++ readonly property bool active: modelData === root.active ++ ++ Layout.fillWidth: true ++ implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 ++ ++ radius: active ? 12 : Tokens.rounding.extraSmall // This should use a token, but tokens are currently extremely scuffed ++ topLeftRadius: index === 0 ? Tokens.rounding.small : radius ++ topRightRadius: index === 0 ? Tokens.rounding.small : radius ++ bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius ++ bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius ++ ++ color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0) ++ ++ Behavior on radius { ++ Anim {} + } + +- StyledText { +- Layout.alignment: Qt.AlignVCenter +- Layout.fillWidth: true +- text: item.modelData.text +- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface ++ StateLayer { ++ topLeftRadius: parent.topLeftRadius ++ topRightRadius: parent.topRightRadius ++ bottomLeftRadius: parent.bottomLeftRadius ++ bottomRightRadius: parent.bottomRightRadius ++ ++ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface ++ disabled: !root.expanded ++ onClicked: { ++ root.itemSelected(item.modelData); ++ root.active = item.modelData; ++ item.modelData.clicked(); ++ root.expanded = false; ++ } + } + +- Loader { +- Layout.alignment: Qt.AlignVCenter +- active: item.modelData.trailingIcon.length > 0 +- visible: active ++ RowLayout { ++ id: menuOptionRow + +- sourceComponent: MaterialIcon { +- text: item.modelData.trailingIcon +- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.small ++ ++ MaterialIcon { ++ Layout.alignment: Qt.AlignVCenter ++ text: item.modelData.icon ++ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant ++ } ++ ++ StyledText { ++ Layout.alignment: Qt.AlignVCenter ++ Layout.fillWidth: true ++ text: item.modelData.text ++ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface ++ } ++ ++ Loader { ++ asynchronous: true ++ Layout.alignment: Qt.AlignVCenter ++ active: item.modelData.trailingIcon.length > 0 ++ visible: active ++ ++ sourceComponent: MaterialIcon { ++ text: item.modelData.trailingIcon ++ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant ++ } + } + } + } +@@ -97,17 +189,4 @@ Elevation { + } + } + } +- +- Behavior on opacity { +- Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- } +- } +- +- Behavior on implicitHeight { +- Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- } + } +diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml +index fe6a1982..dc52c19c 100644 +--- a/components/controls/SpinBoxRow.qml ++++ b/components/controls/SpinBoxRow.qml +@@ -1,10 +1,8 @@ +-import ".." +-import qs.components +-import qs.components.effects +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root +@@ -17,8 +15,8 @@ StyledRect { + property var onValueModified: function (value) {} + + Layout.fillWidth: true +- implicitHeight: row.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal ++ implicitHeight: row.implicitHeight + Tokens.padding.large * 2 ++ radius: Tokens.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { +@@ -31,8 +29,8 @@ StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +@@ -45,7 +43,7 @@ StyledRect { + step: root.step + value: root.value + onValueModified: value => { +- root.onValueModified(value); ++ root.onValueModified(value); // qmllint disable use-proper-function + } + } + } +diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml +index c91474ea..4acd98cf 100644 +--- a/components/controls/SplitButton.qml ++++ b/components/controls/SplitButton.qml +@@ -1,8 +1,8 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + Row { + id: root +@@ -12,8 +12,8 @@ Row { + Tonal + } + +- property real horizontalPadding: Appearance.padding.normal +- property real verticalPadding: Appearance.padding.smaller ++ property real horizontalPadding: Tokens.padding.normal ++ property real verticalPadding: Tokens.padding.smaller + property int type: SplitButton.Filled + property bool disabled + property bool menuOnTop +@@ -33,12 +33,12 @@ Row { + property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) + property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) + +- spacing: Math.floor(Appearance.spacing.small / 2) ++ spacing: Math.floor(Tokens.spacing.small / 2) + + StyledRect { +- radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) +- topRightRadius: Appearance.rounding.small / 2 +- bottomRightRadius: Appearance.rounding.small / 2 ++ radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) ++ topRightRadius: Tokens.rounding.small / 2 ++ bottomRightRadius: Tokens.rounding.small / 2 + color: root.disabled ? root.disabledColour : root.colour + + implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 +@@ -51,10 +51,7 @@ Row { + rect.bottomRightRadius: parent.bottomRightRadius + color: root.textColour + disabled: root.disabled +- +- function onClicked(): void { +- root.active?.clicked(); +- } ++ onClicked: root.active?.clicked() + } + + RowLayout { +@@ -62,7 +59,7 @@ Row { + + anchors.centerIn: parent + anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + MaterialIcon { + id: iconLabel +@@ -86,7 +83,7 @@ Row { + + Behavior on Layout.preferredWidth { + Anim { +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + } +@@ -96,9 +93,9 @@ Row { + StyledRect { + id: expandBtn + +- property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2 ++ property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.small / 2 + +- radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) ++ radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + topLeftRadius: rad + bottomLeftRadius: rad + color: root.disabled ? root.disabledColour : root.colour +@@ -113,10 +110,7 @@ Row { + rect.bottomLeftRadius: parent.bottomLeftRadius + color: root.textColour + disabled: root.disabled +- +- function onClicked(): void { +- root.expanded = !root.expanded; +- } ++ onClicked: root.expanded = !root.expanded + } + + MaterialIcon { +@@ -141,24 +135,14 @@ Row { + Behavior on rad { + Anim {} + } ++ } + +- Menu { +- id: menu +- +- states: State { +- when: root.menuOnTop +- +- AnchorChanges { +- target: menu +- anchors.top: undefined +- anchors.bottom: expandBtn.top +- } +- } ++ Menu { ++ id: menu + +- anchors.top: parent.bottom +- anchors.right: parent.right +- anchors.topMargin: Appearance.spacing.small +- anchors.bottomMargin: Appearance.spacing.small +- } ++ attachTo: expandBtn ++ attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom ++ thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top ++ marginY: Tokens.spacing.small * (root.menuOnTop ? -1 : 1) + } + } +diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml +index db9925ff..bd1ecc62 100644 +--- a/components/controls/SplitButtonRow.qml ++++ b/components/controls/SplitButtonRow.qml +@@ -1,19 +1,16 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import qs.components +-import qs.components.effects +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root + + required property string label + property int expandedZ: 100 +- property bool enabled: true + + property alias menuItems: splitButton.menuItems + property alias active: splitButton.active +@@ -23,8 +20,8 @@ StyledRect { + signal selected(item: MenuItem) + + Layout.fillWidth: true +- implicitHeight: row.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal ++ implicitHeight: row.implicitHeight + Tokens.padding.large * 2 ++ radius: Tokens.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + clip: false +@@ -33,9 +30,10 @@ StyledRect { + + RowLayout { + id: row ++ + anchors.fill: parent +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +@@ -45,6 +43,7 @@ StyledRect { + + SplitButton { + id: splitButton ++ + enabled: root.enabled + type: SplitButton.Filled + +diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml +index 0d199c73..ff7772ca 100644 +--- a/components/controls/StyledInputField.qml ++++ b/components/controls/StyledInputField.qml +@@ -1,10 +1,9 @@ + pragma ComponentBehavior: Bound + +-import ".." ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import QtQuick + + Item { + id: root +@@ -13,23 +12,23 @@ Item { + property var validator: null + property bool readOnly: false + property int horizontalAlignment: TextInput.AlignHCenter +- property int implicitWidth: 70 +- property bool enabled: true + + // Expose activeFocus through alias to avoid FINAL property override + readonly property alias hasFocus: inputField.activeFocus + + signal textEdited(string text) ++ + signal editingFinished + +- implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: 70 ++ implicitHeight: inputField.implicitHeight + Tokens.padding.small * 2 + + StyledRect { + id: container + + anchors.fill: parent + color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + border.width: 1 + border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + opacity: root.enabled ? 1 : 0.5 +@@ -43,6 +42,7 @@ Item { + + MouseArea { + id: inputHover ++ + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.IBeamCursor +@@ -52,20 +52,14 @@ Item { + + StyledTextField { + id: inputField ++ + anchors.centerIn: parent +- width: parent.width - Appearance.padding.normal ++ width: parent.width - Tokens.padding.normal + horizontalAlignment: root.horizontalAlignment + validator: root.validator + readOnly: root.readOnly + enabled: root.enabled + +- Binding { +- target: inputField +- property: "text" +- value: root.text +- when: !inputField.activeFocus +- } +- + onTextChanged: { + root.text = text; + root.textEdited(text); +@@ -74,6 +68,13 @@ Item { + onEditingFinished: { + root.editingFinished(); + } ++ ++ Binding { ++ target: inputField ++ property: "text" ++ value: root.text ++ when: !inputField.activeFocus ++ } + } + } + } +diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml +index b72fc77f..24c7d6e8 100644 +--- a/components/controls/StyledRadioButton.qml ++++ b/components/controls/StyledRadioButton.qml +@@ -1,13 +1,13 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Templates ++import Caelestia.Config ++import qs.components ++import qs.services + + RadioButton { + id: root + +- font.pointSize: Appearance.font.size.smaller ++ font.pointSize: Tokens.font.size.smaller + + implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin + implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) +@@ -17,20 +17,17 @@ RadioButton { + + implicitWidth: 20 + implicitHeight: 20 +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: "transparent" + border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + border.width: 2 + anchors.verticalCenter: parent.verticalCenter + + StateLayer { +- anchors.margins: -Appearance.padding.smaller ++ anchors.margins: -Tokens.padding.smaller + color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary + z: -1 +- +- function onClicked(): void { +- root.click(); +- } ++ onClicked: root.click() + } + + StyledRect { +@@ -38,7 +35,7 @@ RadioButton { + implicitWidth: 8 + implicitHeight: 8 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0) + } + +@@ -52,6 +49,6 @@ RadioButton { + font.pointSize: root.font.pointSize + anchors.verticalCenter: parent.verticalCenter + anchors.left: outerCircle.right +- anchors.leftMargin: Appearance.spacing.smaller ++ anchors.leftMargin: Tokens.spacing.smaller + } + } +diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml +index de8b679c..8cfe3f8d 100644 +--- a/components/controls/StyledScrollBar.qml ++++ b/components/controls/StyledScrollBar.qml +@@ -1,8 +1,8 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Templates ++import Caelestia.Config ++import qs.components ++import qs.services + + ScrollBar { + id: root +@@ -11,6 +11,8 @@ ScrollBar { + property bool shouldBeActive + property real nonAnimPosition + property bool animating ++ property bool _updatingFromFlickable: false ++ property bool _updatingFromUser: false + + onHoveredChanged: { + if (hovered) +@@ -19,9 +21,6 @@ ScrollBar { + shouldBeActive = flickable.moving; + } + +- property bool _updatingFromFlickable: false +- property bool _updatingFromUser: false +- + // Sync nonAnimPosition with Qt's automatic position binding + onPositionChanged: { + if (_updatingFromUser) { +@@ -37,24 +36,6 @@ ScrollBar { + } + } + +- // Sync nonAnimPosition with flickable when not animating +- Connections { +- target: flickable +- function onContentYChanged() { +- if (!animating && !fullMouse.pressed) { +- _updatingFromFlickable = true; +- const contentHeight = flickable.contentHeight; +- const height = flickable.height; +- if (contentHeight > height) { +- nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); +- } else { +- nonAnimPosition = 0; +- } +- _updatingFromFlickable = false; +- } +- } +- } +- + Component.onCompleted: { + if (flickable) { + const contentHeight = flickable.contentHeight; +@@ -64,7 +45,7 @@ ScrollBar { + } + } + } +- implicitWidth: Appearance.padding.small ++ implicitWidth: Tokens.padding.small + + contentItem: StyledRect { + anchors.left: parent.left +@@ -80,7 +61,7 @@ ScrollBar { + return 0.6; + return 0; + } +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3secondary + + MouseArea { +@@ -97,15 +78,34 @@ ScrollBar { + } + } + ++ // Sync nonAnimPosition with flickable when not animating + Connections { ++ function onContentYChanged() { ++ if (!root.animating && !fullMouse.pressed) { ++ root._updatingFromFlickable = true; ++ const contentHeight = root.flickable.contentHeight; ++ const height = root.flickable.height; ++ if (contentHeight > height) { ++ root.nonAnimPosition = Math.max(0, Math.min(1, root.flickable.contentY / (contentHeight - height))); ++ } else { ++ root.nonAnimPosition = 0; ++ } ++ root._updatingFromFlickable = false; ++ } ++ } ++ + target: root.flickable ++ } + ++ Connections { + function onMovingChanged(): void { + if (root.flickable.moving) + root.shouldBeActive = true; + else + hideDelay.restart(); + } ++ ++ target: root.flickable + } + + Timer { +@@ -118,13 +118,14 @@ ScrollBar { + CustomMouseArea { + id: fullMouse + +- anchors.fill: parent +- preventStealing: true +- +- onPressed: event => { ++ function onWheel(event: WheelEvent): void { + root.animating = true; + root._updatingFromUser = true; +- const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); ++ let newPos = root.nonAnimPosition; ++ if (event.angleDelta.y > 0) ++ newPos = Math.max(0, root.nonAnimPosition - 0.1); ++ else if (event.angleDelta.y < 0) ++ newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] +@@ -140,7 +141,11 @@ ScrollBar { + } + } + +- onPositionChanged: event => { ++ anchors.fill: parent ++ preventStealing: true ++ ++ onPressed: event => { ++ root.animating = true; + root._updatingFromUser = true; + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; +@@ -158,14 +163,9 @@ ScrollBar { + } + } + +- function onWheel(event: WheelEvent): void { +- root.animating = true; ++ onPositionChanged: event => { + root._updatingFromUser = true; +- let newPos = root.nonAnimPosition; +- if (event.angleDelta.y > 0) +- newPos = Math.max(0, root.nonAnimPosition - 0.1); +- else if (event.angleDelta.y < 0) +- newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); ++ const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + root.nonAnimPosition = newPos; + // Update flickable position + // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] +diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml +index 0ef229df..f169691b 100644 +--- a/components/controls/StyledSlider.qml ++++ b/components/controls/StyledSlider.qml +@@ -1,8 +1,8 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Templates ++import Caelestia.Config ++import qs.components ++import qs.services + + Slider { + id: root +@@ -18,7 +18,7 @@ Slider { + implicitWidth: root.handle.x - root.implicitHeight / 6 + + color: Colours.palette.m3primary +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + topRightRadius: root.implicitHeight / 15 + bottomRightRadius: root.implicitHeight / 15 + } +@@ -33,7 +33,7 @@ Slider { + implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 + + color: Colours.palette.m3surfaceContainerHighest +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + topLeftRadius: root.implicitHeight / 15 + bottomLeftRadius: root.implicitHeight / 15 + } +@@ -46,7 +46,7 @@ Slider { + implicitHeight: root.implicitHeight + + color: Colours.palette.m3primary +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + MouseArea { + anchors.fill: parent +diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml +index ce93cd50..2e31adbf 100644 +--- a/components/controls/StyledSwitch.qml ++++ b/components/controls/StyledSwitch.qml +@@ -1,9 +1,9 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick +-import QtQuick.Templates + import QtQuick.Shapes ++import QtQuick.Templates ++import Caelestia.Config ++import qs.components ++import qs.services + + Switch { + id: root +@@ -14,21 +14,21 @@ Switch { + implicitHeight: implicitIndicatorHeight + + indicator: StyledRect { +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) + + implicitWidth: implicitHeight * 1.7 +- implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 ++ implicitHeight: Tokens.font.size.normal + Tokens.padding.smaller * 2 + + StyledRect { + readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) + +- x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 ++ x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.small / 2 : Tokens.padding.small / 2 + implicitWidth: nonAnimWidth +- implicitHeight: parent.implicitHeight - Appearance.padding.small ++ implicitHeight: parent.implicitHeight - Tokens.padding.small + anchors.verticalCenter: parent.verticalCenter + + StyledRect { +@@ -83,15 +83,15 @@ Switch { + + anchors.centerIn: parent + width: height +- height: parent.implicitHeight - Appearance.padding.small * 2 ++ height: parent.implicitHeight - Tokens.padding.small * 2 + preferredRendererType: Shape.CurveRenderer + asynchronous: true + + ShapePath { +- strokeWidth: Appearance.font.size.larger * 0.15 ++ strokeWidth: root.Tokens.font.size.larger * 0.15 + strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest + fillColor: "transparent" +- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap ++ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + startX: icon.start1.x + startY: icon.start1.y +@@ -145,8 +145,7 @@ Switch { + } + + component PropAnim: PropertyAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard ++ duration: Tokens.anim.durations.normal ++ easing: Tokens.anim.standard + } + } +diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml +index 60bcff25..4ab2f824 100644 +--- a/components/controls/StyledTextField.qml ++++ b/components/controls/StyledTextField.qml +@@ -1,18 +1,18 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Controls ++import Caelestia.Config ++import qs.components ++import qs.services + + TextField { + id: root + + color: Colours.palette.m3onSurface + placeholderTextColor: Colours.palette.m3outline +- font.family: Appearance.font.family.sans +- font.pointSize: Appearance.font.size.smaller ++ font.family: Tokens.font.family.sans ++ font.pointSize: Tokens.font.size.smaller + renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering + cursorVisible: !readOnly + +@@ -25,11 +25,9 @@ TextField { + + implicitWidth: 2 + color: Colours.palette.m3primary +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + Connections { +- target: root +- + function onCursorPositionChanged(): void { + if (root.activeFocus && root.cursorVisible) { + cursor.opacity = 1; +@@ -37,6 +35,8 @@ TextField { + enableBlink.restart(); + } + } ++ ++ target: root + } + + Timer { +@@ -61,7 +61,7 @@ TextField { + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml +index 6dda3f0c..b909739f 100644 +--- a/components/controls/SwitchRow.qml ++++ b/components/controls/SwitchRow.qml +@@ -1,22 +1,20 @@ +-import ".." +-import qs.components +-import qs.components.effects +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root + + required property string label + required property bool checked +- property bool enabled: true +- property var onToggled: function (checked) {} ++ ++ signal toggled(checked: bool) + + Layout.fillWidth: true +- implicitHeight: row.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal ++ implicitHeight: row.implicitHeight + Tokens.padding.large * 2 ++ radius: Tokens.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + Behavior on implicitHeight { +@@ -29,8 +27,8 @@ StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +@@ -40,9 +38,7 @@ StyledRect { + StyledSwitch { + checked: root.checked + enabled: root.enabled +- onToggled: { +- root.onToggled(checked); +- } ++ onToggled: root.toggled(checked) + } + } + } +diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml +index ecf7eb13..94d76ff8 100644 +--- a/components/controls/TextButton.qml ++++ b/components/controls/TextButton.qml +@@ -1,7 +1,7 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root +@@ -15,8 +15,8 @@ StyledRect { + property alias text: label.text + property bool checked + property bool toggle +- property real horizontalPadding: Appearance.padding.normal +- property real verticalPadding: Appearance.padding.smaller ++ property real horizontalPadding: Tokens.padding.normal ++ property real verticalPadding: Tokens.padding.smaller + property alias font: label.font + property int type: TextButton.Filled + +@@ -47,7 +47,7 @@ StyledRect { + + onCheckedChanged: internalChecked = checked + +- radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) ++ radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour + + implicitWidth: label.implicitWidth + horizontalPadding * 2 +@@ -57,8 +57,7 @@ StyledRect { + id: stateLayer + + color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour +- +- function onClicked(): void { ++ onClicked: { + if (root.toggle) + root.internalChecked = !root.internalChecked; + root.clicked(); +diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml +index 98c7564f..232426c1 100644 +--- a/components/controls/ToggleButton.qml ++++ b/components/controls/ToggleButton.qml +@@ -1,11 +1,11 @@ +-import ".." ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls +-import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + StyledRect { + id: root +@@ -14,50 +14,45 @@ StyledRect { + property string icon + property string label + property string accent: "Secondary" +- property real iconSize: Appearance.font.size.large +- property real horizontalPadding: Appearance.padding.large +- property real verticalPadding: Appearance.padding.normal ++ property real iconSize: Tokens.font.size.large ++ property real horizontalPadding: Tokens.padding.large ++ property real verticalPadding: Tokens.padding.normal + property string tooltip: "" +- + property bool hovered: false ++ + signal clicked + +- Component.onCompleted: { +- hovered = toggleStateLayer.containsMouse; +- } ++ Component.onCompleted: hovered = toggleStateLayer.containsMouse ++ ++ Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.normal * 2 : toggled ? Tokens.padding.small * 2 : 0) ++ implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 ++ implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 ++ radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.small : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) ++ color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + + Connections { +- target: toggleStateLayer + function onContainsMouseChanged() { + const newHovered = toggleStateLayer.containsMouse; +- if (hovered !== newHovered) { +- hovered = newHovered; ++ if (root.hovered !== newHovered) { ++ root.hovered = newHovered; + } + } +- } +- +- Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) +- implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 +- implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 + +- radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) +- color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] ++ target: toggleStateLayer ++ } + + StateLayer { + id: toggleStateLayer + + color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] +- +- function onClicked(): void { +- root.clicked(); +- } ++ onClicked: root.clicked() + } + + RowLayout { + id: toggleBtnInner + + anchors.centerIn: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + id: toggleBtnIcon +@@ -74,6 +69,7 @@ StyledRect { + } + + Loader { ++ asynchronous: true + active: !!root.label + visible: active + +@@ -86,21 +82,21 @@ StyledRect { + + Behavior on radius { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + Behavior on Layout.preferredWidth { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + // Tooltip - positioned absolutely, doesn't affect layout + Loader { + id: tooltipLoader ++ ++ asynchronous: true + active: root.tooltip !== "" + z: 10000 + width: 0 +diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml +index 269d3d6a..beb6a15c 100644 +--- a/components/controls/ToggleRow.qml ++++ b/components/controls/ToggleRow.qml +@@ -1,9 +1,8 @@ +-import qs.components +-import qs.components.controls +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.components.controls + + RowLayout { + id: root +@@ -13,7 +12,7 @@ RowLayout { + property alias toggle: toggle + + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml +index b129a37b..6c62f43d 100644 +--- a/components/controls/Tooltip.qml ++++ b/components/controls/Tooltip.qml +@@ -1,10 +1,9 @@ +-import ".." +-import qs.components.effects +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Controls +-import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.components.effects ++import qs.services + + Popup { + id: root +@@ -24,55 +23,6 @@ Popup { + onTriggered: root.tooltipVisible = false + } + +- // Popup properties - doesn't affect layout +- parent: { +- let p = target; +- // Walk up to find the root Item (usually has anchors.fill: parent) +- while (p && p.parent) { +- const parentItem = p.parent; +- // Check if this looks like a root pane Item +- if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { +- return parentItem; +- } +- p = parentItem; +- } +- // Fallback +- return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; +- } +- +- visible: tooltipVisible +- modal: false +- closePolicy: Popup.NoAutoClose +- padding: 0 +- margins: 0 +- background: Item {} +- +- // Update position when target moves or tooltip becomes visible +- onTooltipVisibleChanged: { +- if (tooltipVisible) { +- Qt.callLater(updatePosition); +- } +- } +- Connections { +- target: root.target +- function onXChanged() { +- if (root.tooltipVisible) +- root.updatePosition(); +- } +- function onYChanged() { +- if (root.tooltipVisible) +- root.updatePosition(); +- } +- function onWidthChanged() { +- if (root.tooltipVisible) +- root.updatePosition(); +- } +- function onHeightChanged() { +- if (root.tooltipVisible) +- root.updatePosition(); +- } +- } +- + function updatePosition() { + if (!target || !parent) + return; +@@ -94,10 +44,10 @@ Popup { + let newX = targetCenterX - tooltipWidth / 2; + + // Position tooltip above target +- let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; ++ let newY = targetPos.y - tooltipHeight - Tokens.spacing.small; + + // Keep within bounds +- const padding = Appearance.padding.normal; ++ const padding = Tokens.padding.normal; + if (newX < padding) { + newX = padding; + } else if (newX + tooltipWidth > (parent.width - padding)) { +@@ -110,13 +60,47 @@ Popup { + }); + } + ++ // Popup properties - doesn't affect layout ++ parent: { ++ let p = target; ++ // Walk up to find the root Item (usually has anchors.fill: parent) ++ while (p && p.parent) { ++ const parentItem = p.parent; ++ // Check if this looks like a root pane Item ++ if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { ++ return parentItem; ++ } ++ p = parentItem; ++ } ++ // Fallback ++ return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; ++ } ++ ++ visible: tooltipVisible ++ modal: false ++ closePolicy: Popup.NoAutoClose ++ padding: 0 ++ margins: 0 ++ background: Item {} ++ ++ // Update position when target moves or tooltip becomes visible ++ onTooltipVisibleChanged: { ++ if (tooltipVisible) { ++ Qt.callLater(updatePosition); ++ } ++ } ++ Component.onCompleted: { ++ if (tooltipVisible) { ++ updatePosition(); ++ } ++ } ++ + enter: Transition { + Anim { + property: "opacity" + from: 0 + to: 1 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + +@@ -125,37 +109,18 @@ Popup { + property: "opacity" + from: 1 + to: 0 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial +- } +- } +- +- // Monitor hover state +- Connections { +- target: root.target +- function onHoveredChanged() { +- if (target.hovered) { +- showTimer.start(); +- if (timeout > 0) { +- hideTimer.stop(); +- hideTimer.start(); +- } +- } else { +- showTimer.stop(); +- hideTimer.stop(); +- tooltipVisible = false; +- } ++ type: Anim.FastSpatial + } + } + + contentItem: StyledRect { + id: tooltipRect + +- implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 ++ implicitWidth: tooltipText.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: tooltipText.implicitHeight + Tokens.padding.smaller * 2 + + color: Colours.palette.m3surfaceContainerHighest +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + antialiasing: true + + // Add elevation for depth +@@ -173,13 +138,47 @@ Popup { + + text: root.text + color: Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + +- Component.onCompleted: { +- if (tooltipVisible) { +- updatePosition(); ++ Connections { ++ function onXChanged() { ++ if (root.tooltipVisible) ++ root.updatePosition(); ++ } ++ function onYChanged() { ++ if (root.tooltipVisible) ++ root.updatePosition(); ++ } ++ function onWidthChanged() { ++ if (root.tooltipVisible) ++ root.updatePosition(); ++ } ++ function onHeightChanged() { ++ if (root.tooltipVisible) ++ root.updatePosition(); ++ } ++ ++ target: root.target ++ } ++ ++ // Monitor hover state ++ Connections { ++ function onHoveredChanged() { ++ if (target.hovered) { ++ showTimer.start(); ++ if (timeout > 0) { ++ hideTimer.stop(); ++ hideTimer.start(); ++ } ++ } else { ++ showTimer.stop(); ++ hideTimer.stop(); ++ tooltipVisible = false; ++ } + } ++ ++ target: root.target + } + } +diff --git a/components/effects/ColouredIcon.qml b/components/effects/ColouredIcon.qml +index 5ef4d4cc..570246bf 100644 +--- a/components/effects/ColouredIcon.qml ++++ b/components/effects/ColouredIcon.qml +@@ -1,8 +1,8 @@ + pragma ComponentBehavior: Bound + +-import Caelestia +-import Quickshell.Widgets + import QtQuick ++import Quickshell.Widgets ++import Caelestia + + IconImage { + id: root +diff --git a/components/effects/Colouriser.qml b/components/effects/Colouriser.qml +index 2948155d..b8abebb1 100644 +--- a/components/effects/Colouriser.qml ++++ b/components/effects/Colouriser.qml +@@ -1,6 +1,6 @@ +-import ".." + import QtQuick + import QtQuick.Effects ++import qs.components + + MultiEffect { + property color sourceColor: "black" +diff --git a/components/effects/Elevation.qml b/components/effects/Elevation.qml +index fb29f16e..9e0962da 100644 +--- a/components/effects/Elevation.qml ++++ b/components/effects/Elevation.qml +@@ -1,7 +1,7 @@ +-import ".." +-import qs.services + import QtQuick + import QtQuick.Effects ++import qs.components ++import qs.services + + RectangularShadow { + property int level +diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml +index d4a751f8..1b502e29 100644 +--- a/components/effects/InnerBorder.qml ++++ b/components/effects/InnerBorder.qml +@@ -1,10 +1,10 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Effects ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + property alias innerRadius: maskInner.radius +@@ -37,8 +37,8 @@ StyledRect { + id: maskInner + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal +- radius: Appearance.rounding.small ++ anchors.margins: Tokens.padding.normal ++ radius: Tokens.rounding.normal + } + } + } +diff --git a/components/effects/OpacityMask.qml b/components/effects/OpacityMask.qml +index 22e42496..8e625034 100644 +--- a/components/effects/OpacityMask.qml ++++ b/components/effects/OpacityMask.qml +@@ -1,9 +1,9 @@ +-import Quickshell + import QtQuick ++import Quickshell + + ShaderEffect { + required property Item source + required property Item maskSource + +- fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) ++ fragmentShader: Quickshell.shellPath("assets/shaders/opacitymask.frag.qsb") + } +diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml +index bb87133c..83cb4a92 100644 +--- a/components/filedialog/CurrentItem.qml ++++ b/components/filedialog/CurrentItem.qml +@@ -1,16 +1,16 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Shapes ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root + + required property var currentItem + +- implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin +- implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 ++ implicitWidth: content.implicitWidth + Tokens.padding.larger + content.anchors.rightMargin ++ implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.normal + content.anchors.bottomMargin : 0 + + Shape { + preferredRendererType: Shape.CurveRenderer +@@ -18,7 +18,7 @@ Item { + ShapePath { + id: path + +- readonly property real rounding: Appearance.rounding.small ++ readonly property real rounding: Tokens.rounding.small + readonly property bool flatten: root.implicitHeight < rounding * 2 + readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding + +@@ -76,16 +76,16 @@ Item { + + anchors.right: parent.right + anchors.bottom: parent.bottom +- anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small +- anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small ++ anchors.rightMargin: Tokens.padding.larger - Tokens.padding.small ++ anchors.bottomMargin: Tokens.padding.normal - Tokens.padding.small + + Connections { +- target: root +- + function onCurrentItemChanged(): void { + if (root.currentItem) + content.text = qsTr(`"%1" selected`).arg(root.currentItem.modelData.name); + } ++ ++ target: root + } + } + } +diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml +index bde9ac27..b46b4f72 100644 +--- a/components/filedialog/DialogButtons.qml ++++ b/components/filedialog/DialogButtons.qml +@@ -1,7 +1,7 @@ +-import ".." +-import qs.services +-import qs.config + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root +@@ -9,7 +9,7 @@ StyledRect { + required property var dialog + required property FolderContents folder + +- implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + +@@ -17,9 +17,9 @@ StyledRect { + id: inner + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Filter:") +@@ -28,14 +28,14 @@ StyledRect { + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true +- Layout.rightMargin: Appearance.spacing.normal ++ Layout.rightMargin: Tokens.spacing.normal + + color: Colours.tPalette.m3surfaceContainerHigh +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + + StyledText { + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` + } +@@ -43,24 +43,21 @@ StyledRect { + + StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + +- implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 ++ implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { + disabled: !root.dialog.selectionValid +- +- function onClicked(): void { +- root.dialog.accepted(root.folder.currentItem.modelData.path); +- } ++ onClicked: root.dialog.accepted(root.folder.currentItem.modelData.path) + } + + StyledText { + id: selectText + + anchors.centerIn: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + text: qsTr("Select") + color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline +@@ -69,13 +66,13 @@ StyledRect { + + StyledRect { + color: Colours.tPalette.m3surfaceContainerHigh +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + +- implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 ++ implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { ++ onClicked: { + root.dialog.rejected(); + } + } +@@ -84,7 +81,7 @@ StyledRect { + id: cancelText + + anchors.centerIn: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + text: qsTr("Cancel") + } +diff --git a/components/filedialog/FileDialog.qml b/components/filedialog/FileDialog.qml +index f3187a55..90dd2bc2 100644 +--- a/components/filedialog/FileDialog.qml ++++ b/components/filedialog/FileDialog.qml +@@ -1,10 +1,10 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import Quickshell + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import qs.components ++import qs.services + + LazyLoader { + id: loader +diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml +index e16c7a15..d8869b83 100644 +--- a/components/filedialog/FolderContents.qml ++++ b/components/filedialog/FolderContents.qml +@@ -1,22 +1,23 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import "../controls" +-import "../images" +-import qs.services +-import qs.config +-import qs.utils +-import Caelestia.Models +-import Quickshell + import QtQuick +-import QtQuick.Layouts + import QtQuick.Effects ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config ++import Caelestia.Models ++import qs.components ++import qs.components.controls ++import qs.components.filedialog ++import qs.components.images ++import qs.services ++import qs.utils + + Item { + id: root + + required property var dialog +- property alias currentItem: view.currentItem ++ readonly property FileEntry currentItem: view.currentItem as FileEntry + + StyledRect { + anchors.fill: parent +@@ -41,12 +42,13 @@ Item { + + Rectangle { + anchors.fill: parent +- anchors.margins: Appearance.padding.small +- radius: Appearance.rounding.small ++ anchors.margins: Tokens.padding.small ++ radius: Tokens.rounding.small + } + } + + Loader { ++ asynchronous: true + anchors.centerIn: parent + + opacity: view.count === 0 ? 1 : 0 +@@ -57,14 +59,14 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: "scan_delete" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.extraLarge * 2 ++ font.pointSize: Tokens.font.size.extraLarge * 2 + font.weight: 500 + } + + StyledText { + text: qsTr("This folder is empty") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + } +@@ -78,10 +80,10 @@ Item { + id: view + + anchors.fill: parent +- anchors.margins: Appearance.padding.small + Appearance.padding.normal ++ anchors.margins: Tokens.padding.small + Tokens.padding.normal + +- cellWidth: Sizes.itemWidth + Appearance.spacing.small +- cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 ++ cellWidth: Sizes.itemWidth + Tokens.spacing.small ++ cellHeight: Sizes.itemWidth + Tokens.spacing.small * 2 + Tokens.padding.normal * 2 + 1 + + clip: true + focus: true +@@ -90,11 +92,11 @@ Item { + + Keys.onReturnPressed: { + if (root.dialog.selectionValid) +- root.dialog.accepted(currentItem.modelData.path); ++ root.dialog.accepted((currentItem as FileEntry).modelData.path); + } + Keys.onEnterPressed: { + if (root.dialog.selectionValid) +- root.dialog.accepted(currentItem.modelData.path); ++ root.dialog.accepted((currentItem as FileEntry).modelData.path); + } + + StyledScrollBar.vertical: StyledScrollBar { +@@ -104,92 +106,21 @@ Item { + model: FileSystemModel { + path: { + if (root.dialog.cwd[0] === "Home") +- return `${Paths.home}/${root.dialog.cwd.slice(1).join("/")}`; ++ return Paths.home + `/${root.dialog.cwd.slice(1).join("/")}`; + else + return root.dialog.cwd.join("/"); + } + onPathChanged: view.currentIndex = -1 + } + +- delegate: StyledRect { +- id: item +- +- required property int index +- required property FileSystemEntry modelData +- +- readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 +- +- implicitWidth: Sizes.itemWidth +- implicitHeight: nonAnimHeight +- +- radius: Appearance.rounding.normal +- color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) +- z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 +- clip: true +- +- StateLayer { +- onDoubleClicked: { +- if (item.modelData.isDir) +- root.dialog.cwd.push(item.modelData.name); +- else if (root.dialog.selectionValid) +- root.dialog.accepted(item.modelData.path); +- } +- +- function onClicked(): void { +- view.currentIndex = item.index; +- } +- } +- +- CachingIconImage { +- id: icon +- +- anchors.horizontalCenter: parent.horizontalCenter +- anchors.top: parent.top +- anchors.topMargin: Appearance.padding.normal +- +- implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 +- +- Component.onCompleted: { +- const file = item.modelData; +- if (file.isImage) +- source = Qt.resolvedUrl(file.path); +- else if (!file.isDir) +- source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); +- else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) +- source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); +- else +- source = Quickshell.iconPath("inode-directory"); +- } +- } +- +- StyledText { +- id: name +- +- anchors.left: parent.left +- anchors.right: parent.right +- anchors.top: icon.bottom +- anchors.topMargin: Appearance.spacing.small +- anchors.margins: Appearance.padding.normal +- +- horizontalAlignment: Text.AlignHCenter +- elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight +- wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap +- +- Component.onCompleted: text = item.modelData.name +- } +- +- Behavior on implicitHeight { +- Anim {} +- } +- } ++ delegate: FileEntry {} + + add: Transition { + Anim { + properties: "opacity,scale" + from: 0 + to: 1 +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + +@@ -208,12 +139,11 @@ Item { + Anim { + properties: "opacity,scale" + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + Anim { + properties: "x,y" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +@@ -221,8 +151,77 @@ Item { + CurrentItem { + anchors.right: parent.right + anchors.bottom: parent.bottom +- anchors.margins: Appearance.padding.small ++ anchors.margins: Tokens.padding.small + + currentItem: view.currentItem + } ++ ++ component FileEntry: StyledRect { ++ id: item ++ ++ required property int index ++ required property FileSystemEntry modelData ++ ++ readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.normal * 2 ++ ++ implicitWidth: Sizes.itemWidth ++ implicitHeight: nonAnimHeight ++ ++ radius: Tokens.rounding.normal ++ color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) ++ z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 ++ clip: true ++ ++ StateLayer { ++ onClicked: view.currentIndex = item.index ++ onDoubleClicked: { ++ if (item.modelData.isDir) ++ root.dialog.cwd.push(item.modelData.name); ++ else if (root.dialog.selectionValid) ++ root.dialog.accepted(item.modelData.path); ++ } ++ } ++ ++ CachingIconImage { ++ id: icon ++ ++ anchors.horizontalCenter: parent.horizontalCenter ++ anchors.top: parent.top ++ anchors.topMargin: Tokens.padding.normal ++ ++ implicitSize: Sizes.itemWidth - Tokens.padding.normal * 2 ++ ++ Component.onCompleted: { ++ const file = item.modelData; ++ if (file.isImage) ++ source = Qt.resolvedUrl(file.path); ++ else if (!file.isDir) ++ source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); ++ else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) ++ source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); ++ else ++ source = Quickshell.iconPath("inode-directory"); ++ } ++ } ++ ++ StyledText { ++ id: name ++ ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.top: icon.bottom ++ anchors.topMargin: Tokens.spacing.small ++ anchors.margins: Tokens.padding.normal ++ ++ horizontalAlignment: Text.AlignHCenter ++ elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight ++ wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap ++ ++ Component.onCompleted: text = item.modelData.name ++ } ++ ++ Behavior on implicitHeight { ++ Anim {} ++ } ++ } + } +diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml +index c9a3feb5..c53d8f76 100644 +--- a/components/filedialog/HeaderBar.qml ++++ b/components/filedialog/HeaderBar.qml +@@ -1,18 +1,18 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root + + required property var dialog + +- implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 ++ implicitWidth: inner.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + +@@ -20,20 +20,17 @@ StyledRect { + id: inner + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal +- spacing: Appearance.spacing.small ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.small + + Item { + implicitWidth: implicitHeight +- implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 + + StateLayer { +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + disabled: root.dialog.cwd.length === 1 +- +- function onClicked(): void { +- root.dialog.cwd.pop(); +- } ++ onClicked: root.dialog.cwd.pop() + } + + MaterialIcon { +@@ -49,7 +46,7 @@ StyledRect { + StyledRect { + Layout.fillWidth: true + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainerHigh + + implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 +@@ -58,10 +55,10 @@ StyledRect { + id: pathComponents + + anchors.fill: parent +- anchors.margins: Appearance.padding.small / 2 ++ anchors.margins: Tokens.padding.small / 2 + anchors.leftMargin: 0 + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Repeater { + model: root.dialog.cwd +@@ -75,7 +72,8 @@ StyledRect { + spacing: 0 + + Loader { +- Layout.rightMargin: Appearance.spacing.small ++ asynchronous: true ++ Layout.rightMargin: Tokens.spacing.small + active: folder.index > 0 + sourceComponent: StyledText { + text: "/" +@@ -85,27 +83,30 @@ StyledRect { + } + + Item { +- implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.small : 0) + folderName.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: folderName.implicitHeight + Tokens.padding.small * 2 + + Loader { ++ asynchronous: true + anchors.fill: parent + active: folder.index < root.dialog.cwd.length - 1 + sourceComponent: StateLayer { +- radius: Appearance.rounding.small +- +- function onClicked(): void { ++ onClicked: { + root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); + } ++ ++ radius: Tokens.rounding.small + } + } + + Loader { + id: homeIcon + ++ asynchronous: true ++ + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: Appearance.padding.normal ++ anchors.leftMargin: Tokens.padding.normal + + active: folder.index === 0 && folder.modelData === "Home" + sourceComponent: MaterialIcon { +@@ -120,7 +121,7 @@ StyledRect { + + anchors.left: homeIcon.right + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 ++ anchors.leftMargin: homeIcon.active ? Tokens.padding.small : 0 + + text: folder.modelData + color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface +diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml +index b55d7b37..e9c0918b 100644 +--- a/components/filedialog/Sidebar.qml ++++ b/components/filedialog/Sidebar.qml +@@ -1,10 +1,11 @@ + pragma ComponentBehavior: Bound + +-import ".." +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.components.filedialog ++import qs.services + + StyledRect { + id: root +@@ -12,7 +13,7 @@ StyledRect { + required property var dialog + + implicitWidth: Sizes.sidebarWidth +- implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 + + color: Colours.tPalette.m3surfaceContainer + +@@ -22,16 +23,16 @@ StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top +- anchors.margins: Appearance.padding.normal +- spacing: Appearance.spacing.small / 2 ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.small / 2 + + StyledText { + Layout.alignment: Qt.AlignHCenter +- Layout.topMargin: Appearance.padding.small / 2 +- Layout.bottomMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.padding.small / 2 ++ Layout.bottomMargin: Tokens.spacing.normal + text: qsTr("Files") + color: Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.bold: true + } + +@@ -45,15 +46,14 @@ StyledRect { + readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] + + Layout.fillWidth: true +- implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: placeInner.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) + + StateLayer { + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- +- function onClicked(): void { ++ onClicked: { + if (place.modelData === "Home") + root.dialog.cwd = ["Home"]; + else +@@ -65,11 +65,11 @@ StyledRect { + id: placeInner + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal +- anchors.leftMargin: Appearance.padding.large +- anchors.rightMargin: Appearance.padding.large ++ anchors.margins: Tokens.padding.normal ++ anchors.leftMargin: Tokens.padding.large ++ anchors.rightMargin: Tokens.padding.large + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: { +@@ -91,7 +91,7 @@ StyledRect { + return "folder"; + } + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: place.selected ? 1 : 0 + + Behavior on fill { +@@ -103,7 +103,7 @@ StyledRect { + Layout.fillWidth: true + text: place.modelData + color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + elide: Text.ElideRight + } + } +diff --git a/components/images/CachingIconImage.qml b/components/images/CachingIconImage.qml +index 1acc6a18..001e95de 100644 +--- a/components/images/CachingIconImage.qml ++++ b/components/images/CachingIconImage.qml +@@ -1,13 +1,14 @@ + pragma ComponentBehavior: Bound + +-import qs.utils +-import Quickshell.Widgets + import QtQuick ++import Quickshell.Widgets ++import qs.utils + + Item { + id: root + +- readonly property int status: loader.item?.status ?? Image.Null ++ // Easier (and more efficient) to ignore it than to check type and cast ++ readonly property int status: loader.item?.status ?? Image.Null // qmllint disable missing-property + readonly property real actualSize: Math.min(width, height) + property real implicitSize + property url source +@@ -18,6 +19,7 @@ Item { + Loader { + id: loader + ++ asynchronous: true + anchors.fill: parent + sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null + } +diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml +index e8f957a7..1e932dba 100644 +--- a/components/images/CachingImage.qml ++++ b/components/images/CachingImage.qml +@@ -1,28 +1,17 @@ +-import qs.utils +-import Caelestia.Internal +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia.Images + + Image { + id: root + +- property alias path: manager.path ++ property string path + + asynchronous: true + fillMode: Image.PreserveAspectCrop +- +- Connections { +- target: QsWindow.window +- +- function onDevicePixelRatioChanged(): void { +- manager.updateSource(); +- } +- } +- +- CachingImageManager { +- id: manager +- +- item: root +- cacheDir: Qt.resolvedUrl(Paths.imagecache) ++ source: IUtils.urlForPath(path, fillMode) ++ sourceSize: { ++ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; ++ return Qt.size(width * dpr, height * dpr); + } + } +diff --git a/components/misc/CustomShortcut.qml b/components/misc/CustomShortcut.qml +index aa35ed8f..9bbcf1f7 100644 +--- a/components/misc/CustomShortcut.qml ++++ b/components/misc/CustomShortcut.qml +@@ -1,5 +1,7 @@ + import Quickshell.Hyprland + ++// qmllint disable unresolved-type + GlobalShortcut { ++ // qmllint enable unresolved-type + appid: "caelestia" + } +diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml +index db73ea08..d426eed8 100644 +--- a/components/widgets/ExtraIndicator.qml ++++ b/components/widgets/ExtraIndicator.qml +@@ -1,20 +1,20 @@ +-import ".." + import "../effects" +-import qs.services +-import qs.config + import QtQuick ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + required property int extra + + anchors.right: parent.right +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + color: Colours.palette.m3tertiary +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + +- implicitWidth: count.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: count.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: count.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: count.implicitHeight + Tokens.padding.small * 2 + + opacity: extra > 0 ? 1 : 0 + scale: extra > 0 ? 1 : 0.5 +@@ -38,14 +38,13 @@ StyledRect { + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial ++ duration: Tokens.anim.durations.expressiveFastSpatial + } + } + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +diff --git a/config/Appearance.qml b/config/Appearance.qml +deleted file mode 100644 +index 241c21a7..00000000 +--- a/config/Appearance.qml ++++ /dev/null +@@ -1,14 +0,0 @@ +-pragma Singleton +- +-import Quickshell +- +-Singleton { +- // Literally just here to shorten accessing stuff :woe: +- // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx` +- readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding +- readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing +- readonly property AppearanceConfig.Padding padding: Config.appearance.padding +- readonly property AppearanceConfig.FontStuff font: Config.appearance.font +- readonly property AppearanceConfig.Anim anim: Config.appearance.anim +- readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency +-} +diff --git a/config/AppearanceConfig.qml b/config/AppearanceConfig.qml +deleted file mode 100644 +index b25945b1..00000000 +--- a/config/AppearanceConfig.qml ++++ /dev/null +@@ -1,92 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property Rounding rounding: Rounding {} +- property Spacing spacing: Spacing {} +- property Padding padding: Padding {} +- property FontStuff font: FontStuff {} +- property Anim anim: Anim {} +- property Transparency transparency: Transparency {} +- +- component Rounding: JsonObject { +- property real scale: 1 +- property int small: 12 * scale +- property int normal: 17 * scale +- property int large: 25 * scale +- property int full: 1000 * scale +- } +- +- component Spacing: JsonObject { +- property real scale: 1 +- property int small: 7 * scale +- property int smaller: 10 * scale +- property int normal: 12 * scale +- property int larger: 15 * scale +- property int large: 20 * scale +- } +- +- component Padding: JsonObject { +- property real scale: 1 +- property int small: 5 * scale +- property int smaller: 7 * scale +- property int normal: 10 * scale +- property int larger: 12 * scale +- property int large: 15 * scale +- } +- +- component FontFamily: JsonObject { +- property string sans: "Rubik" +- property string mono: "CaskaydiaCove NF" +- property string material: "Material Symbols Rounded" +- property string clock: "Rubik" +- } +- +- component FontSize: JsonObject { +- property real scale: 1 +- property int small: 11 * scale +- property int smaller: 12 * scale +- property int normal: 13 * scale +- property int larger: 15 * scale +- property int large: 18 * scale +- property int extraLarge: 28 * scale +- } +- +- component FontStuff: JsonObject { +- property FontFamily family: FontFamily {} +- property FontSize size: FontSize {} +- } +- +- component AnimCurves: JsonObject { +- property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] +- property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] +- property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] +- property list standard: [0.2, 0, 0, 1, 1, 1] +- property list standardAccel: [0.3, 0, 1, 1, 1, 1] +- property list standardDecel: [0, 0, 0, 1, 1, 1] +- property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] +- property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] +- property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] +- } +- +- component AnimDurations: JsonObject { +- property real scale: 1 +- property int small: 200 * scale +- property int normal: 400 * scale +- property int large: 600 * scale +- property int extraLarge: 1000 * scale +- property int expressiveFastSpatial: 350 * scale +- property int expressiveDefaultSpatial: 500 * scale +- property int expressiveEffects: 200 * scale +- } +- +- component Anim: JsonObject { +- property AnimCurves curves: AnimCurves {} +- property AnimDurations durations: AnimDurations {} +- } +- +- component Transparency: JsonObject { +- property bool enabled: false +- property real base: 0.85 +- property real layers: 0.4 +- } +-} +diff --git a/config/BackgroundConfig.qml b/config/BackgroundConfig.qml +deleted file mode 100644 +index b8a8ad92..00000000 +--- a/config/BackgroundConfig.qml ++++ /dev/null +@@ -1,36 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool enabled: true +- property DesktopClock desktopClock: DesktopClock {} +- property Visualiser visualiser: Visualiser {} +- +- component DesktopClock: JsonObject { +- property bool enabled: false +- property real scale: 1.0 +- property string position: "bottom-right" +- property bool invertColors: false +- property DesktopClockBackground background: DesktopClockBackground {} +- property DesktopClockShadow shadow: DesktopClockShadow {} +- } +- +- component DesktopClockBackground: JsonObject { +- property bool enabled: false +- property real opacity: 0.7 +- property bool blur: true +- } +- +- component DesktopClockShadow: JsonObject { +- property bool enabled: true +- property real opacity: 0.7 +- property real blur: 0.4 +- } +- +- component Visualiser: JsonObject { +- property bool enabled: false +- property bool autoHide: true +- property bool blur: false +- property real rounding: 1 +- property real spacing: 1 +- } +-} +diff --git a/config/BarConfig.qml b/config/BarConfig.qml +deleted file mode 100644 +index cf33fd21..00000000 +--- a/config/BarConfig.qml ++++ /dev/null +@@ -1,117 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool persistent: true +- property bool showOnHover: true +- property int dragThreshold: 20 +- property ScrollActions scrollActions: ScrollActions {} +- property Popouts popouts: Popouts {} +- property Workspaces workspaces: Workspaces {} +- property ActiveWindow activeWindow: ActiveWindow {} +- property Tray tray: Tray {} +- property Status status: Status {} +- property Clock clock: Clock {} +- property Sizes sizes: Sizes {} +- property list excludedScreens: [] +- +- property list entries: [ +- { +- id: "logo", +- enabled: true +- }, +- { +- id: "workspaces", +- enabled: true +- }, +- { +- id: "spacer", +- enabled: true +- }, +- { +- id: "activeWindow", +- enabled: true +- }, +- { +- id: "spacer", +- enabled: true +- }, +- { +- id: "tray", +- enabled: true +- }, +- { +- id: "clock", +- enabled: true +- }, +- { +- id: "statusIcons", +- enabled: true +- }, +- { +- id: "power", +- enabled: true +- } +- ] +- +- component ScrollActions: JsonObject { +- property bool workspaces: true +- property bool volume: true +- property bool brightness: true +- } +- +- component Popouts: JsonObject { +- property bool activeWindow: true +- property bool tray: true +- property bool statusIcons: true +- } +- +- component Workspaces: JsonObject { +- property int shown: 5 +- property bool activeIndicator: true +- property bool occupiedBg: false +- property bool showWindows: true +- property bool showWindowsOnSpecialWorkspaces: showWindows +- property bool activeTrail: false +- property bool perMonitorWorkspaces: true +- property string label: " " // if empty, will show workspace name's first letter +- property string occupiedLabel: "󰮯" +- property string activeLabel: "󰮯" +- property string capitalisation: "preserve" // upper, lower, or preserve - relevant only if label is empty +- property list specialWorkspaceIcons: [] +- } +- +- component ActiveWindow: JsonObject { +- property bool inverted: false +- } +- +- component Tray: JsonObject { +- property bool background: false +- property bool recolour: false +- property bool compact: false +- property list iconSubs: [] +- } +- +- component Status: JsonObject { +- property bool showAudio: false +- property bool showMicrophone: false +- property bool showKbLayout: false +- property bool showNetwork: true +- property bool showWifi: true +- property bool showBluetooth: true +- property bool showBattery: true +- property bool showLockStatus: true +- } +- +- component Clock: JsonObject { +- property bool showIcon: true +- } +- +- component Sizes: JsonObject { +- property int innerWidth: 40 +- property int windowPreviewSize: 400 +- property int trayMenuWidth: 300 +- property int batteryWidth: 250 +- property int networkWidth: 320 +- property int kbLayoutWidth: 320 +- } +-} +diff --git a/config/BorderConfig.qml b/config/BorderConfig.qml +deleted file mode 100644 +index b15811fd..00000000 +--- a/config/BorderConfig.qml ++++ /dev/null +@@ -1,6 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property int thickness: Appearance.padding.normal +- property int rounding: Appearance.rounding.large +-} +diff --git a/config/Config.qml b/config/Config.qml +deleted file mode 100644 +index 191c8556..00000000 +--- a/config/Config.qml ++++ /dev/null +@@ -1,507 +0,0 @@ +-pragma Singleton +- +-import qs.utils +-import Caelestia +-import Quickshell +-import Quickshell.Io +-import QtQuick +- +-Singleton { +- id: root +- +- property alias appearance: adapter.appearance +- property alias general: adapter.general +- property alias background: adapter.background +- property alias bar: adapter.bar +- property alias border: adapter.border +- property alias dashboard: adapter.dashboard +- property alias controlCenter: adapter.controlCenter +- property alias launcher: adapter.launcher +- property alias notifs: adapter.notifs +- property alias osd: adapter.osd +- property alias session: adapter.session +- property alias winfo: adapter.winfo +- property alias lock: adapter.lock +- property alias utilities: adapter.utilities +- property alias sidebar: adapter.sidebar +- property alias services: adapter.services +- property alias paths: adapter.paths +- +- // Public save function - call this to persist config changes +- function save(): void { +- saveTimer.restart(); +- recentlySaved = true; +- recentSaveCooldown.restart(); +- } +- +- property bool recentlySaved: false +- +- ElapsedTimer { +- id: timer +- } +- +- Timer { +- id: saveTimer +- +- interval: 500 +- onTriggered: { +- timer.restart(); +- try { +- // Parse current config to preserve structure and comments if possible +- let config = {}; +- try { +- config = JSON.parse(fileView.text()); +- } catch (e) { +- // If parsing fails, start with empty object +- config = {}; +- } +- +- // Update config with current values +- config = serializeConfig(); +- +- // Save to file with pretty printing +- fileView.setText(JSON.stringify(config, null, 2)); +- } catch (e) { +- Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); +- } +- } +- } +- +- Timer { +- id: recentSaveCooldown +- +- interval: 2000 +- onTriggered: { +- recentlySaved = false; +- } +- } +- +- // Helper function to serialize the config object +- function serializeConfig(): var { +- return { +- appearance: serializeAppearance(), +- general: serializeGeneral(), +- background: serializeBackground(), +- bar: serializeBar(), +- border: serializeBorder(), +- dashboard: serializeDashboard(), +- controlCenter: serializeControlCenter(), +- launcher: serializeLauncher(), +- notifs: serializeNotifs(), +- osd: serializeOsd(), +- session: serializeSession(), +- winfo: serializeWinfo(), +- lock: serializeLock(), +- utilities: serializeUtilities(), +- sidebar: serializeSidebar(), +- services: serializeServices(), +- paths: serializePaths() +- }; +- } +- +- function serializeAppearance(): var { +- return { +- rounding: { +- scale: appearance.rounding.scale +- }, +- spacing: { +- scale: appearance.spacing.scale +- }, +- padding: { +- scale: appearance.padding.scale +- }, +- font: { +- family: { +- sans: appearance.font.family.sans, +- mono: appearance.font.family.mono, +- material: appearance.font.family.material, +- clock: appearance.font.family.clock +- }, +- size: { +- scale: appearance.font.size.scale +- } +- }, +- anim: { +- durations: { +- scale: appearance.anim.durations.scale +- } +- }, +- transparency: { +- enabled: appearance.transparency.enabled, +- base: appearance.transparency.base, +- layers: appearance.transparency.layers +- } +- }; +- } +- +- function serializeGeneral(): var { +- return { +- logo: general.logo, +- apps: { +- terminal: general.apps.terminal, +- audio: general.apps.audio, +- playback: general.apps.playback, +- explorer: general.apps.explorer +- }, +- idle: { +- lockBeforeSleep: general.idle.lockBeforeSleep, +- inhibitWhenAudio: general.idle.inhibitWhenAudio, +- timeouts: general.idle.timeouts +- }, +- battery: { +- warnLevels: general.battery.warnLevels, +- criticalLevel: general.battery.criticalLevel +- } +- }; +- } +- +- function serializeBackground(): var { +- return { +- enabled: background.enabled, +- desktopClock: { +- enabled: background.desktopClock.enabled, +- scale: background.desktopClock.scale, +- position: background.desktopClock.position, +- invertColors: background.desktopClock.invertColors, +- background: { +- enabled: background.desktopClock.background.enabled, +- opacity: background.desktopClock.background.opacity, +- blur: background.desktopClock.background.blur +- }, +- shadow: { +- enabled: background.desktopClock.shadow.enabled, +- opacity: background.desktopClock.shadow.opacity, +- blur: background.desktopClock.shadow.blur +- } +- }, +- visualiser: { +- enabled: background.visualiser.enabled, +- autoHide: background.visualiser.autoHide, +- blur: background.visualiser.blur, +- rounding: background.visualiser.rounding, +- spacing: background.visualiser.spacing +- } +- }; +- } +- +- function serializeBar(): var { +- return { +- persistent: bar.persistent, +- showOnHover: bar.showOnHover, +- dragThreshold: bar.dragThreshold, +- scrollActions: { +- workspaces: bar.scrollActions.workspaces, +- volume: bar.scrollActions.volume, +- brightness: bar.scrollActions.brightness +- }, +- popouts: { +- activeWindow: bar.popouts.activeWindow, +- tray: bar.popouts.tray, +- statusIcons: bar.popouts.statusIcons +- }, +- workspaces: { +- shown: bar.workspaces.shown, +- activeIndicator: bar.workspaces.activeIndicator, +- occupiedBg: bar.workspaces.occupiedBg, +- showWindows: bar.workspaces.showWindows, +- showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, +- activeTrail: bar.workspaces.activeTrail, +- perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, +- label: bar.workspaces.label, +- occupiedLabel: bar.workspaces.occupiedLabel, +- activeLabel: bar.workspaces.activeLabel, +- capitalisation: bar.workspaces.capitalisation, +- specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons +- }, +- tray: { +- background: bar.tray.background, +- recolour: bar.tray.recolour, +- compact: bar.tray.compact, +- iconSubs: bar.tray.iconSubs +- }, +- status: { +- showAudio: bar.status.showAudio, +- showMicrophone: bar.status.showMicrophone, +- showKbLayout: bar.status.showKbLayout, +- showNetwork: bar.status.showNetwork, +- showWifi: bar.status.showWifi, +- showBluetooth: bar.status.showBluetooth, +- showBattery: bar.status.showBattery, +- showLockStatus: bar.status.showLockStatus +- }, +- clock: { +- showIcon: bar.clock.showIcon +- }, +- sizes: { +- innerWidth: bar.sizes.innerWidth, +- windowPreviewSize: bar.sizes.windowPreviewSize, +- trayMenuWidth: bar.sizes.trayMenuWidth, +- batteryWidth: bar.sizes.batteryWidth, +- networkWidth: bar.sizes.networkWidth +- }, +- entries: bar.entries +- }; +- } +- +- function serializeBorder(): var { +- return { +- thickness: border.thickness, +- rounding: border.rounding +- }; +- } +- +- function serializeDashboard(): var { +- return { +- enabled: dashboard.enabled, +- showOnHover: dashboard.showOnHover, +- mediaUpdateInterval: dashboard.mediaUpdateInterval, +- dragThreshold: dashboard.dragThreshold, +- sizes: { +- tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, +- tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, +- infoWidth: dashboard.sizes.infoWidth, +- infoIconSize: dashboard.sizes.infoIconSize, +- dateTimeWidth: dashboard.sizes.dateTimeWidth, +- mediaWidth: dashboard.sizes.mediaWidth, +- mediaProgressSweep: dashboard.sizes.mediaProgressSweep, +- mediaProgressThickness: dashboard.sizes.mediaProgressThickness, +- resourceProgessThickness: dashboard.sizes.resourceProgessThickness, +- weatherWidth: dashboard.sizes.weatherWidth, +- mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize, +- mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize, +- resourceSize: dashboard.sizes.resourceSize +- } +- }; +- } +- +- function serializeControlCenter(): var { +- return { +- sizes: { +- heightMult: controlCenter.sizes.heightMult, +- ratio: controlCenter.sizes.ratio +- } +- }; +- } +- +- function serializeLauncher(): var { +- return { +- enabled: launcher.enabled, +- showOnHover: launcher.showOnHover, +- maxShown: launcher.maxShown, +- maxWallpapers: launcher.maxWallpapers, +- specialPrefix: launcher.specialPrefix, +- actionPrefix: launcher.actionPrefix, +- enableDangerousActions: launcher.enableDangerousActions, +- dragThreshold: launcher.dragThreshold, +- vimKeybinds: launcher.vimKeybinds, +- hiddenApps: launcher.hiddenApps, +- useFuzzy: { +- apps: launcher.useFuzzy.apps, +- actions: launcher.useFuzzy.actions, +- schemes: launcher.useFuzzy.schemes, +- variants: launcher.useFuzzy.variants, +- wallpapers: launcher.useFuzzy.wallpapers +- }, +- sizes: { +- itemWidth: launcher.sizes.itemWidth, +- itemHeight: launcher.sizes.itemHeight, +- wallpaperWidth: launcher.sizes.wallpaperWidth, +- wallpaperHeight: launcher.sizes.wallpaperHeight +- }, +- actions: launcher.actions +- }; +- } +- +- function serializeNotifs(): var { +- return { +- expire: notifs.expire, +- defaultExpireTimeout: notifs.defaultExpireTimeout, +- clearThreshold: notifs.clearThreshold, +- expandThreshold: notifs.expandThreshold, +- actionOnClick: notifs.actionOnClick, +- groupPreviewNum: notifs.groupPreviewNum, +- sizes: { +- width: notifs.sizes.width, +- image: notifs.sizes.image, +- badge: notifs.sizes.badge +- } +- }; +- } +- +- function serializeOsd(): var { +- return { +- enabled: osd.enabled, +- hideDelay: osd.hideDelay, +- enableBrightness: osd.enableBrightness, +- enableMicrophone: osd.enableMicrophone, +- sizes: { +- sliderWidth: osd.sizes.sliderWidth, +- sliderHeight: osd.sizes.sliderHeight +- } +- }; +- } +- +- function serializeSession(): var { +- return { +- enabled: session.enabled, +- dragThreshold: session.dragThreshold, +- vimKeybinds: session.vimKeybinds, +- commands: { +- logout: session.commands.logout, +- shutdown: session.commands.shutdown, +- hibernate: session.commands.hibernate, +- reboot: session.commands.reboot +- }, +- sizes: { +- button: session.sizes.button +- } +- }; +- } +- +- function serializeWinfo(): var { +- return { +- sizes: { +- heightMult: winfo.sizes.heightMult, +- detailsWidth: winfo.sizes.detailsWidth +- } +- }; +- } +- +- function serializeLock(): var { +- return { +- recolourLogo: lock.recolourLogo, +- enableFprint: lock.enableFprint, +- maxFprintTries: lock.maxFprintTries, +- sizes: { +- heightMult: lock.sizes.heightMult, +- ratio: lock.sizes.ratio, +- centerWidth: lock.sizes.centerWidth +- } +- }; +- } +- +- function serializeUtilities(): var { +- return { +- enabled: utilities.enabled, +- maxToasts: utilities.maxToasts, +- sizes: { +- width: utilities.sizes.width, +- toastWidth: utilities.sizes.toastWidth +- }, +- toasts: { +- configLoaded: utilities.toasts.configLoaded, +- chargingChanged: utilities.toasts.chargingChanged, +- gameModeChanged: utilities.toasts.gameModeChanged, +- dndChanged: utilities.toasts.dndChanged, +- audioOutputChanged: utilities.toasts.audioOutputChanged, +- audioInputChanged: utilities.toasts.audioInputChanged, +- capsLockChanged: utilities.toasts.capsLockChanged, +- numLockChanged: utilities.toasts.numLockChanged, +- kbLayoutChanged: utilities.toasts.kbLayoutChanged, +- vpnChanged: utilities.toasts.vpnChanged, +- nowPlaying: utilities.toasts.nowPlaying +- }, +- vpn: { +- enabled: utilities.vpn.enabled, +- provider: utilities.vpn.provider +- }, +- recording: { +- videoMode: utilities.recording.videoMode, +- recordSystem: utilities.recording.recordSystem, +- recordMicrophone: utilities.recording.recordMicrophone +- } +- }; +- } +- +- function serializeSidebar(): var { +- return { +- enabled: sidebar.enabled, +- dragThreshold: sidebar.dragThreshold, +- sizes: { +- width: sidebar.sizes.width +- } +- }; +- } +- +- function serializeServices(): var { +- return { +- weatherLocation: services.weatherLocation, +- useFahrenheit: services.useFahrenheit, +- useTwelveHourClock: services.useTwelveHourClock, +- gpuType: services.gpuType, +- visualiserBars: services.visualiserBars, +- audioIncrement: services.audioIncrement, +- brightnessIncrement: services.brightnessIncrement, +- maxVolume: services.maxVolume, +- smartScheme: services.smartScheme, +- defaultPlayer: services.defaultPlayer, +- playerAliases: services.playerAliases +- }; +- } +- +- function serializePaths(): var { +- return { +- wallpaperDir: paths.wallpaperDir, +- sessionGif: paths.sessionGif, +- mediaGif: paths.mediaGif +- }; +- } +- +- FileView { +- id: fileView +- +- path: `${Paths.config}/shell.json` +- watchChanges: true +- onFileChanged: { +- // Prevent reload loop - don't reload if we just saved +- if (!recentlySaved) { +- timer.restart(); +- reload(); +- } else { +- // Self-initiated save - reload without toast +- reload(); +- } +- } +- onLoaded: { +- try { +- JSON.parse(text()); +- const elapsed = timer.elapsedMs(); +- // Only show toast for external changes (not our own saves) and when elapsed time is meaningful +- if (adapter.utilities.toasts.configLoaded && !recentlySaved && elapsed > 0) { +- Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); +- } else if (adapter.utilities.toasts.configLoaded && recentlySaved && elapsed > 0) { +- Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); +- } +- } catch (e) { +- Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); +- } +- } +- onLoadFailed: err => { +- if (err !== FileViewError.FileNotFound) +- Toaster.toast(qsTr("Failed to read config file"), FileViewError.toString(err), "settings_alert", Toast.Warning); +- } +- onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) +- +- JsonAdapter { +- id: adapter +- +- property AppearanceConfig appearance: AppearanceConfig {} +- property GeneralConfig general: GeneralConfig {} +- property BackgroundConfig background: BackgroundConfig {} +- property BarConfig bar: BarConfig {} +- property BorderConfig border: BorderConfig {} +- property DashboardConfig dashboard: DashboardConfig {} +- property ControlCenterConfig controlCenter: ControlCenterConfig {} +- property LauncherConfig launcher: LauncherConfig {} +- property NotifsConfig notifs: NotifsConfig {} +- property OsdConfig osd: OsdConfig {} +- property SessionConfig session: SessionConfig {} +- property WInfoConfig winfo: WInfoConfig {} +- property LockConfig lock: LockConfig {} +- property UtilitiesConfig utilities: UtilitiesConfig {} +- property SidebarConfig sidebar: SidebarConfig {} +- property ServiceConfig services: ServiceConfig {} +- property UserPaths paths: UserPaths {} +- } +- } +-} +diff --git a/config/ControlCenterConfig.qml b/config/ControlCenterConfig.qml +deleted file mode 100644 +index a5889491..00000000 +--- a/config/ControlCenterConfig.qml ++++ /dev/null +@@ -1,10 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property Sizes sizes: Sizes {} +- +- component Sizes: JsonObject { +- property real heightMult: 0.7 +- property real ratio: 16 / 9 +- } +-} +diff --git a/config/DashboardConfig.qml b/config/DashboardConfig.qml +deleted file mode 100644 +index 030292b1..00000000 +--- a/config/DashboardConfig.qml ++++ /dev/null +@@ -1,25 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool enabled: true +- property bool showOnHover: true +- property int mediaUpdateInterval: 500 +- property int dragThreshold: 50 +- property Sizes sizes: Sizes {} +- +- component Sizes: JsonObject { +- readonly property int tabIndicatorHeight: 3 +- readonly property int tabIndicatorSpacing: 5 +- readonly property int infoWidth: 200 +- readonly property int infoIconSize: 25 +- readonly property int dateTimeWidth: 110 +- readonly property int mediaWidth: 200 +- readonly property int mediaProgressSweep: 180 +- readonly property int mediaProgressThickness: 8 +- readonly property int resourceProgessThickness: 10 +- readonly property int weatherWidth: 250 +- readonly property int mediaCoverArtSize: 150 +- readonly property int mediaVisualiserSize: 80 +- readonly property int resourceSize: 200 +- } +-} +diff --git a/config/GeneralConfig.qml b/config/GeneralConfig.qml +deleted file mode 100644 +index 52ef0de3..00000000 +--- a/config/GeneralConfig.qml ++++ /dev/null +@@ -1,60 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property string logo: "" +- property Apps apps: Apps {} +- property Idle idle: Idle {} +- property Battery battery: Battery {} +- +- component Apps: JsonObject { +- property list terminal: ["foot"] +- property list audio: ["pavucontrol"] +- property list playback: ["mpv"] +- property list explorer: ["thunar"] +- } +- +- component Idle: JsonObject { +- property bool lockBeforeSleep: true +- property bool inhibitWhenAudio: true +- property list timeouts: [ +- { +- timeout: 180, +- idleAction: "lock" +- }, +- { +- timeout: 300, +- idleAction: "dpms off", +- returnAction: "dpms on" +- }, +- { +- timeout: 600, +- idleAction: ["systemctl", "suspend-then-hibernate"] +- } +- ] +- } +- +- component Battery: JsonObject { +- property list warnLevels: [ +- { +- level: 20, +- title: qsTr("Low battery"), +- message: qsTr("You might want to plug in a charger"), +- icon: "battery_android_frame_2" +- }, +- { +- level: 10, +- title: qsTr("Did you see the previous message?"), +- message: qsTr("You should probably plug in a charger now"), +- icon: "battery_android_frame_1" +- }, +- { +- level: 5, +- title: qsTr("Critical battery level"), +- message: qsTr("PLUG THE CHARGER RIGHT NOW!!"), +- icon: "battery_android_alert", +- critical: true +- }, +- ] +- property int criticalLevel: 3 +- } +-} +diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml +deleted file mode 100644 +index 7f9c7881..00000000 +--- a/config/LauncherConfig.qml ++++ /dev/null +@@ -1,146 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool enabled: true +- property bool showOnHover: false +- property int maxShown: 7 +- property int maxWallpapers: 9 // Warning: even numbers look bad +- property string specialPrefix: "@" +- property string actionPrefix: ">" +- property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout +- property int dragThreshold: 50 +- property bool vimKeybinds: false +- property list hiddenApps: [] +- property UseFuzzy useFuzzy: UseFuzzy {} +- property Sizes sizes: Sizes {} +- +- component UseFuzzy: JsonObject { +- property bool apps: false +- property bool actions: false +- property bool schemes: false +- property bool variants: false +- property bool wallpapers: false +- } +- +- component Sizes: JsonObject { +- property int itemWidth: 600 +- property int itemHeight: 57 +- property int wallpaperWidth: 280 +- property int wallpaperHeight: 200 +- } +- +- property list actions: [ +- { +- name: "Calculator", +- icon: "calculate", +- description: "Do simple math equations (powered by Qalc)", +- command: ["autocomplete", "calc"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Scheme", +- icon: "palette", +- description: "Change the current colour scheme", +- command: ["autocomplete", "scheme"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Wallpaper", +- icon: "image", +- description: "Change the current wallpaper", +- command: ["autocomplete", "wallpaper"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Variant", +- icon: "colors", +- description: "Change the current scheme variant", +- command: ["autocomplete", "variant"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Transparency", +- icon: "opacity", +- description: "Change shell transparency", +- command: ["autocomplete", "transparency"], +- enabled: false, +- dangerous: false +- }, +- { +- name: "Random", +- icon: "casino", +- description: "Switch to a random wallpaper", +- command: ["caelestia", "wallpaper", "-r"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Light", +- icon: "light_mode", +- description: "Change the scheme to light mode", +- command: ["setMode", "light"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Dark", +- icon: "dark_mode", +- description: "Change the scheme to dark mode", +- command: ["setMode", "dark"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Shutdown", +- icon: "power_settings_new", +- description: "Shutdown the system", +- command: ["systemctl", "poweroff"], +- enabled: true, +- dangerous: true +- }, +- { +- name: "Reboot", +- icon: "cached", +- description: "Reboot the system", +- command: ["systemctl", "reboot"], +- enabled: true, +- dangerous: true +- }, +- { +- name: "Logout", +- icon: "exit_to_app", +- description: "Log out of the current session", +- command: ["loginctl", "terminate-user", ""], +- enabled: true, +- dangerous: true +- }, +- { +- name: "Lock", +- icon: "lock", +- description: "Lock the current session", +- command: ["loginctl", "lock-session"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Sleep", +- icon: "bedtime", +- description: "Suspend then hibernate", +- command: ["systemctl", "suspend-then-hibernate"], +- enabled: true, +- dangerous: false +- }, +- { +- name: "Settings", +- icon: "settings", +- description: "Configure the shell", +- command: ["caelestia", "shell", "controlCenter", "open"], +- enabled: true, +- dangerous: false +- } +- ] +-} +diff --git a/config/LockConfig.qml b/config/LockConfig.qml +deleted file mode 100644 +index 2af4e2cd..00000000 +--- a/config/LockConfig.qml ++++ /dev/null +@@ -1,14 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool recolourLogo: false +- property bool enableFprint: true +- property int maxFprintTries: 3 +- property Sizes sizes: Sizes {} +- +- component Sizes: JsonObject { +- property real heightMult: 0.7 +- property real ratio: 16 / 9 +- property int centerWidth: 600 +- } +-} +diff --git a/config/NotifsConfig.qml b/config/NotifsConfig.qml +deleted file mode 100644 +index fa2db494..00000000 +--- a/config/NotifsConfig.qml ++++ /dev/null +@@ -1,18 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool expire: true +- property int defaultExpireTimeout: 5000 +- property real clearThreshold: 0.3 +- property int expandThreshold: 20 +- property bool actionOnClick: false +- property int groupPreviewNum: 3 +- property bool openExpanded: false // Show the notifichation in expanded state when opening +- property Sizes sizes: Sizes {} +- +- component Sizes: JsonObject { +- property int width: 400 +- property int image: 41 +- property int badge: 20 +- } +-} +diff --git a/config/OsdConfig.qml b/config/OsdConfig.qml +deleted file mode 100644 +index 543fc41e..00000000 +--- a/config/OsdConfig.qml ++++ /dev/null +@@ -1,14 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool enabled: true +- property int hideDelay: 2000 +- property bool enableBrightness: true +- property bool enableMicrophone: false +- property Sizes sizes: Sizes {} +- +- component Sizes: JsonObject { +- property int sliderWidth: 30 +- property int sliderHeight: 150 +- } +-} +diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml +deleted file mode 100644 +index d083b7a1..00000000 +--- a/config/ServiceConfig.qml ++++ /dev/null +@@ -1,21 +0,0 @@ +-import Quickshell.Io +-import QtQuick +- +-JsonObject { +- property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" +- property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) +- property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") +- property string gpuType: "" +- property int visualiserBars: 45 +- property real audioIncrement: 0.1 +- property real brightnessIncrement: 0.1 +- property real maxVolume: 1.0 +- property bool smartScheme: true +- property string defaultPlayer: "Spotify" +- property list playerAliases: [ +- { +- "from": "com.github.th_ch.youtube_music", +- "to": "YT Music" +- } +- ] +-} +diff --git a/config/SessionConfig.qml b/config/SessionConfig.qml +deleted file mode 100644 +index f65ec6d8..00000000 +--- a/config/SessionConfig.qml ++++ /dev/null +@@ -1,21 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool enabled: true +- property int dragThreshold: 30 +- property bool vimKeybinds: false +- property Commands commands: Commands {} +- +- property Sizes sizes: Sizes {} +- +- component Commands: JsonObject { +- property list logout: ["loginctl", "terminate-user", ""] +- property list shutdown: ["systemctl", "poweroff"] +- property list hibernate: ["systemctl", "hibernate"] +- property list reboot: ["systemctl", "reboot"] +- } +- +- component Sizes: JsonObject { +- property int button: 80 +- } +-} +diff --git a/config/SidebarConfig.qml b/config/SidebarConfig.qml +deleted file mode 100644 +index a871562b..00000000 +--- a/config/SidebarConfig.qml ++++ /dev/null +@@ -1,11 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool enabled: true +- property int dragThreshold: 80 +- property Sizes sizes: Sizes {} +- +- component Sizes: JsonObject { +- property int width: 430 +- } +-} +diff --git a/config/UserPaths.qml b/config/UserPaths.qml +deleted file mode 100644 +index f8de2678..00000000 +--- a/config/UserPaths.qml ++++ /dev/null +@@ -1,8 +0,0 @@ +-import qs.utils +-import Quickshell.Io +- +-JsonObject { +- property string wallpaperDir: `${Paths.pictures}/Wallpapers` +- property string sessionGif: "root:/assets/kurukuru.gif" +- property string mediaGif: "root:/assets/bongocat.gif" +-} +diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml +deleted file mode 100644 +index 6b845215..00000000 +--- a/config/UtilitiesConfig.qml ++++ /dev/null +@@ -1,42 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property bool enabled: true +- property int maxToasts: 4 +- +- property Sizes sizes: Sizes {} +- property Toasts toasts: Toasts {} +- property Vpn vpn: Vpn {} +- property Recording recording: Recording {} +- +- component Recording: JsonObject { +- property string videoMode: "fullscreen" +- property bool recordSystem: false +- property bool recordMicrophone: false +- } +- +- component Sizes: JsonObject { +- property int width: 430 +- property int toastWidth: 430 +- } +- +- component Toasts: JsonObject { +- property bool configLoaded: true +- property bool chargingChanged: true +- property bool gameModeChanged: true +- property bool dndChanged: true +- property bool audioOutputChanged: true +- property bool audioInputChanged: true +- property bool capsLockChanged: true +- property bool numLockChanged: true +- property bool kbLayoutChanged: true +- property bool kbLimit: true +- property bool vpnChanged: true +- property bool nowPlaying: false +- } +- +- component Vpn: JsonObject { +- property bool enabled: false +- property list provider: ["netbird"] +- } +-} +diff --git a/config/WInfoConfig.qml b/config/WInfoConfig.qml +deleted file mode 100644 +index 50257807..00000000 +--- a/config/WInfoConfig.qml ++++ /dev/null +@@ -1,10 +0,0 @@ +-import Quickshell.Io +- +-JsonObject { +- property Sizes sizes: Sizes {} +- +- component Sizes: JsonObject { +- property real heightMult: 0.7 +- property real detailsWidth: 500 +- } +-} +diff --git a/extras/version.cpp b/extras/version.cpp +index e1a0cf30..d6343417 100644 +--- a/extras/version.cpp ++++ b/extras/version.cpp +@@ -10,7 +10,8 @@ int main(int argc, char* argv[]) { + std::cout << GIT_REVISION << std::endl; + std::cout << DISTRIBUTOR << std::endl; + } else if (arg == "-s" || arg == "--short") { +- std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION << ", distrubuted by: " << DISTRIBUTOR << std::endl; ++ std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION ++ << ", distributed by: " << DISTRIBUTOR << std::endl; + } else { + std::cout << "Usage: " << argv[0] << " [-t | --terse] [-s | --short]" << std::endl; + return arg != "-h" && arg != "--help"; +diff --git a/flake.lock b/flake.lock +index 5b7fbd21..0721599d 100644 +--- a/flake.lock ++++ b/flake.lock +@@ -8,11 +8,11 @@ + ] + }, + "locked": { +- "lastModified": 1769226332, +- "narHash": "sha256-JKD9M2+/J4e6nRtcY2XRfpLlOHaGXT4aUHyIG/20qlw=", ++ "lastModified": 1778644647, ++ "narHash": "sha256-iQIu4b5by8B3qKeigungSIgRoJg9HS1NiIZwqIbCGYk=", + "owner": "caelestia-dots", + "repo": "cli", +- "rev": "52a3a3c50ef55e3561057e8a74c85cf16f83039f", ++ "rev": "2ce6213698d1cfa15b4b067d35c3cda634f443dd", + "type": "github" + }, + "original": { +@@ -23,11 +23,11 @@ + }, + "nixpkgs": { + "locked": { +- "lastModified": 1769018530, +- "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", ++ "lastModified": 1778869304, ++ "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", + "owner": "nixos", + "repo": "nixpkgs", +- "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", ++ "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", + "type": "github" + }, + "original": { +@@ -44,11 +44,11 @@ + ] + }, + "locked": { +- "lastModified": 1768985439, +- "narHash": "sha256-qkU4r+l+UPz4dutMMRZSin64HuVZkEv9iFpu9yMWVY0=", ++ "lastModified": 1778488696, ++ "narHash": "sha256-QSWgYuZUCNUJ/cxmaq83WkcT7lHQDDfsPVgH+96kIl0=", + "ref": "refs/heads/master", +- "rev": "191085a8821b35680bba16ce5411fc9dbe912237", +- "revCount": 731, ++ "rev": "7d1c9a9c6721606b129829134d6f614f015621e2", ++ "revCount": 818, + "type": "git", + "url": "https://git.outfoxxed.me/outfoxxed/quickshell" + }, +diff --git a/flake.nix b/flake.nix +index 5c884115..0d7ebb8c 100644 +--- a/flake.nix ++++ b/flake.nix +@@ -36,7 +36,6 @@ + withX11 = false; + withI3 = false; + }; +- app2unit = pkgs.callPackage ./nix/app2unit.nix {inherit pkgs;}; + caelestia-cli = inputs.caelestia-cli.packages.${pkgs.stdenv.hostPlatform.system}.default; + }; + with-cli = caelestia-shell.override {withCli = true;}; +diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml +index d24cff27..2d13b5a3 100644 +--- a/modules/BatteryMonitor.qml ++++ b/modules/BatteryMonitor.qml +@@ -1,33 +1,31 @@ +-import qs.config +-import Caelestia ++import QtQuick + import Quickshell + import Quickshell.Services.UPower +-import QtQuick ++import Caelestia ++import Caelestia.Config + + Scope { + id: root + +- readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) ++ readonly property list warnLevels: [...GlobalConfig.general.battery.warnLevels].sort((a, b) => b.level - a.level) + + Connections { +- target: UPower +- + function onOnBatteryChanged(): void { + if (UPower.onBattery) { +- if (Config.utilities.toasts.chargingChanged) ++ if (GlobalConfig.utilities.toasts.chargingChanged) + Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); + } else { +- if (Config.utilities.toasts.chargingChanged) ++ if (GlobalConfig.utilities.toasts.chargingChanged) + Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); + for (const level of root.warnLevels) + level.warned = false; + } + } ++ ++ target: UPower + } + + Connections { +- target: UPower.displayDevice +- + function onPercentageChanged(): void { + if (!UPower.onBattery) + return; +@@ -40,11 +38,13 @@ Scope { + } + } + +- if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) { ++ if (!hibernateTimer.running && p <= GlobalConfig.general.battery.criticalLevel) { + Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error); + hibernateTimer.start(); + } + } ++ ++ target: UPower.displayDevice + } + + Timer { +diff --git a/modules/ConfigToasts.qml b/modules/ConfigToasts.qml +new file mode 100644 +index 00000000..81463b8d +--- /dev/null ++++ b/modules/ConfigToasts.qml +@@ -0,0 +1,39 @@ ++import QtQuick ++import Quickshell ++import Caelestia ++import Caelestia.Config ++ ++Scope { ++ Connections { ++ function onLoaded(): void { ++ if (GlobalConfig.utilities.toasts.configLoaded) ++ Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded successfully!"), "rule_settings"); ++ } ++ ++ function onLoadFailed(error: string, screen: string): void { ++ Toaster.toast(qsTr("Failed to parse config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Warning); ++ } ++ ++ function onSaveFailed(error: string, screen: string): void { ++ Toaster.toast(qsTr("Failed to save config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Error); ++ } ++ ++ function onUnknownOption(key: string, screen: string): void { ++ Toaster.toast(qsTr("Unknown option in%1 config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); ++ } ++ ++ target: GlobalConfig ++ } ++ ++ Connections { ++ function onLoadFailed(error: string, screen: string): void { ++ Toaster.toast(qsTr("Failed to parse token config%1").arg(screen ? "for " + screen : ""), error, "settings_alert", Toast.Warning); ++ } ++ ++ function onUnknownOption(key: string, screen: string): void { ++ Toaster.toast(qsTr("Unknown option in%1 token config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); ++ } ++ ++ target: TokenConfig ++ } ++} +diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml +index b7ce0584..417b0e8a 100644 +--- a/modules/IdleMonitors.qml ++++ b/modules/IdleMonitors.qml +@@ -1,17 +1,17 @@ + pragma ComponentBehavior: Bound + + import "lock" +-import qs.config +-import qs.services +-import Caelestia.Internal + import Quickshell + import Quickshell.Wayland ++import Caelestia.Config ++import Caelestia.Internal ++import qs.services + + Scope { + id: root + + required property Lock lock +- readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) ++ readonly property bool enabled: !GlobalConfig.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) + + function handleIdleAction(action: var): void { + if (!action) +@@ -29,7 +29,7 @@ Scope { + + LogindManager { + onAboutToSleep: { +- if (Config.general.idle.lockBeforeSleep) ++ if (GlobalConfig.general.idle.lockBeforeSleep) + root.lock.lock.locked = true; + } + onLockRequested: root.lock.lock.locked = true +@@ -37,7 +37,7 @@ Scope { + } + + Variants { +- model: Config.general.idle.timeouts ++ model: GlobalConfig.general.idle.timeouts + + IdleMonitor { + required property var modelData +diff --git a/modules/MonitorIdentifier.qml b/modules/MonitorIdentifier.qml +index 096660a4..a618c6b5 100644 +--- a/modules/MonitorIdentifier.qml ++++ b/modules/MonitorIdentifier.qml +@@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound + import qs.components + import qs.components.containers + import qs.services +-import qs.config ++import Caelestia.Config + import Quickshell + import Quickshell.Wayland + import Quickshell.Hyprland +@@ -43,9 +43,9 @@ Variants { + StyledRect { + id: identifierRect + anchors.centerIn: parent +- implicitWidth: Appearance.padding.large * 14 +- implicitHeight: Appearance.padding.large * 14 +- radius: Appearance.rounding.large ++ implicitWidth: Tokens.padding.large * 14 ++ implicitHeight: Tokens.padding.large * 14 ++ radius: Tokens.rounding.large + color: Colours.tPalette.m3surfaceContainer + opacity: root.active ? 0.92 : 0 + +@@ -57,7 +57,7 @@ Variants { + + ColumnLayout { + anchors.centerIn: parent +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter +@@ -70,7 +70,7 @@ Variants { + StyledText { + Layout.alignment: Qt.AlignHCenter + text: win.monitor?.name ?? "" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + +diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml +index a62b827e..9ee79676 100644 +--- a/modules/Shortcuts.qml ++++ b/modules/Shortcuts.qml +@@ -1,23 +1,28 @@ +-import qs.components.misc +-import qs.modules.controlcenter +-import qs.services +-import Caelestia ++import QtQuick + import Quickshell + import Quickshell.Io ++import Caelestia ++import qs.components.misc ++import qs.services ++import qs.modules.controlcenter + + Scope { + id: root + + property bool launcherInterrupted +- readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false ++ readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "controlCenter" + description: "Open control center" + onPressed: WindowFactory.create() + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "showall" + description: "Toggle launcher, dashboard and osd" + onPressed: { +@@ -28,7 +33,9 @@ Scope { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "dashboard" + description: "Toggle dashboard" + onPressed: { +@@ -39,7 +46,9 @@ Scope { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "session" + description: "Toggle session menu" + onPressed: { +@@ -50,7 +59,9 @@ Scope { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "launcher" + description: "Toggle launcher" + onPressed: root.launcherInterrupted = false +@@ -63,15 +74,41 @@ Scope { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "launcherInterrupt" + description: "Interrupt launcher keybind" + onPressed: root.launcherInterrupted = true + } + +- IpcHandler { +- target: "drawers" ++ // qmllint disable unresolved-type ++ CustomShortcut { ++ // qmllint enable unresolved-type ++ name: "sidebar" ++ description: "Toggle sidebar" ++ onPressed: { ++ if (root.hasFullscreen) ++ return; ++ const visibilities = Visibilities.getForActive(); ++ visibilities.sidebar = !visibilities.sidebar; ++ } ++ } + ++ // qmllint disable unresolved-type ++ CustomShortcut { ++ // qmllint enable unresolved-type ++ name: "utilities" ++ description: "Toggle utilities" ++ onPressed: { ++ if (root.hasFullscreen) ++ return; ++ const visibilities = Visibilities.getForActive(); ++ visibilities.utilities = !visibilities.utilities; ++ } ++ } ++ ++ IpcHandler { + function toggle(drawer: string): void { + if (list().split("\n").includes(drawer)) { + if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer)) +@@ -79,7 +116,7 @@ Scope { + const visibilities = Visibilities.getForActive(); + visibilities[drawer] = !visibilities[drawer]; + } else { +- console.warn(`[IPC] Drawer "${drawer}" does not exist`); ++ console.warn(lc, `Drawer "${drawer}" does not exist`); + } + } + +@@ -87,19 +124,19 @@ Scope { + const visibilities = Visibilities.getForActive(); + return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n"); + } ++ ++ target: "drawers" + } + + IpcHandler { +- target: "controlCenter" +- + function open(): void { + WindowFactory.create(); + } ++ ++ target: "controlCenter" + } + + IpcHandler { +- target: "toaster" +- + function info(title: string, message: string, icon: string): void { + Toaster.toast(title, message, icon, Toast.Info); + } +@@ -115,5 +152,14 @@ Scope { + function error(title: string, message: string, icon: string): void { + Toaster.toast(title, message, icon, Toast.Error); + } ++ ++ target: "toaster" ++ } ++ ++ LoggingCategory { ++ id: lc ++ ++ name: "caelestia.qml.shortcuts" ++ defaultLogLevel: LoggingCategory.Info + } + } +diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml +index 0d8b2fe1..76cc1039 100644 +--- a/modules/areapicker/AreaPicker.qml ++++ b/modules/areapicker/AreaPicker.qml +@@ -1,10 +1,11 @@ + pragma ComponentBehavior: Bound + +-import qs.components.containers +-import qs.components.misc + import Quickshell +-import Quickshell.Wayland + import Quickshell.Io ++import Quickshell.Wayland ++import qs.components.containers ++import qs.components.misc ++import qs.services + + Scope { + LazyLoader { +@@ -15,7 +16,7 @@ Scope { + property bool clipboardOnly + + Variants { +- model: Quickshell.screens ++ model: Screens.screens + + StyledWindow { + id: win +@@ -47,8 +48,6 @@ Scope { + } + + IpcHandler { +- target: "picker" +- + function open(): void { + root.freeze = false; + root.closing = false; +@@ -76,9 +75,13 @@ Scope { + root.clipboardOnly = true; + root.activeAsync = true; + } ++ ++ target: "picker" + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "screenshot" + description: "Open screenshot tool" + onPressed: { +@@ -89,7 +92,9 @@ Scope { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "screenshotFreeze" + description: "Open screenshot tool (freeze mode)" + onPressed: { +@@ -100,7 +105,9 @@ Scope { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "screenshotClip" + description: "Open screenshot tool (clipboard)" + onPressed: { +@@ -111,7 +118,9 @@ Scope { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "screenshotFreezeClip" + description: "Open screenshot tool (freeze mode, clipboard)" + onPressed: { +diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml +index 3be2bf30..75c45ab9 100644 +--- a/modules/areapicker/Picker.qml ++++ b/modules/areapicker/Picker.qml +@@ -1,13 +1,13 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import Caelestia +-import Quickshell +-import Quickshell.Wayland + import QtQuick + import QtQuick.Effects ++import Quickshell ++import Quickshell.Io ++import Quickshell.Wayland ++import Caelestia ++import qs.components ++import qs.services + + MouseArea { + id: root +@@ -80,9 +80,12 @@ MouseArea { + } else { + Quickshell.execDetached(["swappy", "-f", path]); + } +- closeAnim.start(); ++ closeAnim.start(); ++ }, () => { ++ console.error("Failed to save screenshot"); ++ closeAnim.start(); + }) +-} ++} + + onClientsChanged: checkClientRects(mouseX, mouseY) + +@@ -166,7 +169,7 @@ onClientsChanged: checkClientRects(mouseX, mouseY) + target: root + property: "opacity" + to: 0 +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + ExAnim { + target: root +@@ -191,9 +194,21 @@ onClientsChanged: checkClientRects(mouseX, mouseY) + } + } + ++ Process { ++ running: true ++ command: ["hyprctl", "cursorpos", "-j"] ++ stdout: StdioCollector { ++ onStreamFinished: { ++ const pos = JSON.parse(text); ++ root.checkClientRects(pos.x - root.screen.x, pos.y - root.screen.y); ++ } ++ } ++ } ++ + Loader { + id: screencopy + ++ asynchronous: true + anchors.fill: parent + + active: root.loader.freeze +@@ -207,6 +222,13 @@ onClientsChanged: checkClientRects(mouseX, mouseY) + root.save(); + } + } ++ ++ Component.onCompleted: { ++ if (hasContent && !root.loader.freeze) { ++ overlay.visible = border.visible = true; ++ root.save(); ++ } ++ } + } + } + +@@ -265,7 +287,7 @@ onClientsChanged: checkClientRects(mouseX, mouseY) + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + } + +@@ -294,7 +316,6 @@ onClientsChanged: checkClientRects(mouseX, mouseY) + } + + component ExAnim: Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } +diff --git a/modules/background/Background.qml b/modules/background/Background.qml +index f8484e16..70f6914d 100644 +--- a/modules/background/Background.qml ++++ b/modules/background/Background.qml +@@ -1,146 +1,158 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Quickshell.Wayland ++import Caelestia.Config + import qs.components + import qs.components.containers + import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Wayland +-import QtQuick + +-Loader { +- active: Config.background.enabled ++Variants { ++ model: Screens.screens.filter(s => GlobalConfig.forScreen(s.name).background.enabled) + +- sourceComponent: Variants { +- model: Quickshell.screens ++ StyledWindow { ++ id: win + +- StyledWindow { +- id: win ++ required property ShellScreen modelData + +- required property ShellScreen modelData ++ screen: modelData ++ name: "background" ++ WlrLayershell.exclusionMode: ExclusionMode.Ignore ++ WlrLayershell.layer: contentItem.Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom ++ color: contentItem.Config.background.wallpaperEnabled ? "black" : "transparent" ++ surfaceFormat.opaque: false + +- screen: modelData +- name: "background" +- WlrLayershell.exclusionMode: ExclusionMode.Ignore +- WlrLayershell.layer: WlrLayer.Background +- color: "black" ++ anchors.top: true ++ anchors.bottom: true ++ anchors.left: true ++ anchors.right: true + +- anchors.top: true +- anchors.bottom: true +- anchors.left: true +- anchors.right: true ++ Item { ++ id: behindClock + +- Item { +- id: behindClock ++ anchors.fill: parent ++ ++ Loader { ++ id: wallpaper ++ ++ asynchronous: true + + anchors.fill: parent ++ active: Config.background.wallpaperEnabled + +- Wallpaper { +- id: wallpaper +- } ++ sourceComponent: Wallpaper {} ++ } + +- Visualiser { +- anchors.fill: parent +- screen: win.modelData +- wallpaper: wallpaper +- } ++ Visualiser { ++ anchors.fill: parent ++ screen: win.modelData ++ wallpaper: wallpaper + } ++ } + +- Loader { +- id: clockLoader +- active: Config.background.desktopClock.enabled +- +- anchors.margins: Appearance.padding.large * 2 +- anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) +- +- state: Config.background.desktopClock.position +- states: [ +- State { +- name: "top-left" +- AnchorChanges { +- target: clockLoader +- anchors.top: parent.top +- anchors.left: parent.left +- } +- }, +- State { +- name: "top-center" +- AnchorChanges { +- target: clockLoader +- anchors.top: parent.top +- anchors.horizontalCenter: parent.horizontalCenter +- } +- }, +- State { +- name: "top-right" +- AnchorChanges { +- target: clockLoader +- anchors.top: parent.top +- anchors.right: parent.right +- } +- }, +- State { +- name: "middle-left" +- AnchorChanges { +- target: clockLoader +- anchors.verticalCenter: parent.verticalCenter +- anchors.left: parent.left +- } +- }, +- State { +- name: "middle-center" +- AnchorChanges { +- target: clockLoader +- anchors.verticalCenter: parent.verticalCenter +- anchors.horizontalCenter: parent.horizontalCenter +- } +- }, +- State { +- name: "middle-right" +- AnchorChanges { +- target: clockLoader +- anchors.verticalCenter: parent.verticalCenter +- anchors.right: parent.right +- } +- }, +- State { +- name: "bottom-left" +- AnchorChanges { +- target: clockLoader +- anchors.bottom: parent.bottom +- anchors.left: parent.left +- } +- }, +- State { +- name: "bottom-center" +- AnchorChanges { +- target: clockLoader +- anchors.bottom: parent.bottom +- anchors.horizontalCenter: parent.horizontalCenter +- } +- }, +- State { +- name: "bottom-right" +- AnchorChanges { +- target: clockLoader +- anchors.bottom: parent.bottom +- anchors.right: parent.right +- } +- } +- ] ++ Loader { ++ id: clockLoader ++ ++ asynchronous: true ++ active: Config.background.desktopClock.enabled ++ ++ anchors.margins: Tokens.padding.large * 2 ++ anchors.leftMargin: Tokens.padding.large * 2 + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) ++ ++ state: Config.background.desktopClock.position ++ states: [ ++ State { ++ name: "top-left" + +- transitions: Transition { +- AnchorAnimation { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ AnchorChanges { ++ target: clockLoader ++ anchors.top: parent.top ++ anchors.left: parent.left ++ } ++ }, ++ State { ++ name: "top-center" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.top: parent.top ++ anchors.horizontalCenter: parent.horizontalCenter ++ } ++ }, ++ State { ++ name: "top-right" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.top: parent.top ++ anchors.right: parent.right ++ } ++ }, ++ State { ++ name: "middle-left" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.left: parent.left ++ } ++ }, ++ State { ++ name: "middle-center" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.horizontalCenter: parent.horizontalCenter ++ } ++ }, ++ State { ++ name: "middle-right" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.right: parent.right ++ } ++ }, ++ State { ++ name: "bottom-left" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.bottom: parent.bottom ++ anchors.left: parent.left ++ } ++ }, ++ State { ++ name: "bottom-center" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.bottom: parent.bottom ++ anchors.horizontalCenter: parent.horizontalCenter ++ } ++ }, ++ State { ++ name: "bottom-right" ++ ++ AnchorChanges { ++ target: clockLoader ++ anchors.bottom: parent.bottom ++ anchors.right: parent.right + } + } ++ ] + +- sourceComponent: DesktopClock { +- wallpaper: behindClock +- absX: clockLoader.x +- absY: clockLoader.y +- } ++ transitions: Transition { ++ AnchorAnim {} ++ } ++ ++ sourceComponent: DesktopClock { ++ wallpaper: behindClock ++ absX: clockLoader.x ++ absY: clockLoader.y + } + } + } +diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml +index 77fe447f..c7415be8 100644 +--- a/modules/background/DesktopClock.qml ++++ b/modules/background/DesktopClock.qml +@@ -1,11 +1,11 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config + import QtQuick +-import QtQuick.Layouts + import QtQuick.Effects ++import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root +@@ -14,17 +14,17 @@ Item { + required property real absX + required property real absY + +- property real scale: Config.background.desktopClock.scale ++ property real clockScale: Config.background.desktopClock.scale + readonly property bool bgEnabled: Config.background.desktopClock.background.enabled +- readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur ++ readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled + readonly property bool invertColors: Config.background.desktopClock.invertColors + readonly property bool useLightSet: Colours.light ? !invertColors : invertColors + readonly property color safePrimary: useLightSet ? Colours.palette.m3primaryContainer : Colours.palette.m3primary + readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary + readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary + +- implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.scale) +- implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.scale) ++ implicitWidth: layout.implicitWidth + (Tokens.padding.large * 4 * root.clockScale) ++ implicitHeight: layout.implicitHeight + (Tokens.padding.large * 2 * root.clockScale) + + Item { + id: clockContainer +@@ -40,6 +40,7 @@ Item { + } + + Loader { ++ asynchronous: true + anchors.fill: parent + active: root.blurEnabled + +@@ -62,7 +63,7 @@ Item { + + visible: root.bgEnabled + anchors.fill: parent +- radius: Appearance.rounding.large * root.scale ++ radius: Tokens.rounding.large * root.clockScale + opacity: Config.background.desktopClock.background.opacity + color: Colours.palette.m3surface + +@@ -73,43 +74,44 @@ Item { + id: layout + + anchors.centerIn: parent +- spacing: Appearance.spacing.larger * root.scale ++ spacing: Tokens.spacing.larger * root.clockScale + + RowLayout { +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: Time.hourStr +- font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale ++ font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale + font.weight: Font.Bold + color: root.safePrimary + } + + StyledText { + text: ":" +- font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale ++ font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale + color: root.safeTertiary + opacity: 0.8 +- Layout.topMargin: -Appearance.padding.large * 1.5 * root.scale ++ Layout.topMargin: -Tokens.padding.large * 1.5 * root.clockScale + } + + StyledText { + text: Time.minuteStr +- font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale ++ font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale + font.weight: Font.Bold + color: root.safeSecondary + } + + Loader { ++ asynchronous: true + Layout.alignment: Qt.AlignTop +- Layout.topMargin: Appearance.padding.large * 1.4 * root.scale ++ Layout.topMargin: Tokens.padding.large * 1.4 * root.clockScale + +- active: Config.services.useTwelveHourClock ++ active: GlobalConfig.services.useTwelveHourClock + visible: active + + sourceComponent: StyledText { + text: Time.amPmStr +- font.pointSize: Appearance.font.size.large * root.scale ++ font.pointSize: Tokens.font.size.large * root.clockScale + color: root.safeSecondary + } + } +@@ -117,10 +119,10 @@ Item { + + StyledRect { + Layout.fillHeight: true +- Layout.preferredWidth: 4 * root.scale +- Layout.topMargin: Appearance.spacing.larger * root.scale +- Layout.bottomMargin: Appearance.spacing.larger * root.scale +- radius: Appearance.rounding.full ++ Layout.preferredWidth: 4 * root.clockScale ++ Layout.topMargin: Tokens.spacing.larger * root.clockScale ++ Layout.bottomMargin: Tokens.spacing.larger * root.clockScale ++ radius: Tokens.rounding.full + color: root.safePrimary + opacity: 0.8 + } +@@ -130,7 +132,7 @@ Item { + + StyledText { + text: Time.format("MMMM").toUpperCase() +- font.pointSize: Appearance.font.size.large * root.scale ++ font.pointSize: Tokens.font.size.large * root.clockScale + font.letterSpacing: 4 + font.weight: Font.Bold + color: root.safeSecondary +@@ -138,7 +140,7 @@ Item { + + StyledText { + text: Time.format("dd") +- font.pointSize: Appearance.font.size.extraLarge * root.scale ++ font.pointSize: Tokens.font.size.extraLarge * root.clockScale + font.letterSpacing: 2 + font.weight: Font.Medium + color: root.safePrimary +@@ -146,7 +148,7 @@ Item { + + StyledText { + text: Time.format("dddd") +- font.pointSize: Appearance.font.size.larger * root.scale ++ font.pointSize: Tokens.font.size.larger * root.clockScale + font.letterSpacing: 2 + color: root.safeSecondary + } +@@ -154,16 +156,15 @@ Item { + } + } + +- Behavior on scale { ++ Behavior on clockScale { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + Behavior on implicitWidth { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml +index c9bb9efb..45055879 100644 +--- a/modules/background/Visualiser.qml ++++ b/modules/background/Visualiser.qml +@@ -1,19 +1,19 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import Caelestia.Services +-import Quickshell +-import Quickshell.Widgets + import QtQuick + import QtQuick.Effects ++import Quickshell ++import Caelestia.Config ++import Caelestia.Internal ++import Caelestia.Services ++import qs.components ++import qs.services + + Item { + id: root + + required property ShellScreen screen +- required property Wallpaper wallpaper ++ required property Item wallpaper + + readonly property bool shouldBeActive: Config.background.visualiser.enabled && (!Config.background.visualiser.autoHide || (Hypr.monitorFor(screen)?.activeWorkspace?.toplevels?.values.every(t => t.lastIpcObject?.floating) ?? true)) + property real offset: shouldBeActive ? 0 : screen.height * 0.2 +@@ -21,6 +21,7 @@ Item { + opacity: shouldBeActive ? 1 : 0 + + Loader { ++ asynchronous: true + anchors.fill: parent + active: root.opacity > 0 && Config.background.visualiser.blur + +@@ -42,6 +43,7 @@ Item { + layer.enabled: true + + Loader { ++ asynchronous: true + anchors.fill: parent + anchors.topMargin: root.offset + anchors.bottomMargin: -root.offset +@@ -53,25 +55,29 @@ Item { + service: Audio.cava + } + +- Item { +- id: content ++ VisualiserBars { ++ id: bars + + anchors.fill: parent + anchors.margins: Config.border.thickness +- anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing ++ anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Tokens.spacing.small * Config.background.visualiser.spacing + +- Side { +- content: content +- } +- Side { +- content: content +- isRight: true +- } ++ values: Audio.cava.values ++ primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) ++ secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) ++ rounding: Tokens.rounding.small * Config.background.visualiser.rounding ++ spacing: Tokens.spacing.small * Config.background.visualiser.spacing ++ animationDuration: Tokens.anim.durations.normal + + Behavior on anchors.leftMargin { + Anim {} + } + } ++ ++ FrameAnimation { ++ running: root.opacity > 0 && !bars.settled ++ onTriggered: bars.advance(frameTime) ++ } + } + } + } +@@ -83,69 +89,4 @@ Item { + Behavior on opacity { + Anim {} + } +- +- component Side: Repeater { +- id: side +- +- required property Item content +- property bool isRight +- +- model: Config.services.visualiserBars +- +- ClippingRectangle { +- id: bar +- +- required property int modelData +- property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1])) +- +- clip: true +- +- x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0) +- implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing +- +- y: side.content.height - height +- implicitHeight: bar.value * side.content.height * 0.4 +- +- color: "transparent" +- topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding +- topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding +- +- Rectangle { +- topLeftRadius: parent.topLeftRadius +- topRightRadius: parent.topRightRadius +- +- gradient: Gradient { +- orientation: Gradient.Vertical +- +- GradientStop { +- position: 0 +- color: Qt.alpha(Colours.palette.m3primary, 0.7) +- +- Behavior on color { +- CAnim {} +- } +- } +- GradientStop { +- position: 1 +- color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) +- +- Behavior on color { +- CAnim {} +- } +- } +- } +- +- anchors.left: parent.left +- anchors.right: parent.right +- y: parent.height - height +- implicitHeight: side.content.height * 0.4 +- } +- +- Behavior on value { +- Anim { +- duration: Appearance.anim.durations.small +- } +- } +- } +- } + } +diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml +index b5d7d4af..8cc2df9d 100644 +--- a/modules/background/Wallpaper.qml ++++ b/modules/background/Wallpaper.qml +@@ -1,20 +1,19 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Caelestia.Config + import qs.components +-import qs.components.images + import qs.components.filedialog ++import qs.components.images + import qs.services +-import qs.config + import qs.utils +-import QtQuick + + Item { + id: root + + property string source: Wallpapers.current + property Image current: one +- +- anchors.fill: parent ++ property bool completed + + onSourceChanged: { + if (!source) +@@ -27,43 +26,47 @@ Item { + + Component.onCompleted: { + if (source) +- Qt.callLater(() => one.update()); ++ Qt.callLater(() => { ++ one.update(); ++ completed = true; ++ }); + } + + Loader { ++ asynchronous: true + anchors.fill: parent + +- active: !root.source ++ active: root.completed && !root.source + + sourceComponent: StyledRect { + color: Colours.palette.m3surfaceContainer + + Row { + anchors.centerIn: parent +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + MaterialIcon { + text: "sentiment_stressed" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.extraLarge * 5 ++ font.pointSize: Tokens.font.size.extraLarge * 5 + } + + Column { + anchors.verticalCenter: parent.verticalCenter +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Wallpaper missing?") + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.extraLarge * 2 ++ font.pointSize: Tokens.font.size.extraLarge * 2 + font.bold: true + } + + StyledRect { +- implicitWidth: selectWallText.implicitWidth + Appearance.padding.large * 2 +- implicitHeight: selectWallText.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: selectWallText.implicitWidth + Tokens.padding.large * 2 ++ implicitHeight: selectWallText.implicitHeight + Tokens.padding.small * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3primary + + FileDialog { +@@ -78,10 +81,7 @@ Item { + StateLayer { + radius: parent.radius + color: Colours.palette.m3onPrimary +- +- function onClicked(): void { +- dialog.open(); +- } ++ onClicked: dialog.open() + } + + StyledText { +@@ -91,7 +91,7 @@ Item { + + text: qsTr("Set it now!") + color: Colours.palette.m3onPrimary +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml +index cb384e39..a2ed060e 100644 +--- a/modules/bar/Bar.qml ++++ b/modules/bar/Bar.qml +@@ -1,30 +1,32 @@ + pragma ComponentBehavior: Bound + +-import qs.services +-import qs.config + import "popouts" as BarPopouts + import "components" + import "components/workspaces" +-import Quickshell + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root + + required property ShellScreen screen +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + required property BarPopouts.Wrapper popouts +- readonly property int vPadding: Appearance.padding.large ++ required property bool fullscreen ++ readonly property int vPadding: Tokens.padding.large + + function closeTray(): void { + if (!Config.bar.tray.compact) + return; + + for (let i = 0; i < repeater.count; i++) { +- const item = repeater.itemAt(i); +- if (item?.enabled && item.id === "tray") { +- item.item.expanded = false; ++ const loader = repeater.itemAt(i) as WrappedLoader; ++ if (loader?.enabled && loader.id === "tray") { ++ (loader.item as Tray).expanded = false; + } + } + } +@@ -42,11 +44,9 @@ ColumnLayout { + + const id = ch.id; + const top = ch.y; +- const item = ch.item; +- const itemHeight = item.implicitHeight; + + if (id === "statusIcons" && Config.bar.popouts.statusIcons) { +- const items = item.items; ++ const items = (ch.item as StatusIcons).items; + const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); + if (icon) { + popouts.currentName = icon.name; +@@ -54,9 +54,10 @@ ColumnLayout { + popouts.hasCurrent = true; + } + } else if (id === "tray" && Config.bar.popouts.tray) { +- if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) { +- const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count); +- const trayItem = item.items.itemAt(index); ++ const tray = ch.item as Tray; ++ if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { ++ const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); ++ const trayItem = tray.items.itemAt(index); + if (trayItem) { + popouts.currentName = `traymenu${index}`; + popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); +@@ -66,11 +67,11 @@ ColumnLayout { + } + } else { + popouts.hasCurrent = false; +- item.expanded = true; ++ tray.expanded = true; + } +- } else if (id === "activeWindow" && Config.bar.popouts.activeWindow) { ++ } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { + popouts.currentName = id.toLowerCase(); +- popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y; ++ popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; + popouts.hasCurrent = true; + } + } +@@ -79,11 +80,11 @@ ColumnLayout { + const ch = childAt(width / 2, y) as WrappedLoader; + if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) { + // Workspace scroll +- const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); ++ const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); + const specialWs = mon?.lastIpcObject.specialWorkspace.name; + if (specialWs?.length > 0) + Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`); +- else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) ++ else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) + Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`); + } else if (y < screen.height / 2 && Config.bar.scrollActions.volume) { + // Volume scroll on top half +@@ -95,13 +96,13 @@ ColumnLayout { + // Brightness scroll on bottom half + const monitor = Brightness.getMonitorForScreen(screen); + if (angleDelta.y > 0) +- monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); ++ monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); + else if (angleDelta.y < 0) +- monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); ++ monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); + } + } + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Repeater { + id: repeater +@@ -128,12 +129,15 @@ ColumnLayout { + delegate: WrappedLoader { + sourceComponent: Workspaces { + screen: root.screen ++ fullscreen: root.fullscreen + } + } + } + DelegateChoice { + roleValue: "activeWindow" + delegate: WrappedLoader { ++ Layout.fillWidth: true ++ visible: !root.fullscreen + sourceComponent: ActiveWindow { + bar: root + monitor: Brightness.getMonitorForScreen(root.screen) +@@ -143,18 +147,21 @@ ColumnLayout { + DelegateChoice { + roleValue: "tray" + delegate: WrappedLoader { ++ visible: !root.fullscreen + sourceComponent: Tray {} + } + } + DelegateChoice { + roleValue: "clock" + delegate: WrappedLoader { ++ visible: !root.fullscreen + sourceComponent: Clock {} + } + } + DelegateChoice { + roleValue: "statusIcons" + delegate: WrappedLoader { ++ visible: !root.fullscreen + sourceComponent: StatusIcons {} + } + } +@@ -170,7 +177,7 @@ ColumnLayout { + } + + component WrappedLoader: Loader { +- required property bool enabled ++ required enabled + required property string id + required property int index + +@@ -193,6 +200,7 @@ ColumnLayout { + return null; + } + ++ asynchronous: true + Layout.alignment: Qt.AlignHCenter + + // Cursed ahh thing to add padding to first and last enabled components +diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml +index 29961b62..c57dcd64 100644 +--- a/modules/bar/BarWrapper.qml ++++ b/modules/bar/BarWrapper.qml +@@ -1,39 +1,44 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config +-import "popouts" as BarPopouts +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.utils ++import qs.modules.bar.popouts as BarPopouts + + Item { + id: root + + required property ShellScreen screen +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + required property BarPopouts.Wrapper popouts +- required property bool disabled ++ required property bool fullscreen ++ ++ readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) + +- readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) +- readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 +- readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness +- readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered) ++ readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) ++ readonly property int padding: Math.max(Tokens.padding.smaller, Config.border.thickness) ++ readonly property int contentWidth: Tokens.sizes.bar.innerWidth + padding * 2 ++ readonly property int exclusiveZone: shouldBeVisible ? contentWidth : Config.border.thickness ++ readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) + property bool isHovered + + function closeTray(): void { +- content.item?.closeTray(); ++ (content.item as Bar)?.closeTray(); + } + + function checkPopout(y: real): void { +- content.item?.checkPopout(y); ++ (content.item as Bar)?.checkPopout(y); + } + + function handleWheel(y: real, angleDelta: point): void { +- content.item?.handleWheel(y, angleDelta); ++ (content.item as Bar)?.handleWheel(y, angleDelta); + } + +- visible: width > Config.border.thickness +- implicitWidth: Config.border.thickness ++ clip: true ++ visible: width > 0 ++ implicitWidth: fullscreen ? 0 : Config.border.thickness + + states: State { + name: "visible" +@@ -52,8 +57,7 @@ Item { + Anim { + target: root + property: "implicitWidth" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + }, + Transition { +@@ -63,7 +67,7 @@ Item { + Anim { + target: root + property: "implicitWidth" +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + ] +@@ -81,7 +85,8 @@ Item { + width: root.contentWidth + screen: root.screen + visibilities: root.visibilities +- popouts: root.popouts ++ popouts: root.popouts // qmllint disable incompatible-type ++ fullscreen: root.fullscreen + } + } + } +diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml +index 0c9b21e6..f39aa4c3 100644 +--- a/modules/bar/components/ActiveWindow.qml ++++ b/modules/bar/components/ActiveWindow.qml +@@ -1,10 +1,10 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services + import qs.utils +-import qs.config +-import QtQuick + + Item { + id: root +@@ -13,6 +13,19 @@ Item { + required property Brightness.Monitor monitor + property color colour: Colours.palette.m3primary + ++ readonly property string windowTitle: { ++ const title = Hypr.activeToplevel?.title; ++ if (!title) ++ return qsTr("Desktop"); ++ if (Config.bar.activeWindow.compact) { ++ // " - " (standard hyphen), " — " (em dash), " – " (en dash) ++ const parts = title.split(/\s+[\-\u2013\u2014]\s+/); ++ if (parts.length > 1) ++ return parts[parts.length - 1].trim(); ++ } ++ return title; ++ } ++ + readonly property int maxHeight: { + const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer"); + const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0); +@@ -25,6 +38,32 @@ Item { + implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight) + implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin + ++ Loader { ++ asynchronous: true ++ anchors.fill: parent ++ active: !Config.bar.activeWindow.showOnHover ++ ++ sourceComponent: MouseArea { ++ cursorShape: Qt.PointingHandCursor ++ hoverEnabled: true ++ onPositionChanged: { ++ const popouts = root.bar.popouts; ++ if (popouts.hasCurrent && popouts.currentName !== "activewindow") ++ popouts.hasCurrent = false; ++ } ++ onClicked: { ++ const popouts = root.bar.popouts; ++ if (popouts.hasCurrent) { ++ popouts.hasCurrent = false; ++ } else { ++ popouts.currentName = "activewindow"; ++ popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y; ++ popouts.hasCurrent = true; ++ } ++ } ++ } ++ } ++ + MaterialIcon { + id: icon + +@@ -46,9 +85,9 @@ Item { + TextMetrics { + id: metrics + +- text: Hypr.activeToplevel?.title ?? qsTr("Desktop") +- font.pointSize: Appearance.font.size.smaller +- font.family: Appearance.font.family.mono ++ text: root.windowTitle ++ font.pointSize: root.Tokens.font.size.smaller ++ font.family: root.Tokens.font.family.mono + elide: Qt.ElideRight + elideWidth: root.maxHeight - icon.height + +@@ -62,8 +101,7 @@ Item { + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + +@@ -72,7 +110,7 @@ Item { + + anchors.horizontalCenter: icon.horizontalCenter + anchors.top: icon.bottom +- anchors.topMargin: Appearance.spacing.small ++ anchors.topMargin: Tokens.spacing.small + + font.pointSize: metrics.font.pointSize + font.family: metrics.font.family +@@ -81,10 +119,10 @@ Item { + + transform: [ + Translate { +- x: Config.bar.activeWindow.inverted ? -implicitWidth + text.implicitHeight : 0 ++ x: root.Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0 + }, + Rotation { +- angle: Config.bar.activeWindow.inverted ? 270 : 90 ++ angle: root.Config.bar.activeWindow.inverted ? 270 : 90 + origin.x: text.implicitHeight / 2 + origin.y: text.implicitHeight / 2 + } +diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml +index 801e93d7..ffe599af 100644 +--- a/modules/bar/components/Clock.qml ++++ b/modules/bar/components/Clock.qml +@@ -1,38 +1,71 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import QtQuick + +-Column { ++StyledRect { + id: root + +- property color colour: Colours.palette.m3tertiary ++ readonly property color colour: Colours.palette.m3tertiary ++ readonly property int padding: Config.bar.clock.background ? Tokens.padding.normal : Tokens.padding.small ++ ++ implicitWidth: Tokens.sizes.bar.innerWidth ++ implicitHeight: layout.implicitHeight + root.padding * 2 ++ ++ color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) ++ radius: Tokens.rounding.full + +- spacing: Appearance.spacing.small ++ Column { ++ id: layout + +- Loader { +- anchors.horizontalCenter: parent.horizontalCenter ++ anchors.centerIn: parent ++ spacing: Tokens.spacing.small ++ ++ Loader { ++ asynchronous: true ++ anchors.horizontalCenter: parent.horizontalCenter ++ ++ active: Config.bar.clock.showIcon ++ visible: active ++ ++ sourceComponent: MaterialIcon { ++ text: "calendar_month" ++ color: root.colour ++ } ++ } + +- active: Config.bar.clock.showIcon +- visible: active ++ StyledText { ++ anchors.horizontalCenter: parent.horizontalCenter + +- sourceComponent: MaterialIcon { +- text: "calendar_month" ++ visible: Config.bar.clock.showDate ++ ++ horizontalAlignment: StyledText.AlignHCenter ++ text: Time.format("ddd\nd") ++ font.pointSize: Tokens.font.size.smaller ++ font.family: Tokens.font.family.sans + color: root.colour + } +- } + +- StyledText { +- id: text ++ Rectangle { ++ anchors.horizontalCenter: parent.horizontalCenter ++ visible: Config.bar.clock.showDate ++ height: visible ? 1 : 0 + +- anchors.horizontalCenter: parent.horizontalCenter ++ width: parent.width * 0.8 ++ color: root.colour ++ opacity: 0.2 ++ } + +- horizontalAlignment: StyledText.AlignHCenter +- text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") +- font.pointSize: Appearance.font.size.smaller +- font.family: Appearance.font.family.mono +- color: root.colour ++ StyledText { ++ anchors.horizontalCenter: parent.horizontalCenter ++ ++ horizontalAlignment: StyledText.AlignHCenter ++ text: Time.format(GlobalConfig.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") ++ font.pointSize: Tokens.font.size.smaller ++ font.family: Tokens.font.family.mono ++ color: root.colour ++ } + } + } +diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml +index 2bc38644..94d1a1c4 100644 +--- a/modules/bar/components/OsIcon.qml ++++ b/modules/bar/components/OsIcon.qml +@@ -1,12 +1,16 @@ ++import QtQuick ++import Caelestia.Config ++import qs.components + import qs.components.effects + import qs.services +-import qs.config + import qs.utils +-import QtQuick + + Item { + id: root + ++ implicitWidth: Math.round(Tokens.font.size.large * 1.2) ++ implicitHeight: Math.round(Tokens.font.size.large * 1.2) ++ + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor +@@ -16,13 +20,28 @@ Item { + } + } + +- ColouredIcon { ++ Loader { ++ asynchronous: true + anchors.centerIn: parent +- source: SysInfo.osLogo +- implicitSize: Appearance.font.size.large * 1.2 +- colour: Colours.palette.m3tertiary ++ sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon ++ } ++ ++ Component { ++ id: caelestiaLogo ++ ++ Logo { ++ implicitWidth: Math.round(Tokens.font.size.large * 1.6) ++ implicitHeight: Math.round(Tokens.font.size.large * 1.6) ++ } + } + +- implicitWidth: Appearance.font.size.large * 1.2 +- implicitHeight: Appearance.font.size.large * 1.2 ++ Component { ++ id: distroIcon ++ ++ ColouredIcon { ++ source: SysInfo.osLogo ++ implicitSize: Math.round(Tokens.font.size.large * 1.2) ++ colour: Colours.palette.m3tertiary ++ } ++ } + } +diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml +index 917bdf7f..681a805d 100644 +--- a/modules/bar/components/Power.qml ++++ b/modules/bar/components/Power.qml +@@ -1,15 +1,14 @@ ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell +-import QtQuick + + Item { + id: root + +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + +- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: icon.implicitHeight + + StateLayer { +@@ -17,13 +16,9 @@ Item { + anchors.fill: undefined + anchors.centerIn: parent + implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 +- +- radius: Appearance.rounding.full +- +- function onClicked(): void { +- root.visibilities.session = !root.visibilities.session; +- } ++ implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 ++ radius: Tokens.rounding.full ++ onClicked: root.visibilities.session = !root.visibilities.session + } + + MaterialIcon { +@@ -35,6 +30,6 @@ Item { + text: "power_settings_new" + color: Colours.palette.m3error + font.bold: true +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + } +diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml +deleted file mode 100644 +index 5d562cef..00000000 +--- a/modules/bar/components/Settings.qml ++++ /dev/null +@@ -1,41 +0,0 @@ +-import qs.components +-import qs.modules.controlcenter +-import qs.services +-import qs.config +-import Quickshell +-import QtQuick +- +-Item { +- id: root +- +- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 +- implicitHeight: icon.implicitHeight +- +- StateLayer { +- // Cursed workaround to make the height larger than the parent +- anchors.fill: undefined +- anchors.centerIn: parent +- implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 +- +- radius: Appearance.rounding.full +- +- function onClicked(): void { +- WindowFactory.create(null, { +- active: "network" +- }); +- } +- } +- +- MaterialIcon { +- id: icon +- +- anchors.centerIn: parent +- anchors.horizontalCenterOffset: -1 +- +- text: "settings" +- color: Colours.palette.m3onSurface +- font.bold: true +- font.pointSize: Appearance.font.size.normal +- } +-} +diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml +deleted file mode 100644 +index 5d562cef..00000000 +--- a/modules/bar/components/SettingsIcon.qml ++++ /dev/null +@@ -1,41 +0,0 @@ +-import qs.components +-import qs.modules.controlcenter +-import qs.services +-import qs.config +-import Quickshell +-import QtQuick +- +-Item { +- id: root +- +- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 +- implicitHeight: icon.implicitHeight +- +- StateLayer { +- // Cursed workaround to make the height larger than the parent +- anchors.fill: undefined +- anchors.centerIn: parent +- implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 +- +- radius: Appearance.rounding.full +- +- function onClicked(): void { +- WindowFactory.create(null, { +- active: "network" +- }); +- } +- } +- +- MaterialIcon { +- id: icon +- +- anchors.centerIn: parent +- anchors.horizontalCenterOffset: -1 +- +- text: "settings" +- color: Colours.palette.m3onSurface +- font.bold: true +- font.pointSize: Appearance.font.size.normal +- } +-} +diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml +index ca7dc2e3..900e5574 100644 +--- a/modules/bar/components/StatusIcons.qml ++++ b/modules/bar/components/StatusIcons.qml +@@ -1,14 +1,14 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.utils +-import qs.config ++import QtQuick ++import QtQuick.Layouts + import Quickshell + import Quickshell.Bluetooth + import Quickshell.Services.UPower +-import QtQuick +-import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services ++import qs.utils + + StyledRect { + id: root +@@ -17,11 +17,11 @@ StyledRect { + readonly property alias items: iconColumn + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + clip: true +- implicitWidth: Config.bar.sizes.innerWidth +- implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) ++ implicitWidth: Tokens.sizes.bar.innerWidth ++ implicitHeight: iconColumn.implicitHeight + Tokens.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) + + ColumnLayout { + id: iconColumn +@@ -29,9 +29,9 @@ StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom +- anchors.bottomMargin: Appearance.padding.normal ++ anchors.bottomMargin: Tokens.padding.normal + +- spacing: Appearance.spacing.smaller / 2 ++ spacing: Tokens.spacing.smaller / 2 + + // Lock keys status + WrappedLoader { +@@ -136,7 +136,7 @@ StyledRect { + animate: true + text: Hypr.kbLayout + color: root.colour +- font.family: Appearance.font.family.mono ++ font.family: Tokens.font.family.mono + } + } + +@@ -172,15 +172,15 @@ StyledRect { + active: Config.bar.status.showBluetooth + + sourceComponent: ColumnLayout { +- spacing: Appearance.spacing.smaller / 2 ++ spacing: Tokens.spacing.smaller / 2 + + // Bluetooth icon + MaterialIcon { + animate: true + text: { +- if (!Bluetooth.defaultAdapter?.enabled) ++ if (!Bluetooth.defaultAdapter?.enabled) // qmllint disable unresolved-type + return "bluetooth_disabled"; +- if (Bluetooth.devices.values.some(d => d.connected)) ++ if (Bluetooth.devices.values.some(d => d.connected)) // qmllint disable unresolved-type + return "bluetooth_connected"; + return "bluetooth"; + } +@@ -190,7 +190,7 @@ StyledRect { + // Connected bluetooth devices + Repeater { + model: ScriptModel { +- values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) ++ values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) // qmllint disable unresolved-type + } + + MaterialIcon { +@@ -204,21 +204,21 @@ StyledRect { + fill: 1 + + SequentialAnimation on opacity { +- running: device.modelData?.state !== BluetoothDeviceState.Connected ++ running: device.modelData?.state !== BluetoothDeviceState.Connected // qmllint disable unresolved-type + alwaysRunToEnd: true + loops: Animation.Infinite + + Anim { + from: 1 + to: 0 +- duration: Appearance.anim.durations.large +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ duration: Tokens.anim.durations.large ++ easing: Tokens.anim.standardAccel + } + Anim { + from: 0 + to: 1 +- duration: Appearance.anim.durations.large +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ duration: Tokens.anim.durations.large ++ easing: Tokens.anim.standardDecel + } + } + } +@@ -264,6 +264,7 @@ StyledRect { + component WrappedLoader: Loader { + required property string name + ++ asynchronous: true + Layout.alignment: Qt.AlignHCenter + visible: active + } +diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml +index 96956f6f..85a920c1 100644 +--- a/modules/bar/components/Tray.qml ++++ b/modules/bar/components/Tray.qml +@@ -1,10 +1,11 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Quickshell.Services.SystemTray ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell.Services.SystemTray +-import QtQuick + + StyledRect { + id: root +@@ -13,8 +14,8 @@ StyledRect { + readonly property alias items: items + readonly property alias expandIcon: expandIcon + +- readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small +- readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0 ++ readonly property int padding: Config.bar.tray.background ? Tokens.padding.normal : Tokens.padding.small ++ readonly property int spacing: Config.bar.tray.background ? Tokens.spacing.small : 0 + + property bool expanded + +@@ -27,11 +28,11 @@ StyledRect { + clip: true + visible: height > 0 + +- implicitWidth: Config.bar.sizes.innerWidth ++ implicitWidth: Tokens.sizes.bar.innerWidth + implicitHeight: nonAnimHeight + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + Column { + id: layout +@@ -39,7 +40,7 @@ StyledRect { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: root.padding +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0 + +@@ -48,7 +49,7 @@ StyledRect { + properties: "scale" + from: 0 + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + } + +@@ -56,7 +57,7 @@ StyledRect { + Anim { + properties: "scale" + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + Anim { + properties: "x,y" +@@ -66,7 +67,9 @@ StyledRect { + Repeater { + id: items + +- model: SystemTray.items ++ model: ScriptModel { ++ values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) ++ } + + TrayItem {} + } +@@ -79,23 +82,25 @@ StyledRect { + Loader { + id: expandIcon + ++ asynchronous: true ++ + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + +- active: Config.bar.tray.compact ++ active: Config.bar.tray.compact && items.count > 0 + + sourceComponent: Item { + implicitWidth: expandIconInner.implicitWidth +- implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2 ++ implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small * 2 + + MaterialIcon { + id: expandIconInner + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom +- anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small ++ anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.small : -Tokens.padding.small + text: "expand_less" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + rotation: root.expanded ? 180 : 0 + + Behavior on rotation { +@@ -111,8 +116,7 @@ StyledRect { + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml +index 99119073..fefb532c 100644 +--- a/modules/bar/components/TrayItem.qml ++++ b/modules/bar/components/TrayItem.qml +@@ -1,11 +1,11 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell.Services.SystemTray ++import Caelestia.Config + import qs.components.effects + import qs.services +-import qs.config + import qs.utils +-import Quickshell.Services.SystemTray +-import QtQuick + + MouseArea { + id: root +@@ -13,8 +13,8 @@ MouseArea { + required property SystemTrayItem modelData + + acceptedButtons: Qt.LeftButton | Qt.RightButton +- implicitWidth: Appearance.font.size.small * 2 +- implicitHeight: Appearance.font.size.small * 2 ++ implicitWidth: Tokens.font.size.small * 2 ++ implicitHeight: Tokens.font.size.small * 2 + + onClicked: event => { + if (event.button === Qt.LeftButton) +diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml +index dae54b37..ebc0caf2 100644 +--- a/modules/bar/components/workspaces/ActiveIndicator.qml ++++ b/modules/bar/components/workspaces/ActiveIndicator.qml +@@ -1,8 +1,10 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick + + StyledRect { + id: root +@@ -10,6 +12,7 @@ StyledRect { + required property int activeWsId + required property Repeater workspaces + required property Item mask ++ required property bool fullscreen + + readonly property int currentWsIdx: { + let i = activeWsId - 1; +@@ -20,13 +23,12 @@ StyledRect { + + property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 + property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 +- property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0 ++ property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0 + property real offset: Math.min(leading, trailing) + property real size: { + const s = Math.abs(leading - trailing) + currentSize; + if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) { +- const ws = workspaces.itemAt(lastWs); +- // console.log(ws, lastWs); ++ const ws = workspaces.itemAt(lastWs) as Workspace; + return ws ? Math.min(ws.y + ws.size - offset, s) : 0; + } + return s; +@@ -42,9 +44,9 @@ StyledRect { + + clip: true + y: offset + mask.y +- implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 ++ implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + implicitHeight: size +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3primary + + Colouriser { +@@ -61,38 +63,38 @@ StyledRect { + } + + Behavior on leading { +- enabled: Config.bar.workspaces.activeTrail ++ enabled: root.Config.bar.workspaces.activeTrail + + EAnim {} + } + + Behavior on trailing { +- enabled: Config.bar.workspaces.activeTrail ++ enabled: root.Config.bar.workspaces.activeTrail + + EAnim { +- duration: Appearance.anim.durations.normal * 2 ++ duration: Tokens.anim.durations.normal * 2 + } + } + + Behavior on currentSize { +- enabled: Config.bar.workspaces.activeTrail ++ enabled: root.Config.bar.workspaces.activeTrail + + EAnim {} + } + + Behavior on offset { +- enabled: !Config.bar.workspaces.activeTrail ++ enabled: !root.Config.bar.workspaces.activeTrail + + EAnim {} + } + + Behavior on size { +- enabled: !Config.bar.workspaces.activeTrail ++ enabled: !root.Config.bar.workspaces.activeTrail + + EAnim {} + } + + component EAnim: Anim { +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } +diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml +index 56b215e6..2bd3c8cb 100644 +--- a/modules/bar/components/workspaces/OccupiedBg.qml ++++ b/modules/bar/components/workspaces/OccupiedBg.qml +@@ -1,10 +1,10 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell +-import QtQuick + + Item { + id: root +@@ -52,8 +52,8 @@ Item { + + required property var modelData + +- readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null +- readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null ++ readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null // qmllint disable incompatible-type ++ readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null // qmllint disable incompatible-type + + function getWsIdx(ws: int): int { + let i = ws - 1; +@@ -65,18 +65,18 @@ Item { + anchors.horizontalCenter: root.horizontalCenter + + y: (start?.y ?? 0) - 1 +- implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2 ++ implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + 2 + implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 + + color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + scale: 0 + Component.onCompleted: scale = 1 + + Behavior on scale { + Anim { +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + } + +@@ -90,14 +90,14 @@ Item { + } + } + +- component Pill: QtObject { +- property int start +- property int end +- } +- + Component { + id: pillComp + + Pill {} + } ++ ++ component Pill: QtObject { ++ property int start ++ property int end ++ } + } +diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml +index ad85af89..3ed382e9 100644 +--- a/modules/bar/components/workspaces/SpecialWorkspaces.qml ++++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml +@@ -1,21 +1,21 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Hyprland ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services + import qs.utils +-import qs.config +-import Quickshell +-import Quickshell.Hyprland +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root + + required property ShellScreen screen + readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) +- readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? "" ++ readonly property string activeSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" + + layer.enabled: true + layer.effect: OpacityMask { +@@ -31,7 +31,7 @@ Item { + + Rectangle { + anchors.fill: parent +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + gradient: Gradient { + orientation: Gradient.Vertical +@@ -60,7 +60,7 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + implicitHeight: parent.height / 2 + opacity: view.contentY > 0 ? 0 : 1 + +@@ -74,9 +74,9 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + implicitHeight: parent.height / 2 +- opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1 ++ opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.small ? 0 : 1 + + Behavior on opacity { + Anim {} +@@ -88,14 +88,14 @@ Item { + id: view + + anchors.fill: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + interactive: false + + currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) + onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial)) + + model: ScriptModel { +- values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) ++ values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!GlobalConfig.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) + } + + preferredHighlightBegin: 0 +@@ -105,150 +105,21 @@ Item { + highlightFollowsCurrentItem: false + highlight: Item { + y: view.currentItem?.y ?? 0 +- implicitHeight: view.currentItem?.size ?? 0 ++ implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 + + Behavior on y { + Anim {} + } + } + +- delegate: ColumnLayout { +- id: ws +- +- required property HyprlandWorkspace modelData +- readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) +- property int wsId +- property string icon +- property bool hasWindows +- +- anchors.left: view.contentItem.left +- anchors.right: view.contentItem.right +- +- spacing: 0 +- +- Component.onCompleted: { +- wsId = modelData.id; +- icon = Icons.getSpecialWsIcon(modelData.name); +- hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; +- } +- +- // Hacky thing cause modelData gets destroyed before the remove anim finishes +- Connections { +- target: ws.modelData +- +- function onIdChanged(): void { +- if (ws.modelData) +- ws.wsId = ws.modelData.id; +- } +- +- function onNameChanged(): void { +- if (ws.modelData) +- ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); +- } +- +- function onLastIpcObjectChanged(): void { +- if (ws.modelData) +- ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; +- } +- } +- +- Connections { +- target: Config.bar.workspaces +- +- function onShowWindowsOnSpecialWorkspacesChanged(): void { +- if (ws.modelData) +- ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; +- } +- } +- +- Loader { +- id: label +- +- Layout.alignment: Qt.AlignHCenter | Qt.AlignTop +- Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 +- +- sourceComponent: ws.icon.length === 1 ? letterComp : iconComp +- +- Component { +- id: iconComp +- +- MaterialIcon { +- fill: 1 +- text: ws.icon +- verticalAlignment: Qt.AlignVCenter +- } +- } +- +- Component { +- id: letterComp +- +- StyledText { +- text: ws.icon +- verticalAlignment: Qt.AlignVCenter +- } +- } +- } +- +- Loader { +- id: windows +- +- Layout.alignment: Qt.AlignHCenter +- Layout.fillHeight: true +- Layout.preferredHeight: implicitHeight +- +- visible: active +- active: ws.hasWindows +- +- sourceComponent: Column { +- spacing: 0 +- +- add: Transition { +- Anim { +- properties: "scale" +- from: 0 +- to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel +- } +- } +- +- move: Transition { +- Anim { +- properties: "scale" +- to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel +- } +- Anim { +- properties: "x,y" +- } +- } +- +- Repeater { +- model: ScriptModel { +- values: Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId) +- } +- +- MaterialIcon { +- required property var modelData +- +- grade: 0 +- text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") +- color: Colours.palette.m3onSurfaceVariant +- } +- } +- } +- +- Behavior on Layout.preferredHeight { +- Anim {} +- } +- } +- } ++ delegate: SpecialWsDelegate {} + + add: Transition { + Anim { + properties: "scale" + from: 0 + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + } + +@@ -256,12 +127,12 @@ Item { + Anim { + property: "scale" + to: 0.5 +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + Anim { + property: "opacity" + to: 0 +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + +@@ -269,7 +140,7 @@ Item { + Anim { + properties: "scale" + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + Anim { + properties: "x,y" +@@ -280,7 +151,7 @@ Item { + Anim { + properties: "scale" + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + Anim { + properties: "x,y" +@@ -289,6 +160,7 @@ Item { + } + + Loader { ++ asynchronous: true + active: Config.bar.workspaces.activeIndicator + anchors.fill: parent + +@@ -300,10 +172,10 @@ Item { + anchors.right: parent.right + + y: (view.currentItem?.y ?? 0) - view.contentY +- implicitHeight: view.currentItem?.size ?? 0 ++ implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 + + color: Colours.palette.m3tertiary +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + Colouriser { + source: view +@@ -320,13 +192,13 @@ Item { + + Behavior on y { + Anim { +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + + Behavior on implicitHeight { + Anim { +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + } +@@ -341,7 +213,7 @@ Item { + drag.target: view.contentItem + drag.axis: Drag.YAxis + drag.maximumY: 0 +- drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) ++ drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.small) + + onPressed: event => startY = event.y + +@@ -349,11 +221,150 @@ Item { + if (Math.abs(event.y - startY) > drag.threshold) + return; + +- const ws = view.itemAt(event.x, event.y); ++ const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; + if (ws?.modelData) + Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); + else + Hypr.dispatch("togglespecialworkspace special"); + } + } ++ ++ component SpecialWsDelegate: ColumnLayout { ++ id: ws ++ ++ required property HyprlandWorkspace modelData ++ readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.small : 0) ++ property int wsId ++ property string icon ++ property bool hasWindows ++ ++ anchors.left: view.contentItem.left ++ anchors.right: view.contentItem.right ++ ++ spacing: 0 ++ ++ Component.onCompleted: { ++ wsId = modelData.id; ++ icon = Icons.getSpecialWsIcon(modelData.name); ++ hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; ++ } ++ ++ // Hacky thing cause modelData gets destroyed before the remove anim finishes ++ Connections { ++ function onIdChanged(): void { ++ if (ws.modelData) ++ ws.wsId = ws.modelData.id; ++ } ++ ++ function onNameChanged(): void { ++ if (ws.modelData) ++ ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); ++ } ++ ++ function onLastIpcObjectChanged(): void { ++ if (ws.modelData) ++ ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; ++ } ++ ++ target: ws.modelData ++ } ++ ++ Connections { ++ function onShowWindowsOnSpecialWorkspacesChanged(): void { ++ if (ws.modelData) ++ ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; ++ } ++ ++ target: root.Config.bar.workspaces ++ } ++ ++ Loader { ++ id: label ++ ++ asynchronous: true ++ ++ Layout.alignment: Qt.AlignHCenter | Qt.AlignTop ++ Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 ++ ++ sourceComponent: ws.icon.length === 1 ? letterComp : iconComp ++ ++ Component { ++ id: iconComp ++ ++ MaterialIcon { ++ fill: 1 ++ text: ws.icon ++ verticalAlignment: Qt.AlignVCenter ++ } ++ } ++ ++ Component { ++ id: letterComp ++ ++ StyledText { ++ text: ws.icon ++ verticalAlignment: Qt.AlignVCenter ++ } ++ } ++ } ++ ++ Loader { ++ id: windows ++ ++ asynchronous: true ++ ++ Layout.alignment: Qt.AlignHCenter ++ Layout.fillHeight: true ++ Layout.preferredHeight: implicitHeight ++ ++ visible: active ++ active: ws.hasWindows ++ ++ sourceComponent: Column { ++ spacing: 0 ++ ++ add: Transition { ++ Anim { ++ properties: "scale" ++ from: 0 ++ to: 1 ++ easing: Tokens.anim.standardDecel ++ } ++ } ++ ++ move: Transition { ++ Anim { ++ properties: "scale" ++ to: 1 ++ easing: Tokens.anim.standardDecel ++ } ++ Anim { ++ properties: "x,y" ++ } ++ } ++ ++ Repeater { ++ model: ScriptModel { ++ values: { ++ const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); ++ const maxIcons = root.Config.bar.workspaces.maxWindowIcons; ++ return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; ++ } ++ } ++ ++ MaterialIcon { ++ required property var modelData ++ ++ grade: 0 ++ text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ } ++ } ++ ++ Behavior on Layout.preferredHeight { ++ Anim {} ++ } ++ } ++ } + } +diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml +index 3c8238b5..bd581aab 100644 +--- a/modules/bar/components/workspaces/Workspace.qml ++++ b/modules/bar/components/workspaces/Workspace.qml +@@ -1,10 +1,12 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.services + import qs.utils +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root +@@ -16,7 +18,7 @@ ColumnLayout { + + readonly property bool isWorkspace: true // Flag for finding workspace children + // Unanimated prop for others to use as reference +- readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0) ++ readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.small : 0) + + readonly property int ws: groupOffset + index + 1 + readonly property bool isOccupied: occupied[ws] ?? false +@@ -31,7 +33,7 @@ ColumnLayout { + id: indicator + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop +- Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 ++ Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + + animate: true + text: { +@@ -55,9 +57,11 @@ ColumnLayout { + Loader { + id: windows + ++ asynchronous: true ++ + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true +- Layout.topMargin: -Config.bar.sizes.innerWidth / 10 ++ Layout.topMargin: -Tokens.sizes.bar.innerWidth / 10 + + visible: active + active: root.hasWindows +@@ -70,7 +74,7 @@ ColumnLayout { + properties: "scale" + from: 0 + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + } + +@@ -78,7 +82,7 @@ ColumnLayout { + Anim { + properties: "scale" + to: 1 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + Anim { + properties: "x,y" +@@ -87,7 +91,12 @@ ColumnLayout { + + Repeater { + model: ScriptModel { +- values: Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws) ++ values: { ++ const ws = root.ws; ++ const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); ++ const maxIcons = root.Config.bar.workspaces.maxWindowIcons; ++ return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; ++ } + } + + MaterialIcon { +diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml +index bfa80ab6..2030bf80 100644 +--- a/modules/bar/components/workspaces/Workspaces.qml ++++ b/modules/bar/components/workspaces/Workspaces.qml +@@ -1,39 +1,43 @@ + pragma ComponentBehavior: Bound + +-import qs.services +-import qs.config +-import qs.components +-import Quickshell + import QtQuick +-import QtQuick.Layouts + import QtQuick.Effects ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledClippingRect { + id: root + + required property ShellScreen screen ++ required property bool fullscreen + +- readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== "" +- readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId ++ readonly property bool onSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" ++ readonly property int activeWsId: GlobalConfig.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId + +- readonly property var occupied: Hypr.workspaces.values.reduce((acc, curr) => { +- acc[curr.id] = curr.lastIpcObject.windows > 0; +- return acc; +- }, {}) ++ readonly property var occupied: { ++ const occ = {}; ++ for (const ws of Hypr.workspaces.values) ++ occ[ws.id] = ws.lastIpcObject.windows > 0; ++ return occ; ++ } + readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown + + property real blur: onSpecial ? 1 : 0 + +- implicitWidth: Config.bar.sizes.innerWidth +- implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: Tokens.sizes.bar.innerWidth ++ implicitHeight: layout.implicitHeight + Tokens.padding.small * 2 + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + Item { + anchors.fill: parent + scale: root.onSpecial ? 0.8 : 1 + opacity: root.onSpecial ? 0.5 : 1 ++ visible: !root.fullscreen + + layer.enabled: root.blur > 0 + layer.effect: MultiEffect { +@@ -43,10 +47,11 @@ StyledClippingRect { + } + + Loader { ++ asynchronous: true + active: Config.bar.workspaces.occupiedBg + + anchors.fill: parent +- anchors.margins: Appearance.padding.small ++ anchors.margins: Tokens.padding.small + + sourceComponent: OccupiedBg { + workspaces: workspaces +@@ -59,7 +64,7 @@ StyledClippingRect { + id: layout + + anchors.centerIn: parent +- spacing: Math.floor(Appearance.spacing.small / 2) ++ spacing: Math.floor(Tokens.spacing.small / 2) + + Repeater { + id: workspaces +@@ -75,6 +80,7 @@ StyledClippingRect { + } + + Loader { ++ asynchronous: true + anchors.horizontalCenter: parent.horizontalCenter + active: Config.bar.workspaces.activeIndicator + +@@ -82,13 +88,14 @@ StyledClippingRect { + activeWsId: root.activeWsId + workspaces: workspaces + mask: layout ++ fullscreen: root.fullscreen + } + } + + MouseArea { + anchors.fill: layout + onClicked: event => { +- const ws = layout.childAt(event.x, event.y).ws; ++ const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws; + if (Hypr.activeWsId !== ws) + Hypr.dispatch(`workspace ${ws}`); + else +@@ -108,8 +115,10 @@ StyledClippingRect { + Loader { + id: specialWs + ++ asynchronous: true ++ + anchors.fill: parent +- anchors.margins: Appearance.padding.small ++ anchors.margins: Tokens.padding.small + + active: opacity > 0 + +@@ -131,7 +140,7 @@ StyledClippingRect { + + Behavior on blur { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml +index adf7b774..12246645 100644 +--- a/modules/bar/popouts/ActiveWindow.qml ++++ b/modules/bar/popouts/ActiveWindow.qml +@@ -1,36 +1,37 @@ ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Wayland ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components + import qs.services + import qs.utils +-import qs.config +-import Quickshell.Widgets +-import Quickshell.Wayland +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root + +- required property Item wrapper ++ required property PopoutState popouts + +- implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 ++ implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.large * 2 + implicitHeight: child.implicitHeight + + Column { + id: child + + anchors.centerIn: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + RowLayout { + id: detailsRow + + anchors.left: parent.left + anchors.right: parent.right +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + IconImage { + id: icon + ++ asynchronous: true + Layout.alignment: Qt.AlignVCenter + implicitSize: details.implicitHeight + source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing") +@@ -45,7 +46,7 @@ Item { + StyledText { + Layout.fillWidth: true + text: Hypr.activeToplevel?.title ?? "" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + elide: Text.ElideRight + } + +@@ -58,17 +59,14 @@ Item { + } + + Item { +- implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2 +- implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: expandIcon.implicitHeight + Tokens.padding.small * 2 ++ implicitHeight: expandIcon.implicitHeight + Tokens.padding.small * 2 + + Layout.alignment: Qt.AlignVCenter + + StateLayer { +- radius: Appearance.rounding.normal +- +- function onClicked(): void { +- root.wrapper.detach("winfo"); +- } ++ radius: Tokens.rounding.normal ++ onClicked: root.popouts.detachRequested("winfo") + } + + MaterialIcon { +@@ -79,23 +77,23 @@ Item { + + text: "chevron_right" + +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } + + ClippingWrapperRectangle { + color: "transparent" +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + + ScreencopyView { + id: preview + +- captureSource: Hypr.activeToplevel?.wayland ?? null ++ captureSource: Hypr.activeToplevel?.wayland ?? null // qmllint disable unresolved-type + live: visible + +- constraintSize.width: Config.bar.sizes.windowPreviewSize +- constraintSize.height: Config.bar.sizes.windowPreviewSize ++ constraintSize.width: Tokens.sizes.bar.windowPreviewSize ++ constraintSize.height: Tokens.sizes.bar.windowPreviewSize + } + } + } +diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml +index 58b29ba8..e8dc9203 100644 +--- a/modules/bar/popouts/Audio.qml ++++ b/modules/bar/popouts/Audio.qml +@@ -1,23 +1,21 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Controls ++import QtQuick.Layouts ++import Quickshell.Services.Pipewire ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Services.Pipewire +-import QtQuick +-import QtQuick.Layouts +-import QtQuick.Controls +-import "../../controlcenter/network" + + Item { + id: root + +- required property var wrapper ++ required property PopoutState popouts + +- implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 ++ implicitWidth: layout.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: layout.implicitHeight + Tokens.padding.normal * 2 + + ButtonGroup { + id: sinks +@@ -32,7 +30,7 @@ Item { + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: qsTr("Output device") +@@ -55,7 +53,7 @@ Item { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.smaller ++ Layout.topMargin: Tokens.spacing.smaller + text: qsTr("Input device") + font.weight: 500 + } +@@ -74,15 +72,15 @@ Item { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.smaller +- Layout.bottomMargin: -Appearance.spacing.small / 2 ++ Layout.topMargin: Tokens.spacing.smaller ++ Layout.bottomMargin: -Tokens.spacing.small / 2 + text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) + font.weight: 500 + } + + CustomMouseArea { + Layout.fillWidth: true +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + + onWheel: event => { + if (event.angleDelta.y > 0) +@@ -107,14 +105,14 @@ Item { + + IconTextButton { + Layout.fillWidth: true +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer +- verticalPadding: Appearance.padding.small ++ verticalPadding: Tokens.padding.small + text: qsTr("Open settings") + icon: "settings" + +- onClicked: root.wrapper.detach("audio") ++ onClicked: root.popouts.detachRequested("audio") + } + } + } +diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml +deleted file mode 100644 +index 075b6988..00000000 +--- a/modules/bar/popouts/Background.qml ++++ /dev/null +@@ -1,73 +0,0 @@ +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Shapes +- +-ShapePath { +- id: root +- +- required property Wrapper wrapper +- required property bool invertBottomRounding +- readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding +- readonly property bool flatten: wrapper.width < rounding * 2 +- readonly property real roundingX: flatten ? wrapper.width / 2 : rounding +- property real ibr: invertBottomRounding ? -1 : 1 +- +- property real sideRounding: startX > 0 ? -1 : 1 +- +- strokeWidth: -1 +- fillColor: Colours.palette.m3surface +- +- PathArc { +- relativeX: root.roundingX +- relativeY: root.rounding * root.sideRounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise +- } +- PathLine { +- relativeX: root.wrapper.width - root.roundingX * 2 +- relativeY: 0 +- } +- PathArc { +- relativeX: root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- } +- PathLine { +- relativeX: 0 +- relativeY: root.wrapper.height - root.rounding * 2 +- } +- PathArc { +- relativeX: -root.roundingX * root.ibr +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise +- } +- PathLine { +- relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr) +- relativeY: 0 +- } +- PathArc { +- relativeX: -root.roundingX +- relativeY: root.rounding * root.sideRounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise +- } +- +- Behavior on fillColor { +- CAnim {} +- } +- +- Behavior on ibr { +- Anim {} +- } +- +- Behavior on sideRounding { +- Anim {} +- } +-} +diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml +index ac975e1b..93d2012b 100644 +--- a/modules/bar/popouts/Battery.qml ++++ b/modules/bar/popouts/Battery.qml +@@ -1,16 +1,16 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell.Services.UPower ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell.Services.UPower +-import QtQuick + + Column { + id: root + +- spacing: Appearance.spacing.normal +- width: Config.bar.sizes.batteryWidth ++ spacing: Tokens.spacing.normal ++ width: Tokens.sizes.bar.batteryWidth + + StyledText { + text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected") +@@ -37,18 +37,19 @@ Column { + } + + Loader { ++ asynchronous: true + anchors.horizontalCenter: parent.horizontalCenter + + active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None + +- height: active ? (item?.implicitHeight ?? 0) : 0 ++ height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 + + sourceComponent: StyledRect { +- implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2 ++ implicitWidth: child.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: child.implicitHeight + Tokens.padding.smaller * 2 + + color: Colours.palette.m3error +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + Column { + id: child +@@ -57,7 +58,7 @@ Column { + + Row { + anchors.horizontalCenter: parent.horizontalCenter +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + MaterialIcon { + anchors.verticalCenter: parent.verticalCenter +@@ -71,7 +72,7 @@ Column { + anchors.verticalCenter: parent.verticalCenter + text: qsTr("Performance Degraded") + color: Colours.palette.m3onError +- font.family: Appearance.font.family.mono ++ font.family: Tokens.font.family.mono + font.weight: 500 + } + +@@ -108,17 +109,17 @@ Column { + + anchors.horizontalCenter: parent.horizontalCenter + +- implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2 +- implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2 ++ implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.normal * 2 + Tokens.spacing.large * 2 ++ implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small * 2 + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + StyledRect { + id: indicator + + color: Colours.palette.m3primary +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + state: profiles.current + + states: [ +@@ -146,10 +147,8 @@ Column { + ] + + transitions: Transition { +- AnchorAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ AnchorAnim { ++ type: AnchorAnim.Emphasized + } + } + } +@@ -159,7 +158,7 @@ Column { + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left +- anchors.leftMargin: Appearance.padding.small ++ anchors.leftMargin: Tokens.padding.small + + profile: PowerProfile.PowerSaver + icon: "energy_savings_leaf" +@@ -179,7 +178,7 @@ Column { + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right +- anchors.rightMargin: Appearance.padding.small ++ anchors.rightMargin: Tokens.padding.small + + profile: PowerProfile.Performance + icon: "rocket_launch" +@@ -200,16 +199,13 @@ Column { + required property string icon + required property int profile + +- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 +- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 ++ implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + + StateLayer { +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface +- +- function onClicked(): void { +- PowerProfiles.profile = parent.profile; +- } ++ onClicked: PowerProfiles.profile = parent.profile + } + + MaterialIcon { +@@ -218,7 +214,7 @@ Column { + anchors.centerIn: parent + + text: parent.icon +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + fill: profiles.current === text ? 1 : 0 + +diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml +index 676da82f..baca115e 100644 +--- a/modules/bar/popouts/Bluetooth.qml ++++ b/modules/bar/popouts/Bluetooth.qml +@@ -1,35 +1,35 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Bluetooth ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import Quickshell.Bluetooth +-import QtQuick +-import QtQuick.Layouts +-import "../../controlcenter/network" + + ColumnLayout { + id: root + +- required property Item wrapper ++ required property PopoutState popouts + +- spacing: Appearance.spacing.small ++ width: 300 ++ spacing: Tokens.spacing.small + + StyledText { +- Layout.topMargin: Appearance.padding.normal +- Layout.rightMargin: Appearance.padding.small ++ Layout.topMargin: Tokens.padding.normal ++ Layout.rightMargin: Tokens.padding.small + text: qsTr("Bluetooth") + font.weight: 500 + } + + Toggle { + label: qsTr("Enabled") +- checked: Bluetooth.defaultAdapter?.enabled ?? false ++ checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type + toggle.onToggled: { +- const adapter = Bluetooth.defaultAdapter; ++ const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + if (adapter) + adapter.enabled = checked; + } +@@ -37,19 +37,19 @@ ColumnLayout { + + Toggle { + label: qsTr("Discovering") +- checked: Bluetooth.defaultAdapter?.discovering ?? false ++ checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type + toggle.onToggled: { +- const adapter = Bluetooth.defaultAdapter; ++ const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + if (adapter) + adapter.discovering = checked; + } + } + + StyledText { +- Layout.topMargin: Appearance.spacing.small +- Layout.rightMargin: Appearance.padding.small ++ Layout.topMargin: Tokens.spacing.small ++ Layout.rightMargin: Tokens.padding.small + text: { +- const devices = Bluetooth.devices.values; ++ const devices = Bluetooth.devices.values; // qmllint disable unresolved-type + let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); + const connected = devices.filter(d => d.connected).length; + if (connected > 0) +@@ -57,23 +57,23 @@ ColumnLayout { + return available; + } + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + Repeater { + model: ScriptModel { +- values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) ++ values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type + } + + RowLayout { + id: device + + required property BluetoothDevice modelData +- readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting ++ readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type + + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- spacing: Appearance.spacing.small ++ Layout.rightMargin: Tokens.padding.small ++ spacing: Tokens.spacing.small + + opacity: 0 + scale: 0.7 +@@ -96,20 +96,27 @@ ColumnLayout { + } + + StyledText { +- Layout.leftMargin: Appearance.spacing.small / 2 +- Layout.rightMargin: Appearance.spacing.small / 2 ++ Layout.leftMargin: Tokens.spacing.small / 2 ++ Layout.rightMargin: Tokens.spacing.small / 2 + Layout.fillWidth: true + text: device.modelData.name ++ elide: Text.ElideRight ++ } ++ ++ MaterialIcon { ++ visible: device.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type ++ text: Icons.getBatteryIcon(device.modelData.batteryAvailable ? device.modelData.battery * 100 : -1) ++ color: device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant + } + + StyledRect { + id: connectBtn + + implicitWidth: implicitHeight +- implicitHeight: connectIcon.implicitHeight + Appearance.padding.small ++ implicitHeight: connectIcon.implicitHeight + Tokens.padding.small + +- radius: Appearance.rounding.full +- color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) ++ radius: Tokens.rounding.full ++ color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type + + CircularIndicator { + anchors.fill: parent +@@ -117,12 +124,9 @@ ColumnLayout { + } + + StateLayer { +- color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface ++ color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type + disabled: device.loading +- +- function onClicked(): void { +- device.modelData.connected = !device.modelData.connected; +- } ++ onClicked: device.modelData.connected = !device.modelData.connected + } + + MaterialIcon { +@@ -131,7 +135,7 @@ ColumnLayout { + anchors.centerIn: parent + animate: true + text: device.modelData.connected ? "link_off" : "link" +- color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface ++ color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type + + opacity: device.loading ? 0 : 1 + +@@ -142,17 +146,16 @@ ColumnLayout { + } + + Loader { ++ visible: status === Loader.Ready ++ asynchronous: true + active: device.modelData.bonded + sourceComponent: Item { + implicitWidth: connectBtn.implicitWidth + implicitHeight: connectBtn.implicitHeight + + StateLayer { +- radius: Appearance.rounding.full +- +- function onClicked(): void { +- device.modelData.forget(); +- } ++ radius: Tokens.rounding.full ++ onClicked: device.modelData.forget() + } + + MaterialIcon { +@@ -166,14 +169,14 @@ ColumnLayout { + + IconTextButton { + Layout.fillWidth: true +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer +- verticalPadding: Appearance.padding.small ++ verticalPadding: Tokens.padding.small + text: qsTr("Open settings") + icon: "settings" + +- onClicked: root.wrapper.detach("bluetooth") ++ onClicked: root.popouts.detachRequested("bluetooth") + } + + component Toggle: RowLayout { +@@ -182,8 +185,8 @@ ColumnLayout { + property alias toggle: toggle + + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- spacing: Appearance.spacing.normal ++ Layout.rightMargin: Tokens.padding.small ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml +new file mode 100644 +index 00000000..ab80d2cc +--- /dev/null ++++ b/modules/bar/popouts/ClipWrapper.qml +@@ -0,0 +1,67 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import Quickshell ++import qs.components ++import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others ++ ++Item { ++ id: root ++ ++ required property ShellScreen screen ++ required property real borderThickness ++ ++ readonly property alias content: content ++ property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 ++ ++ visible: width > 0 && height > 0 ++ clip: true ++ ++ implicitWidth: content.implicitWidth * (1 - offsetScale) ++ implicitHeight: content.implicitHeight ++ ++ x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0 ++ y: { ++ if (content.isDetached) ++ return (parent.height - content.nonAnimHeight) / 2; ++ ++ const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2; ++ const diff = parent.height - Math.floor(off + content.nonAnimHeight); ++ if (diff < 0) ++ return off + diff; ++ return Math.max(off, 0); ++ } ++ ++ Behavior on offsetScale { ++ Anim { ++ type: Anim.DefaultSpatial ++ } ++ } ++ ++ Behavior on x { ++ Anim { ++ duration: content.animLength ++ easing: content.animCurve ++ } ++ } ++ ++ Behavior on y { ++ enabled: root.offsetScale < 1 ++ ++ Anim { ++ duration: content.animLength ++ easing: content.animCurve ++ } ++ } ++ ++ Wrapper { ++ id: content ++ ++ screen: root.screen ++ offsetScale: root.offsetScale ++ ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.left: parent.left ++ anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale ++ } ++} +diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml +index 40768444..42c5a766 100644 +--- a/modules/bar/popouts/Content.qml ++++ b/modules/bar/popouts/Content.qml +@@ -1,43 +1,41 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config ++import "./kblayout" ++import QtQuick + import Quickshell + import Quickshell.Services.SystemTray +-import QtQuick +- +-import "./kblayout" ++import Caelestia.Config ++import qs.components + + Item { + id: root + +- required property Item wrapper ++ required property PopoutState popouts + readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null + readonly property Item current: currentPopout?.item ?? null + +- anchors.centerIn: parent +- +- implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2 +- implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2 ++ implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.large * 2 ++ implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.large * 2 + + Item { + id: content + + anchors.fill: parent +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + + Popout { + name: "activewindow" + sourceComponent: ActiveWindow { +- wrapper: root.wrapper ++ popouts: root.popouts + } + } + + Popout { + id: networkPopout ++ + name: "network" + sourceComponent: Network { +- wrapper: root.wrapper ++ popouts: root.popouts + view: "wireless" + } + } +@@ -45,60 +43,64 @@ Item { + Popout { + name: "ethernet" + sourceComponent: Network { +- wrapper: root.wrapper ++ popouts: root.popouts + view: "ethernet" + } + } + + Popout { + id: passwordPopout ++ + name: "wirelesspassword" + sourceComponent: WirelessPassword { + id: passwordComponent +- wrapper: root.wrapper +- network: networkPopout.item?.passwordNetwork ?? null ++ ++ popouts: root.popouts ++ network: (networkPopout.item as Network)?.passwordNetwork ?? null + } + + Connections { +- target: root.wrapper + function onCurrentNameChanged() { + // Update network immediately when password popout becomes active +- if (root.wrapper.currentName === "wirelesspassword") { ++ if (root.popouts.currentName === "wirelesspassword") { + // Set network immediately if available +- if (networkPopout.item && networkPopout.item.passwordNetwork) { ++ if ((networkPopout.item as Network)?.passwordNetwork) { + if (passwordPopout.item) { +- passwordPopout.item.network = networkPopout.item.passwordNetwork; ++ (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + } + } + // Also try after a short delay in case networkPopout.item wasn't ready + Qt.callLater(() => { +- if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { +- passwordPopout.item.network = networkPopout.item.passwordNetwork; ++ if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) { ++ (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + } + }, 100); + } + } ++ ++ target: root.popouts + } + + Connections { +- target: networkPopout + function onItemChanged() { + // When network popout loads, update password popout if it's active +- if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { ++ if (root.popouts.currentName === "wirelesspassword" && passwordPopout.item) { + Qt.callLater(() => { +- if (networkPopout.item && networkPopout.item.passwordNetwork) { +- passwordPopout.item.network = networkPopout.item.passwordNetwork; ++ if ((networkPopout.item as Network)?.passwordNetwork) { ++ (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; + } + }); + } + } ++ ++ target: networkPopout + } + } + + Popout { + name: "bluetooth" + sourceComponent: Bluetooth { +- wrapper: root.wrapper ++ popouts: root.popouts + } + } + +@@ -110,15 +112,13 @@ Item { + Popout { + name: "audio" + sourceComponent: Audio { +- wrapper: root.wrapper ++ popouts: root.popouts + } + } + + Popout { + name: "kblayout" +- sourceComponent: KbLayout { +- wrapper: root.wrapper +- } ++ sourceComponent: KbLayout {} + } + + Popout { +@@ -128,7 +128,7 @@ Item { + + Repeater { + model: ScriptModel { +- values: [...SystemTray.items.values] ++ values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) + } + + Popout { +@@ -141,22 +141,22 @@ Item { + sourceComponent: trayMenuComp + + Connections { +- target: root.wrapper +- + function onHasCurrentChanged(): void { +- if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { ++ if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { + trayMenu.sourceComponent = null; + trayMenu.sourceComponent = trayMenuComp; + } + } ++ ++ target: root.popouts + } + + Component { + id: trayMenuComp + + TrayMenu { +- popouts: root.wrapper +- trayItem: trayMenu.modelData.menu ++ popouts: root.popouts ++ trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type + } + } + } +@@ -167,10 +167,9 @@ Item { + id: popout + + required property string name +- readonly property bool shouldBeActive: root.wrapper.currentName === name ++ readonly property bool shouldBeActive: root.popouts.currentName === name + +- anchors.verticalCenter: parent.verticalCenter +- anchors.right: parent.right ++ anchors.centerIn: parent + + opacity: 0 + scale: 0.8 +@@ -195,7 +194,7 @@ Item { + SequentialAnimation { + Anim { + properties: "opacity,scale" +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + PropertyAction { + target: popout +diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml +index 7d74530e..caab7487 100644 +--- a/modules/bar/popouts/LockStatus.qml ++++ b/modules/bar/popouts/LockStatus.qml +@@ -1,10 +1,10 @@ ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import QtQuick.Layouts + + ColumnLayout { +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Capslock: %1").arg(Hypr.capsLock ? "Enabled" : "Disabled") +diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml +index 5b32e4a6..63b69b57 100644 +--- a/modules/bar/popouts/Network.qml ++++ b/modules/bar/popouts/Network.qml +@@ -1,33 +1,33 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + +- required property Item wrapper ++ required property PopoutState popouts + + property string connectingToSsid: "" + property string view: "wireless" // "wireless" or "ethernet" + property var passwordNetwork: null + property bool showPasswordDialog: false + +- spacing: Appearance.spacing.small +- width: Config.bar.sizes.networkWidth ++ spacing: Tokens.spacing.small ++ width: Tokens.sizes.bar.networkWidth + + // Wireless section + StyledText { + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 +- Layout.topMargin: visible ? Appearance.padding.normal : 0 +- Layout.rightMargin: Appearance.padding.small ++ Layout.topMargin: visible ? Tokens.padding.normal : 0 ++ Layout.rightMargin: Tokens.padding.small + text: qsTr("Wireless") + font.weight: 500 + } +@@ -43,11 +43,11 @@ ColumnLayout { + StyledText { + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 +- Layout.topMargin: visible ? Appearance.spacing.small : 0 +- Layout.rightMargin: Appearance.padding.small +- text: qsTr("%1 networks available").arg(Nmcli.networks.length) ++ Layout.topMargin: visible ? Tokens.spacing.small : 0 ++ Layout.rightMargin: Tokens.padding.small ++ text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + Repeater { +@@ -70,8 +70,8 @@ ColumnLayout { + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- spacing: Appearance.spacing.small ++ Layout.rightMargin: Tokens.padding.small ++ spacing: Tokens.spacing.small + + opacity: 0 + scale: 0.7 +@@ -97,12 +97,12 @@ ColumnLayout { + MaterialIcon { + visible: networkItem.modelData.isSecure + text: "lock" +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.leftMargin: Appearance.spacing.small / 2 +- Layout.rightMargin: Appearance.spacing.small / 2 ++ Layout.leftMargin: Tokens.spacing.small / 2 ++ Layout.rightMargin: Tokens.spacing.small / 2 + Layout.fillWidth: true + text: networkItem.modelData.ssid + elide: Text.ElideRight +@@ -112,9 +112,9 @@ ColumnLayout { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small ++ implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.small + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) + + CircularIndicator { +@@ -126,7 +126,7 @@ ColumnLayout { + color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: networkItem.loading || !Nmcli.wifiEnabled + +- function onClicked(): void { ++ onClicked: { + if (networkItem.modelData.active) { + Nmcli.disconnectFromNetwork(); + } else { +@@ -135,7 +135,7 @@ ColumnLayout { + // Password is required - show password dialog + root.passwordNetwork = network; + root.showPasswordDialog = true; +- root.wrapper.currentName = "wirelesspassword"; ++ root.popouts.currentName = "wirelesspassword"; + }); + + // Clear connecting state if connection succeeds immediately (saved profile) +@@ -165,27 +165,24 @@ ColumnLayout { + StyledRect { + visible: root.view === "wireless" + Layout.preferredHeight: visible ? implicitHeight : 0 +- Layout.topMargin: visible ? Appearance.spacing.small : 0 ++ Layout.topMargin: visible ? Tokens.spacing.small : 0 + Layout.fillWidth: true +- implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3primaryContainer + + StateLayer { + color: Colours.palette.m3onPrimaryContainer + disabled: Nmcli.scanning || !Nmcli.wifiEnabled +- +- function onClicked(): void { +- Nmcli.rescanWifi(); +- } ++ onClicked: Nmcli.rescanWifi() + } + + RowLayout { + id: rescanBtn + + anchors.centerIn: parent +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + opacity: Nmcli.scanning ? 0 : 1 + + MaterialIcon { +@@ -210,9 +207,9 @@ ColumnLayout { + + CircularIndicator { + anchors.centerIn: parent +- strokeWidth: Appearance.padding.small / 2 ++ strokeWidth: Tokens.padding.small / 2 + bgColour: "transparent" +- implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2 ++ implicitSize: parent.implicitHeight - Tokens.padding.smaller * 2 + running: Nmcli.scanning + } + } +@@ -221,8 +218,8 @@ ColumnLayout { + StyledText { + visible: root.view === "ethernet" + Layout.preferredHeight: visible ? implicitHeight : 0 +- Layout.topMargin: visible ? Appearance.padding.normal : 0 +- Layout.rightMargin: Appearance.padding.small ++ Layout.topMargin: visible ? Tokens.padding.normal : 0 ++ Layout.rightMargin: Tokens.padding.small + text: qsTr("Ethernet") + font.weight: 500 + } +@@ -230,11 +227,11 @@ ColumnLayout { + StyledText { + visible: root.view === "ethernet" + Layout.preferredHeight: visible ? implicitHeight : 0 +- Layout.topMargin: visible ? Appearance.spacing.small : 0 +- Layout.rightMargin: Appearance.padding.small ++ Layout.topMargin: visible ? Tokens.spacing.small : 0 ++ Layout.rightMargin: Tokens.padding.small + text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + Repeater { +@@ -256,8 +253,8 @@ ColumnLayout { + visible: root.view === "ethernet" + Layout.preferredHeight: visible ? implicitHeight : 0 + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- spacing: Appearance.spacing.small ++ Layout.rightMargin: Tokens.padding.small ++ spacing: Tokens.spacing.small + + opacity: 0 + scale: 0.7 +@@ -281,8 +278,8 @@ ColumnLayout { + } + + StyledText { +- Layout.leftMargin: Appearance.spacing.small / 2 +- Layout.rightMargin: Appearance.spacing.small / 2 ++ Layout.leftMargin: Tokens.spacing.small / 2 ++ Layout.rightMargin: Tokens.spacing.small / 2 + Layout.fillWidth: true + text: ethernetItem.modelData.interface || qsTr("Unknown") + elide: Text.ElideRight +@@ -292,9 +289,9 @@ ColumnLayout { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: connectIcon.implicitHeight + Appearance.padding.small ++ implicitHeight: connectIcon.implicitHeight + Tokens.padding.small + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) + + CircularIndicator { +@@ -306,7 +303,7 @@ ColumnLayout { + color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + disabled: ethernetItem.loading + +- function onClicked(): void { ++ onClicked: { + if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { + Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); + } else { +@@ -334,8 +331,6 @@ ColumnLayout { + } + + Connections { +- target: Nmcli +- + function onActiveChanged(): void { + if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { + root.connectingToSsid = ""; +@@ -343,8 +338,8 @@ ColumnLayout { + if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { + root.showPasswordDialog = false; + root.passwordNetwork = null; +- if (root.wrapper.currentName === "wirelesspassword") { +- root.wrapper.currentName = "network"; ++ if (root.popouts.currentName === "wirelesspassword") { ++ root.popouts.currentName = "network"; + } + } + } +@@ -354,17 +349,20 @@ ColumnLayout { + if (!Nmcli.scanning) + scanIcon.rotation = 0; + } ++ ++ target: Nmcli + } + + Connections { +- target: root.wrapper + function onCurrentNameChanged(): void { + // Clear password network when leaving password dialog +- if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { ++ if (root.popouts.currentName !== "wirelesspassword" && root.showPasswordDialog) { + root.showPasswordDialog = false; + root.passwordNetwork = null; + } + } ++ ++ target: root.popouts + } + + component Toggle: RowLayout { +@@ -373,8 +371,8 @@ ColumnLayout { + property alias toggle: toggle + + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- spacing: Appearance.spacing.normal ++ Layout.rightMargin: Tokens.padding.small ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +diff --git a/modules/bar/popouts/PopoutState.qml b/modules/bar/popouts/PopoutState.qml +new file mode 100644 +index 00000000..6be8169b +--- /dev/null ++++ b/modules/bar/popouts/PopoutState.qml +@@ -0,0 +1,8 @@ ++import QtQuick ++ ++QtObject { ++ property string currentName ++ property bool hasCurrent ++ ++ signal detachRequested(mode: string) ++} +diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml +index 9b743db1..7975d1bf 100644 +--- a/modules/bar/popouts/TrayMenu.qml ++++ b/modules/bar/popouts/TrayMenu.qml +@@ -1,21 +1,21 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Widgets + import QtQuick + import QtQuick.Controls ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components ++import qs.services + + StackView { + id: root + +- required property Item popouts ++ required property PopoutState popouts + required property QsMenuHandle trayItem + +- implicitWidth: currentItem.implicitWidth +- implicitHeight: currentItem.implicitHeight ++ implicitWidth: currentItem?.implicitWidth ?? 0 ++ implicitHeight: currentItem?.implicitHeight ?? 0 + + initialItem: SubMenu { + handle: root.trayItem +@@ -26,6 +26,12 @@ StackView { + popEnter: NoAnim {} + popExit: NoAnim {} + ++ Component { ++ id: subMenuComp ++ ++ SubMenu {} ++ } ++ + component NoAnim: Transition { + NumberAnimation { + duration: 0 +@@ -39,8 +45,8 @@ StackView { + property bool isSubMenu + property bool shown + +- padding: Appearance.padding.smaller +- spacing: Appearance.spacing.small ++ padding: Tokens.padding.smaller ++ spacing: Tokens.spacing.small + + opacity: shown ? 1 : 0 + scale: shown ? 1 : 0.8 +@@ -72,15 +78,16 @@ StackView { + + required property QsMenuEntry modelData + +- implicitWidth: Config.bar.sizes.trayMenuWidth ++ implicitWidth: Tokens.sizes.bar.trayMenuWidth + implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent" + + Loader { + id: children + ++ asynchronous: true + anchors.left: parent.left + anchors.right: parent.right + +@@ -90,14 +97,14 @@ StackView { + implicitHeight: label.implicitHeight + + StateLayer { +- anchors.margins: -Appearance.padding.small / 2 +- anchors.leftMargin: -Appearance.padding.smaller +- anchors.rightMargin: -Appearance.padding.smaller ++ anchors.margins: -Tokens.padding.small / 2 ++ anchors.leftMargin: -Tokens.padding.smaller ++ anchors.rightMargin: -Tokens.padding.smaller + + radius: item.radius + disabled: !item.modelData.enabled + +- function onClicked(): void { ++ onClicked: { + const entry = item.modelData; + if (entry.hasChildren) + root.push(subMenuComp.createObject(null, { +@@ -114,11 +121,13 @@ StackView { + Loader { + id: icon + ++ asynchronous: true + anchors.left: parent.left + + active: item.modelData.icon !== "" + + sourceComponent: IconImage { ++ asynchronous: true + implicitSize: label.implicitHeight + + source: item.modelData.icon +@@ -129,7 +138,7 @@ StackView { + id: label + + anchors.left: icon.right +- anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0 ++ anchors.leftMargin: icon.active ? Tokens.spacing.smaller : 0 + + text: labelMetrics.elidedText + color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline +@@ -143,12 +152,13 @@ StackView { + font.family: label.font.family + + elide: Text.ElideRight +- elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0) ++ elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.normal : 0) + } + + Loader { + id: expand + ++ asynchronous: true + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + +@@ -165,11 +175,12 @@ StackView { + } + + Loader { ++ asynchronous: true + active: menu.isSubMenu + + sourceComponent: Item { + implicitWidth: back.implicitWidth +- implicitHeight: back.implicitHeight + Appearance.spacing.small / 2 ++ implicitHeight: back.implicitHeight + Tokens.spacing.small / 2 + + Item { + anchors.bottom: parent.bottom +@@ -178,20 +189,17 @@ StackView { + + StyledRect { + anchors.fill: parent +- anchors.margins: -Appearance.padding.small / 2 +- anchors.leftMargin: -Appearance.padding.smaller +- anchors.rightMargin: -Appearance.padding.smaller * 2 ++ anchors.margins: -Tokens.padding.small / 2 ++ anchors.leftMargin: -Tokens.padding.smaller ++ anchors.rightMargin: -Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3secondaryContainer + + StateLayer { + radius: parent.radius + color: Colours.palette.m3onSecondaryContainer +- +- function onClicked(): void { +- root.pop(); +- } ++ onClicked: root.pop() + } + } + +@@ -216,10 +224,4 @@ StackView { + } + } + } +- +- Component { +- id: subMenuComp +- +- SubMenu {} +- } + } +diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml +index 96639e71..f6a635fe 100644 +--- a/modules/bar/popouts/WirelessPassword.qml ++++ b/modules/bar/popouts/WirelessPassword.qml +@@ -1,27 +1,100 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + +- required property Item wrapper ++ required property PopoutState popouts + property var network: null + property bool isClosing: false + +- readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" ++ readonly property bool shouldBeVisible: root.popouts.currentName === "wirelesspassword" ++ ++ function checkConnectionStatus(): void { ++ if (!root.shouldBeVisible || !connectButton.connecting) { ++ return; ++ } ++ ++ // Check if we're connected to the target network (case-insensitive SSID comparison) ++ const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); ++ ++ if (isConnected) { ++ // Successfully connected - give it a moment for network list to update ++ // Use Timer for actual delay ++ connectionSuccessTimer.start(); ++ return; ++ } ++ ++ // Check for connection failures - if pending connection was cleared but we're not connected ++ if (Nmcli.pendingConnection === null && connectButton.connecting) { ++ // Wait a bit more before giving up (allow time for connection to establish) ++ if (connectionMonitor.repeatCount > 10) { ++ connectionMonitor.stop(); ++ connectButton.connecting = false; ++ connectButton.hasError = true; ++ connectButton.enabled = true; ++ connectButton.text = qsTr("Connect"); ++ passwordContainer.passwordBuffer = ""; ++ // Delete the failed connection ++ if (root.network && root.network.ssid) { ++ Nmcli.forgetNetwork(root.network.ssid); ++ } ++ } ++ } ++ } ++ ++ function closeDialog(): void { ++ if (isClosing) { ++ return; ++ } ++ ++ isClosing = true; ++ passwordContainer.passwordBuffer = ""; ++ connectButton.connecting = false; ++ connectButton.hasError = false; ++ connectButton.text = qsTr("Connect"); ++ connectionMonitor.stop(); ++ ++ // Return to network popout ++ if (root.popouts.currentName === "wirelesspassword") { ++ root.popouts.currentName = "network"; ++ } ++ } ++ ++ spacing: Tokens.spacing.normal ++ implicitWidth: 400 ++ implicitHeight: content.implicitHeight + Tokens.padding.large * 2 ++ visible: shouldBeVisible || isClosing ++ enabled: shouldBeVisible && !isClosing ++ focus: enabled ++ ++ Component.onCompleted: { ++ if (shouldBeVisible) { ++ // Use Timer for actual delay to ensure dialog is fully rendered ++ focusTimer.start(); ++ } ++ } ++ ++ onShouldBeVisibleChanged: { ++ if (shouldBeVisible) { ++ // Use Timer for actual delay to ensure dialog is fully rendered ++ focusTimer.start(); ++ } ++ } ++ ++ Keys.onEscapePressed: closeDialog() + + Connections { +- target: root.wrapper + function onCurrentNameChanged() { +- if (root.wrapper.currentName === "wirelesspassword") { ++ if (root.popouts.currentName === "wirelesspassword") { + // Update network when popout becomes active + Qt.callLater(() => { + // Try to get network from parent Content's networkPopout +@@ -38,10 +111,13 @@ ColumnLayout { + }); + } + } ++ ++ target: root.popouts + } + + Timer { + id: focusTimer ++ + interval: 150 + onTriggered: { + root.forceActiveFocus(); +@@ -49,41 +125,16 @@ ColumnLayout { + } + } + +- spacing: Appearance.spacing.normal +- +- implicitWidth: 400 +- implicitHeight: content.implicitHeight + Appearance.padding.large * 2 +- +- visible: shouldBeVisible || isClosing +- enabled: shouldBeVisible && !isClosing +- focus: enabled +- +- Component.onCompleted: { +- if (shouldBeVisible) { +- // Use Timer for actual delay to ensure dialog is fully rendered +- focusTimer.start(); +- } +- } +- +- onShouldBeVisibleChanged: { +- if (shouldBeVisible) { +- // Use Timer for actual delay to ensure dialog is fully rendered +- focusTimer.start(); +- } +- } +- +- Keys.onEscapePressed: closeDialog() +- + StyledRect { + Layout.fillWidth: true + Layout.preferredWidth: 400 +- implicitHeight: content.implicitHeight + Appearance.padding.large * 2 +- +- radius: Appearance.rounding.normal ++ implicitHeight: content.implicitHeight + Tokens.padding.large * 2 ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + visible: root.shouldBeVisible || root.isClosing + opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 + scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 ++ Keys.onEscapePressed: root.closeDialog() + + Behavior on opacity { + Anim {} +@@ -113,33 +164,32 @@ ColumnLayout { + } + } + +- Keys.onEscapePressed: root.closeDialog() +- + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" +- font.pointSize: Appearance.font.size.extraLarge * 2 ++ font.pointSize: Tokens.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + + StyledText { + id: networkNameText ++ + Layout.alignment: Qt.AlignHCenter + text: { + if (root.network) { +@@ -151,14 +201,15 @@ ColumnLayout { + return qsTr("Network: Unknown"); + } + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + Timer { ++ property int attempts: 0 ++ + interval: 50 + running: root.shouldBeVisible && (!root.network || !root.network.ssid) + repeat: true +- property int attempts: 0 + onTriggered: { + attempts++; + // Keep trying to get network from Network component +@@ -186,7 +237,7 @@ ColumnLayout { + id: statusText + + Layout.alignment: Qt.AlignHCenter +- Layout.topMargin: Appearance.spacing.small ++ Layout.topMargin: Tokens.spacing.small + visible: connectButton.connecting || connectButton.hasError + text: { + if (connectButton.hasError) { +@@ -198,23 +249,30 @@ ColumnLayout { + return ""; + } + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + font.weight: 400 + wrapMode: Text.WordWrap +- Layout.maximumWidth: parent.width - Appearance.padding.large * 2 ++ Layout.maximumWidth: parent.width - Tokens.padding.large * 2 + } + + FocusScope { + id: passwordContainer ++ ++ property string passwordBuffer: "" ++ + objectName: "passwordContainer" +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + Layout.fillWidth: true +- implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) +- ++ implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) + focus: true + activeFocusOnTab: true + +- property string passwordBuffer: "" ++ Component.onCompleted: { ++ if (root.shouldBeVisible) { ++ // Use Timer for actual delay to ensure focus works correctly ++ passwordFocusTimer.start(); ++ } ++ } + + Keys.onPressed: event => { + // Ensure we have focus when receiving keyboard input +@@ -222,6 +280,11 @@ ColumnLayout { + forceActiveFocus(); + } + ++ if (event.key === Qt.Key_Escape) { ++ event.accepted = false; ++ closeDialog(); ++ } ++ + // Clear error when user starts typing + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; +@@ -240,13 +303,16 @@ ColumnLayout { + } + event.accepted = true; + } else if (event.text && event.text.length > 0) { ++ if (event.key === Qt.Key_Tab) { ++ event.accepted = false; ++ return; ++ } + passwordBuffer += event.text; + event.accepted = true; + } + } + + Connections { +- target: root + function onShouldBeVisibleChanged(): void { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly +@@ -255,26 +321,22 @@ ColumnLayout { + connectButton.hasError = false; + } + } ++ ++ target: root + } + + Timer { + id: passwordFocusTimer ++ + interval: 50 + onTriggered: { + passwordContainer.forceActiveFocus(); + } + } + +- Component.onCompleted: { +- if (root.shouldBeVisible) { +- // Use Timer for actual delay to ensure focus works correctly +- passwordFocusTimer.start(); +- } +- } +- + StyledRect { + anchors.fill: parent +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) + border.color: { +@@ -303,11 +365,8 @@ ColumnLayout { + StateLayer { + hoverEnabled: false + cursorShape: Qt.IBeamCursor +- radius: Appearance.rounding.normal +- +- function onClicked(): void { +- passwordContainer.forceActiveFocus(); +- } ++ radius: Tokens.rounding.normal ++ onClicked: passwordContainer.forceActiveFocus() + } + + StyledText { +@@ -316,8 +375,8 @@ ColumnLayout { + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.normal ++ font.family: Tokens.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { +@@ -332,10 +391,10 @@ ColumnLayout { + + anchors.centerIn: parent + implicitWidth: fullWidth +- implicitHeight: Appearance.font.size.normal ++ implicitHeight: Tokens.font.size.normal + + orientation: Qt.Horizontal +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + interactive: false + + model: ScriptModel { +@@ -349,7 +408,7 @@ ColumnLayout { + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface +- radius: Appearance.rounding.small / 2 ++ radius: Tokens.rounding.small / 2 + + opacity: 0 + scale: 0 +@@ -392,8 +451,7 @@ ColumnLayout { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +@@ -405,15 +463,15 @@ ColumnLayout { + } + + RowLayout { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + TextButton { + id: cancelButton + + Layout.fillWidth: true +- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 ++ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") +@@ -428,7 +486,7 @@ ColumnLayout { + property bool hasError: false + + Layout.fillWidth: true +- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 ++ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + inactiveColour: Colours.palette.m3primary + inactiveOnColour: Colours.palette.m3onPrimary + text: qsTr("Connect") +@@ -491,45 +549,14 @@ ColumnLayout { + } + } + +- function checkConnectionStatus(): void { +- if (!root.shouldBeVisible || !connectButton.connecting) { +- return; +- } +- +- // Check if we're connected to the target network (case-insensitive SSID comparison) +- const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); +- +- if (isConnected) { +- // Successfully connected - give it a moment for network list to update +- // Use Timer for actual delay +- connectionSuccessTimer.start(); +- return; +- } +- +- // Check for connection failures - if pending connection was cleared but we're not connected +- if (Nmcli.pendingConnection === null && connectButton.connecting) { +- // Wait a bit more before giving up (allow time for connection to establish) +- if (connectionMonitor.repeatCount > 10) { +- connectionMonitor.stop(); +- connectButton.connecting = false; +- connectButton.hasError = true; +- connectButton.enabled = true; +- connectButton.text = qsTr("Connect"); +- passwordContainer.passwordBuffer = ""; +- // Delete the failed connection +- if (root.network && root.network.ssid) { +- Nmcli.forgetNetwork(root.network.ssid); +- } +- } +- } +- } +- + Timer { + id: connectionMonitor ++ ++ property int repeatCount: 0 ++ + interval: 1000 + repeat: true + triggeredOnStart: false +- property int repeatCount: 0 + + onTriggered: { + repeatCount++; +@@ -545,6 +572,7 @@ ColumnLayout { + + Timer { + id: connectionSuccessTimer ++ + interval: 500 + onTriggered: { + // Double-check connection is still active +@@ -555,8 +583,8 @@ ColumnLayout { + connectButton.connecting = false; + connectButton.text = qsTr("Connect"); + // Return to network popout on successful connection +- if (root.wrapper.currentName === "wirelesspassword") { +- root.wrapper.currentName = "network"; ++ if (root.popouts.currentName === "wirelesspassword") { ++ root.popouts.currentName = "network"; + } + closeDialog(); + } +@@ -565,12 +593,12 @@ ColumnLayout { + } + + Connections { +- target: Nmcli + function onActiveChanged() { + if (root.shouldBeVisible) { + root.checkConnectionStatus(); + } + } ++ + function onConnectionFailed(ssid: string) { + if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { + connectionMonitor.stop(); +@@ -583,23 +611,7 @@ ColumnLayout { + Nmcli.forgetNetwork(ssid); + } + } +- } +- +- function closeDialog(): void { +- if (isClosing) { +- return; +- } +- +- isClosing = true; +- passwordContainer.passwordBuffer = ""; +- connectButton.connecting = false; +- connectButton.hasError = false; +- connectButton.text = qsTr("Connect"); +- connectionMonitor.stop(); + +- // Return to network popout +- if (root.wrapper.currentName === "wirelesspassword") { +- root.wrapper.currentName = "network"; +- } ++ target: Nmcli + } + } +diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml +index 05a1d3c9..7a66d3b9 100644 +--- a/modules/bar/popouts/Wrapper.qml ++++ b/modules/bar/popouts/Wrapper.qml +@@ -1,57 +1,66 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Quickshell.Hyprland ++import Quickshell.Wayland ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import qs.modules.windowinfo + import qs.modules.controlcenter +-import Quickshell +-import Quickshell.Wayland +-import Quickshell.Hyprland +-import QtQuick ++import qs.modules.windowinfo + + Item { + id: root + + required property ShellScreen screen ++ required property real offsetScale ++ ++ readonly property alias content: content ++ readonly property alias winfo: winfo ++ readonly property alias controlCenter: controlCenter + +- readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 ++ readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth + readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight +- readonly property Item current: content.item?.current ?? null ++ readonly property Item current: (content.item as Content)?.current ?? null ++ readonly property bool isDetached: detachedMode.length > 0 + +- property string currentName ++ property alias currentName: popoutState.currentName ++ property alias hasCurrent: popoutState.hasCurrent + property real currentCenter +- property bool hasCurrent + + property string detachedMode + property string queuedMode +- readonly property bool isDetached: detachedMode.length > 0 + +- property int animLength: Appearance.anim.durations.normal +- property list animCurve: Appearance.anim.curves.emphasized ++ // Dummy object so Tokens attached prop resolves to global config ++ // Anim configs are not per-monitor ++ readonly property QtObject dummy: QtObject {} ++ property int animLength: dummy.Tokens.anim.durations.expressiveDefaultSpatial ++ property var animCurve: dummy.Tokens.anim.expressiveDefaultSpatial // The easingCurve type is Qt 6.11+ so we gotta use var for now ++ ++ function setAnims(detach: bool): void { ++ const type = `expressive${detach ? "Slow" : "Default"}Spatial`; ++ animLength = dummy.Tokens.anim.durations[type]; ++ animCurve = dummy.Tokens.anim[type]; ++ } + + function detach(mode: string): void { +- animLength = Appearance.anim.durations.large; ++ setAnims(true); + if (mode === "winfo") { + detachedMode = mode; + } else { + queuedMode = mode; + detachedMode = "any"; + } ++ setAnims(false); + focus = true; + } + + function close(): void { + hasCurrent = false; +- animCurve = Appearance.anim.curves.emphasizedAccel; +- animLength = Appearance.anim.durations.normal; + detachedMode = ""; +- animCurve = Appearance.anim.curves.emphasized; + } + +- visible: width > 0 && height > 0 +- clip: true +- + implicitWidth: nonAnimWidth + implicitHeight: nonAnimHeight + +@@ -59,7 +68,7 @@ Item { + Keys.onEscapePressed: { + // Forward escape to password popout if active, otherwise close + if (currentName === "wirelesspassword" && content.item) { +- const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); ++ const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); + if (passwordPopout && passwordPopout.item) { + passwordPopout.item.closeDialog(); + return; +@@ -75,6 +84,12 @@ Item { + } + } + ++ PopoutState { ++ id: popoutState ++ ++ onDetachRequested: mode => root.detach(mode) ++ } ++ + HyprlandFocusGrab { + active: root.isDetached + windows: [QsWindow.window] +@@ -82,15 +97,7 @@ Item { + } + + Binding { +- when: root.isDetached +- +- target: QsWindow.window +- property: "WlrLayershell.keyboardFocus" +- value: WlrKeyboardFocus.OnDemand +- } +- +- Binding { +- when: root.hasCurrent && root.currentName === "wirelesspassword" ++ when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword") + + target: QsWindow.window + property: "WlrLayershell.keyboardFocus" +@@ -101,15 +108,16 @@ Item { + id: content + + shouldBeActive: root.hasCurrent && !root.detachedMode +- anchors.right: parent.right +- anchors.verticalCenter: parent.verticalCenter ++ anchors.fill: parent + + sourceComponent: Content { +- wrapper: root ++ popouts: popoutState + } + } + + Comp { ++ id: winfo ++ + shouldBeActive: root.detachedMode === "winfo" + anchors.centerIn: parent + +@@ -120,48 +128,31 @@ Item { + } + + Comp { ++ id: controlCenter ++ + shouldBeActive: root.detachedMode === "any" + anchors.centerIn: parent + + sourceComponent: ControlCenter { + screen: root.screen + active: root.queuedMode +- +- function close(): void { +- root.close(); +- } +- } +- } +- +- Behavior on x { +- Anim { +- duration: root.animLength +- easing.bezierCurve: root.animCurve +- } +- } +- +- Behavior on y { +- enabled: root.implicitWidth > 0 +- +- Anim { +- duration: root.animLength +- easing.bezierCurve: root.animCurve ++ onClose: root.close() + } + } + + Behavior on implicitWidth { + Anim { + duration: root.animLength +- easing.bezierCurve: root.animCurve ++ easing: root.animCurve + } + } + + Behavior on implicitHeight { +- enabled: root.implicitWidth > 0 ++ enabled: root.offsetScale < 1 + + Anim { + duration: root.animLength +- easing.bezierCurve: root.animCurve ++ easing: root.animCurve + } + } + +@@ -173,6 +164,7 @@ Item { + active: false + opacity: 0 + ++ // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set + states: State { + name: "active" + when: comp.shouldBeActive +diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml +index 94b6f7ec..d8f9a462 100644 +--- a/modules/bar/popouts/kblayout/KbLayout.qml ++++ b/modules/bar/popouts/kblayout/KbLayout.qml +@@ -3,51 +3,47 @@ pragma ComponentBehavior: Bound + import QtQuick + import QtQuick.Controls + import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.services +-import qs.config +-import qs.utils +- +-import "." + + ColumnLayout { + id: root + +- required property Item wrapper ++ function refresh() { ++ kb.refresh(); ++ } ++ ++ spacing: Tokens.spacing.small ++ width: Tokens.sizes.bar.kbLayoutWidth + +- spacing: Appearance.spacing.small +- width: Config.bar.sizes.kbLayoutWidth ++ Component.onCompleted: kb.start() + + KbLayoutModel { + id: kb + } + +- function refresh() { +- kb.refresh(); +- } +- Component.onCompleted: kb.start() +- + StyledText { +- Layout.topMargin: Appearance.padding.normal +- Layout.rightMargin: Appearance.padding.small ++ Layout.topMargin: Tokens.padding.normal ++ Layout.rightMargin: Tokens.padding.small + text: qsTr("Keyboard Layouts") + font.weight: 500 + } + + ListView { + id: list ++ + model: kb.visibleModel + + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- Layout.topMargin: Appearance.spacing.small ++ Layout.rightMargin: Tokens.padding.small ++ Layout.topMargin: Tokens.spacing.small + + clip: true + interactive: true + implicitHeight: Math.min(contentHeight, 320) + visible: kb.visibleModel.count > 0 +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + add: Transition { + NumberAnimation { +@@ -85,54 +81,55 @@ ColumnLayout { + } + + delegate: Item { ++ id: kbDelegate ++ + required property int layoutIndex + required property string label ++ readonly property bool isDisabled: layoutIndex > 3 + + width: list.width +- height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) +- +- readonly property bool isDisabled: layoutIndex > 3 ++ height: Math.max(36, rowText.implicitHeight + Tokens.padding.small * 2) ++ ToolTip.visible: isDisabled && layer.containsMouse ++ ToolTip.text: "XKB limitation: maximum 4 layouts allowed" + + StateLayer { + id: layer ++ ++ onClicked: { ++ if (!kbDelegate.isDisabled) ++ kb.switchTo(kbDelegate.layoutIndex); ++ } ++ + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + implicitHeight: parent.height - 4 +- +- radius: Appearance.rounding.full +- enabled: !isDisabled +- +- function onClicked(): void { +- if (!isDisabled) +- kb.switchTo(layoutIndex); +- } ++ radius: Tokens.rounding.full ++ enabled: !kbDelegate.isDisabled + } + + StyledText { + id: rowText ++ + anchors.verticalCenter: layer.verticalCenter + anchors.left: layer.left + anchors.right: layer.right +- anchors.leftMargin: Appearance.padding.small +- anchors.rightMargin: Appearance.padding.small +- text: label ++ anchors.leftMargin: Tokens.padding.small ++ anchors.rightMargin: Tokens.padding.small ++ text: kbDelegate.label + elide: Text.ElideRight +- opacity: isDisabled ? 0.4 : 1.0 ++ opacity: kbDelegate.isDisabled ? 0.4 : 1.0 + } +- +- ToolTip.visible: isDisabled && layer.containsMouse +- ToolTip.text: "XKB limitation: maximum 4 layouts allowed" + } + } + + Rectangle { + visible: kb.activeLabel.length > 0 + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- Layout.topMargin: Appearance.spacing.small ++ Layout.rightMargin: Tokens.padding.small ++ Layout.topMargin: Tokens.spacing.small + +- height: 1 ++ implicitHeight: 1 + color: Colours.palette.m3onSurfaceVariant + opacity: 0.35 + } +@@ -142,9 +139,9 @@ ColumnLayout { + + visible: kb.activeLabel.length > 0 + Layout.fillWidth: true +- Layout.rightMargin: Appearance.padding.small +- Layout.topMargin: Appearance.spacing.small +- spacing: Appearance.spacing.small ++ Layout.rightMargin: Tokens.padding.small ++ Layout.topMargin: Tokens.spacing.small ++ spacing: Tokens.spacing.small + + opacity: 1 + scale: 1 +@@ -163,16 +160,18 @@ ColumnLayout { + } + + Connections { +- target: kb + function onActiveLabelChanged() { + if (!activeRow.visible) + return; + popIn.restart(); + } ++ ++ target: kb + } + + SequentialAnimation { + id: popIn ++ + running: false + + ParallelAnimation { +diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml +index 43710953..f62d0b98 100644 +--- a/modules/bar/popouts/kblayout/KbLayoutModel.qml ++++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml +@@ -1,24 +1,20 @@ + pragma ComponentBehavior: Bound + + import QtQuick +- +-import Quickshell + import Quickshell.Io +- +-import qs.config + import Caelestia ++import Caelestia.Config ++ ++// TODO: handle this better later + + Item { + id: model +- visible: false + +- ListModel { +- id: _visibleModel +- } + property alias visibleModel: _visibleModel +- + property string activeLabel: "" + property int activeIndex: -1 ++ property var _xkbMap: ({}) ++ property bool _notifiedLimit: false + + function start() { + _xkbXmlBase.running = true; +@@ -35,31 +31,6 @@ Item { + _switchProc.running = true; + } + +- ListModel { +- id: _layoutsModel +- } +- +- property var _xkbMap: ({}) +- property bool _notifiedLimit: false +- +- Process { +- id: _xkbXmlBase +- command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] +- stdout: StdioCollector { +- onStreamFinished: _buildXmlMap(text) +- } +- onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) +- _xkbXmlEvdev.running = true +- } +- +- Process { +- id: _xkbXmlEvdev +- command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] +- stdout: StdioCollector { +- onStreamFinished: _buildXmlMap(text) +- } +- } +- + function _buildXmlMap(xml) { + const map = {}; + +@@ -105,8 +76,84 @@ Item { + return `${lang} (${code})`; + } + ++ function _setLayouts(raw) { ++ const parts = raw.split(",").map(s => s.trim()).filter(Boolean); ++ _layoutsModel.clear(); ++ ++ const seen = new Set(); ++ let idx = 0; ++ ++ for (const p of parts) { ++ if (seen.has(p)) ++ continue; ++ seen.add(p); ++ _layoutsModel.append({ ++ layoutIndex: idx, ++ token: p, ++ label: _pretty(p) ++ }); ++ idx++; ++ } ++ } ++ ++ function _rebuildVisible() { ++ _visibleModel.clear(); ++ ++ let arr = []; ++ for (let i = 0; i < _layoutsModel.count; i++) ++ arr.push(_layoutsModel.get(i)); ++ ++ arr = arr.filter(i => i.layoutIndex !== activeIndex); ++ arr.forEach(i => _visibleModel.append(i)); ++ ++ if (!GlobalConfig.utilities.toasts.kbLimit) ++ return; ++ ++ if (_layoutsModel.count > 4) { ++ Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); ++ } ++ } ++ ++ function _pretty(token) { ++ const code = token.replace(/\(.*\)$/, "").trim(); ++ if (_xkbMap[code]) ++ return code.toUpperCase() + " - " + _xkbMap[code]; ++ return code.toUpperCase() + " - " + code; ++ } ++ ++ visible: false ++ ++ ListModel { ++ id: _visibleModel ++ } ++ ++ ListModel { ++ id: _layoutsModel ++ } ++ ++ Process { ++ id: _xkbXmlBase ++ ++ command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] ++ stdout: StdioCollector { ++ onStreamFinished: model._buildXmlMap(text) ++ } ++ onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property ++ _xkbXmlEvdev.running = true ++ } ++ ++ Process { ++ id: _xkbXmlEvdev ++ ++ command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] ++ stdout: StdioCollector { ++ onStreamFinished: model._buildXmlMap(text) ++ } ++ } ++ + Process { + id: _getKbLayoutOpt ++ + command: ["hyprctl", "-j", "getoption", "input:kb_layout"] + stdout: StdioCollector { + onStreamFinished: { +@@ -114,7 +161,7 @@ Item { + const j = JSON.parse(text); + const raw = (j?.str || j?.value || "").toString().trim(); + if (raw.length) { +- _setLayouts(raw); ++ model._setLayouts(raw); + _fetchActiveLayouts.running = true; + return; + } +@@ -126,6 +173,7 @@ Item { + + Process { + id: _fetchLayoutsFromDevices ++ + command: ["hyprctl", "-j", "devices"] + stdout: StdioCollector { + onStreamFinished: { +@@ -134,7 +182,7 @@ Item { + const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; + const raw = (kb?.layout || "").trim(); + if (raw.length) +- _setLayouts(raw); ++ model._setLayouts(raw); + } catch (e) {} + _fetchActiveLayouts.running = true; + } +@@ -143,6 +191,7 @@ Item { + + Process { + id: _fetchActiveLayouts ++ + command: ["hyprctl", "-j", "devices"] + stdout: StdioCollector { + onStreamFinished: { +@@ -151,66 +200,22 @@ Item { + const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; + const idx = kb?.active_layout_index ?? -1; + +- activeIndex = idx >= 0 ? idx : -1; +- activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; ++ model.activeIndex = idx >= 0 ? idx : -1; ++ model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; + } catch (e) { +- activeIndex = -1; +- activeLabel = ""; ++ model.activeIndex = -1; ++ model.activeLabel = ""; + } + +- _rebuildVisible(); ++ model._rebuildVisible(); + } + } + } + + Process { + id: _switchProc ++ + onRunningChanged: if (!running) + _fetchActiveLayouts.running = true + } +- +- function _setLayouts(raw) { +- const parts = raw.split(",").map(s => s.trim()).filter(Boolean); +- _layoutsModel.clear(); +- +- const seen = new Set(); +- let idx = 0; +- +- for (const p of parts) { +- if (seen.has(p)) +- continue; +- seen.add(p); +- _layoutsModel.append({ +- layoutIndex: idx, +- token: p, +- label: _pretty(p) +- }); +- idx++; +- } +- } +- +- function _rebuildVisible() { +- _visibleModel.clear(); +- +- let arr = []; +- for (let i = 0; i < _layoutsModel.count; i++) +- arr.push(_layoutsModel.get(i)); +- +- arr = arr.filter(i => i.layoutIndex !== activeIndex); +- arr.forEach(i => _visibleModel.append(i)); +- +- if (!Config.utilities.toasts.kbLimit) +- return; +- +- if (_layoutsModel.count > 4) { +- Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); +- } +- } +- +- function _pretty(token) { +- const code = token.replace(/\(.*\)$/, "").trim(); +- if (_xkbMap[code]) +- return code.toUpperCase() + " - " + _xkbMap[code]; +- return code.toUpperCase() + " - " + code; +- } + } +diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml +index 4aacfad9..c8b435ca 100644 +--- a/modules/controlcenter/ControlCenter.qml ++++ b/modules/controlcenter/ControlCenter.qml +@@ -1,34 +1,34 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root + + required property ShellScreen screen +- readonly property int rounding: floating ? 0 : Appearance.rounding.normal ++ readonly property int rounding: floating ? 0 : Tokens.rounding.large + + property alias floating: session.floating + property alias active: session.active + property alias navExpanded: session.navExpanded + ++ readonly property bool initialOpeningComplete: panes.initialOpeningComplete + readonly property Session session: Session { + id: session + + root: root + } + +- function close(): void { +- } ++ signal close + +- implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio +- implicitHeight: screen.height * Config.controlCenter.sizes.heightMult ++ implicitWidth: implicitHeight * Tokens.sizes.controlCenter.ratio ++ implicitHeight: screen.height * Tokens.sizes.controlCenter.heightMult + + GridLayout { + anchors.fill: parent +@@ -42,6 +42,7 @@ Item { + Layout.fillWidth: true + Layout.columnSpan: 2 + ++ asynchronous: true + active: root.floating + visible: active + +@@ -60,8 +61,6 @@ Item { + color: Colours.tPalette.m3surfaceContainer + + CustomMouseArea { +- anchors.fill: parent +- + function onWheel(event: WheelEvent): void { + // Prevent tab switching during initial opening animation to avoid blank pages + if (!panes.initialOpeningComplete) { +@@ -73,6 +72,8 @@ Item { + else if (event.angleDelta.y > 0) + root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); + } ++ ++ anchors.fill: parent + } + + NavRail { +@@ -95,6 +96,4 @@ Item { + session: root.session + } + } +- +- readonly property bool initialOpeningComplete: panes.initialOpeningComplete + } +diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml +index e61a741a..a126c800 100644 +--- a/modules/controlcenter/NavRail.qml ++++ b/modules/controlcenter/NavRail.qml +@@ -1,12 +1,12 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config + import qs.modules.controlcenter +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root +@@ -15,23 +15,23 @@ Item { + required property Session session + required property bool initialOpeningComplete + +- implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 +- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 ++ implicitWidth: layout.implicitWidth + Tokens.padding.larger * 4 ++ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + + ColumnLayout { + id: layout + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: Appearance.padding.larger * 2 +- spacing: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.padding.larger * 2 ++ spacing: Tokens.spacing.normal + + states: State { + name: "expanded" + when: root.session.navExpanded + + PropertyChanges { +- layout.spacing: Appearance.spacing.small ++ layout.spacing: root.Tokens.spacing.small + } + } + +@@ -42,7 +42,8 @@ Item { + } + + Loader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large ++ asynchronous: true + active: !root.session.floating + visible: active + +@@ -50,23 +51,23 @@ Item { + readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 + + implicitWidth: nonAnimWidth +- implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth ++ implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Tokens.padding.normal * 2 : nonAnimWidth + + color: Colours.palette.m3primaryContainer +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + + StateLayer { + id: normalWinState + +- color: Colours.palette.m3onPrimaryContainer +- +- function onClicked(): void { ++ onClicked: { + root.session.root.close(); + WindowFactory.create(null, { + active: root.session.active, + navExpanded: root.session.navExpanded + }); + } ++ ++ color: Colours.palette.m3onPrimaryContainer + } + + MaterialIcon { +@@ -74,11 +75,11 @@ Item { + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: Appearance.padding.large ++ anchors.leftMargin: Tokens.padding.large + + text: "select_window" + color: Colours.palette.m3onPrimaryContainer +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: 1 + } + +@@ -87,7 +88,7 @@ Item { + + anchors.left: normalWinIcon.right + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.spacing.normal + + text: qsTr("Float window") + color: Colours.palette.m3onPrimaryContainer +@@ -95,22 +96,20 @@ Item { + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } + + Behavior on implicitWidth { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +@@ -121,7 +120,8 @@ Item { + + NavItem { + required property int index +- Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 ++ ++ Layout.topMargin: index === 0 ? Tokens.spacing.large * 2 : 0 + icon: PaneRegistry.getByIndex(index).icon + label: PaneRegistry.getByIndex(index).label + } +@@ -146,7 +146,7 @@ Item { + expandedLabel.opacity: 1 + smallLabel.opacity: 0 + background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth +- background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 ++ background.implicitHeight: icon.implicitHeight + root.Tokens.padding.normal * 2 + item.implicitHeight: background.implicitHeight + } + } +@@ -154,35 +154,34 @@ Item { + transitions: Transition { + Anim { + property: "opacity" +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + + Anim { + properties: "implicitWidth,implicitHeight" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + StyledRect { + id: background + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) + + implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 +- implicitHeight: icon.implicitHeight + Appearance.padding.small ++ implicitHeight: icon.implicitHeight + Tokens.padding.small + + StateLayer { +- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- +- function onClicked(): void { ++ onClicked: { + // Prevent tab switching during initial opening animation to avoid blank pages + if (!root.initialOpeningComplete) { + return; + } + root.session.active = item.label; + } ++ ++ color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + } + + MaterialIcon { +@@ -190,11 +189,11 @@ Item { + + anchors.left: parent.left + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: Appearance.padding.large ++ anchors.leftMargin: Tokens.padding.large + + text: item.icon + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: item.active ? 1 : 0 + + Behavior on fill { +@@ -207,7 +206,7 @@ Item { + + anchors.left: icon.right + anchors.verticalCenter: parent.verticalCenter +- anchors.leftMargin: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.spacing.normal + + opacity: 0 + text: item.label +@@ -220,10 +219,10 @@ Item { + + anchors.horizontalCenter: icon.horizontalCenter + anchors.top: icon.bottom +- anchors.topMargin: Appearance.spacing.small / 2 ++ anchors.topMargin: Tokens.spacing.small / 2 + + text: item.label +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + font.capitalization: Font.Capitalize + } + } +diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml +index b3e19c81..f26a2aad 100644 +--- a/modules/controlcenter/PaneRegistry.qml ++++ b/modules/controlcenter/PaneRegistry.qml +@@ -36,6 +36,12 @@ QtObject { + readonly property string icon: "task_alt" + readonly property string component: "taskbar/TaskbarPane.qml" + }, ++ QtObject { ++ readonly property string id: "notifications" ++ readonly property string label: "notifications" ++ readonly property string icon: "notifications" ++ readonly property string component: "notifications/NotificationsPane.qml" ++ }, + QtObject { + readonly property string id: "launcher" + readonly property string label: "launcher" +@@ -47,6 +53,12 @@ QtObject { + readonly property string label: "monitors" + readonly property string icon: "monitor" + readonly property string component: "monitors/MonitorsPane.qml" ++ }, ++ QtObject { ++ readonly property string id: "dashboard" ++ readonly property string label: "dashboard" ++ readonly property string icon: "dashboard" ++ readonly property string component: "dashboard/DashboardPane.qml" + } + ] + +@@ -60,7 +72,7 @@ QtObject { + return result; + } + +- function getByIndex(index: int): QtObject { ++ function getByIndex(index: int): var { + if (index >= 0 && index < panes.length) { + return panes[index]; + } +@@ -76,12 +88,12 @@ QtObject { + return -1; + } + +- function getByLabel(label: string): QtObject { ++ function getByLabel(label: string): var { + const index = getIndexByLabel(label); + return getByIndex(index); + } + +- function getById(id: string): QtObject { ++ function getById(id: string): var { + for (let i = 0; i < panes.length; i++) { + if (panes[i].id === id) { + return panes[i]; +diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml +index 90a369a9..e8803011 100644 +--- a/modules/controlcenter/Panes.qml ++++ b/modules/controlcenter/Panes.qml +@@ -5,15 +5,17 @@ import "network" + import "audio" + import "appearance" + import "taskbar" ++import "notifications" + import "launcher" + import "monitors" ++import "dashboard" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config + import qs.modules.controlcenter +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts + + ClippingRectangle { + id: root +@@ -37,26 +39,27 @@ ClippingRectangle { + } + + Connections { +- target: root.session +- + function onActiveIndexChanged(): void { + root.focus = true; + } ++ ++ target: root.session + } + + ColumnLayout { + id: layout + ++ property bool animationComplete: true ++ property bool initialOpeningComplete: false ++ + spacing: 0 + y: -root.session.activeIndex * root.height + clip: true + +- property bool animationComplete: true +- property bool initialOpeningComplete: false +- + Timer { + id: animationDelayTimer +- interval: Appearance.anim.durations.normal ++ ++ interval: Tokens.anim.durations.normal + onTriggered: { + layout.animationComplete = true; + } +@@ -64,7 +67,8 @@ ClippingRectangle { + + Timer { + id: initialOpeningTimer +- interval: Appearance.anim.durations.large ++ ++ interval: Tokens.anim.durations.large + running: true + onTriggered: { + layout.initialOpeningComplete = true; +@@ -76,6 +80,7 @@ ClippingRectangle { + + Pane { + required property int index ++ + paneIndex: index + componentPath: PaneRegistry.getByIndex(index).component + } +@@ -86,11 +91,12 @@ ClippingRectangle { + } + + Connections { +- target: root.session + function onActiveIndexChanged(): void { + layout.animationComplete = false; + animationDelayTimer.restart(); + } ++ ++ target: root.session + } + } + +@@ -99,10 +105,6 @@ ClippingRectangle { + + required property int paneIndex + required property string componentPath +- +- implicitWidth: root.width +- implicitHeight: root.height +- + property bool hasBeenLoaded: false + + function updateActive(): void { +@@ -125,10 +127,14 @@ ClippingRectangle { + loader.active = shouldBeActive; + } + ++ implicitWidth: root.width ++ implicitHeight: root.height ++ + Loader { + id: loader + + anchors.fill: parent ++ asynchronous: true + clip: false + active: false + +@@ -156,20 +162,22 @@ ClippingRectangle { + } + + Connections { +- target: root.session + function onActiveIndexChanged(): void { + pane.updateActive(); + } ++ ++ target: root.session + } + + Connections { +- target: layout + function onInitialOpeningCompleteChanged(): void { + pane.updateActive(); + } + function onAnimationCompleteChanged(): void { + pane.updateActive(); + } ++ ++ target: layout + } + } + } +diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml +index 3678093b..33805609 100644 +--- a/modules/controlcenter/Session.qml ++++ b/modules/controlcenter/Session.qml +@@ -1,5 +1,5 @@ +-import QtQuick + import "./state" ++import QtQuick + import qs.modules.controlcenter + + QtObject { +diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml +index abcf5df1..dc0dc4a0 100644 +--- a/modules/controlcenter/WindowFactory.qml ++++ b/modules/controlcenter/WindowFactory.qml +@@ -1,9 +1,9 @@ + pragma Singleton + ++import QtQuick ++import Quickshell + import qs.components + import qs.services +-import Quickshell +-import QtQuick + + Singleton { + id: root +@@ -47,11 +47,8 @@ Singleton { + + anchors.fill: parent + screen: win.screen ++ onClose: win.destroy() + floating: true +- +- function close(): void { +- win.destroy(); +- } + } + + Behavior on color { +diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml +index fb716089..8f66968b 100644 +--- a/modules/controlcenter/WindowTitle.qml ++++ b/modules/controlcenter/WindowTitle.qml +@@ -1,8 +1,8 @@ ++import QtQuick ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell +-import QtQuick + + StyledRect { + id: root +@@ -10,7 +10,7 @@ StyledRect { + required property ShellScreen screen + required property Session session + +- implicitHeight: text.implicitHeight + Appearance.padding.normal ++ implicitHeight: text.implicitHeight + Tokens.padding.normal + color: Colours.tPalette.m3surfaceContainer + + StyledText { +@@ -21,24 +21,24 @@ StyledRect { + + text: qsTr("Caelestia Settings - %1").arg(root.session.active) + font.capitalization: Font.Capitalize +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + + Item { + anchors.right: parent.right + anchors.top: parent.top +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + implicitWidth: implicitHeight +- implicitHeight: closeIcon.implicitHeight + Appearance.padding.small ++ implicitHeight: closeIcon.implicitHeight + Tokens.padding.small + + StateLayer { +- radius: Appearance.rounding.full +- +- function onClicked(): void { ++ onClicked: { + QsWindow.window.destroy(); + } ++ ++ radius: Tokens.rounding.full + } + + MaterialIcon { +diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml +index 42511677..a1e0e42c 100644 +--- a/modules/controlcenter/appearance/AppearancePane.qml ++++ b/modules/controlcenter/appearance/AppearancePane.qml +@@ -4,26 +4,26 @@ import ".." + import "../components" + import "./sections" + import "../../launcher/services" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config ++import Caelestia.Models + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.components.images + import qs.services +-import qs.config + import qs.utils +-import Caelestia.Models +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root + + required property Session session + +- property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 ++ property real animDurationsScale: GlobalConfig.appearance.anim.durations.scale ?? 1 + property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" + property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" + property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" +@@ -31,9 +31,9 @@ Item { + property real paddingScale: Config.appearance.padding.scale ?? 1 + property real roundingScale: Config.appearance.rounding.scale ?? 1 + property real spacingScale: Config.appearance.spacing.scale ?? 1 +- property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false +- property real transparencyBase: Config.appearance.transparency.base ?? 0.85 +- property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4 ++ property bool transparencyEnabled: GlobalConfig.appearance.transparency.enabled ?? false ++ property real transparencyBase: GlobalConfig.appearance.transparency.base ?? 0.85 ++ property real transparencyLayers: GlobalConfig.appearance.transparency.layers ?? 0.4 + property real borderRounding: Config.border.rounding ?? 1 + property real borderThickness: Config.border.thickness ?? 1 + +@@ -48,52 +48,53 @@ Item { + property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false + property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false + property bool backgroundEnabled: Config.background.enabled ?? true ++ property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true + property bool visualiserEnabled: Config.background.visualiser.enabled ?? false + property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true + property real visualiserRounding: Config.background.visualiser.rounding ?? 1 + property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 + +- anchors.fill: parent +- + function saveConfig() { +- Config.appearance.anim.durations.scale = root.animDurationsScale; +- +- Config.appearance.font.family.material = root.fontFamilyMaterial; +- Config.appearance.font.family.mono = root.fontFamilyMono; +- Config.appearance.font.family.sans = root.fontFamilySans; +- Config.appearance.font.size.scale = root.fontSizeScale; +- +- Config.appearance.padding.scale = root.paddingScale; +- Config.appearance.rounding.scale = root.roundingScale; +- Config.appearance.spacing.scale = root.spacingScale; +- +- Config.appearance.transparency.enabled = root.transparencyEnabled; +- Config.appearance.transparency.base = root.transparencyBase; +- Config.appearance.transparency.layers = root.transparencyLayers; +- +- Config.background.desktopClock.enabled = root.desktopClockEnabled; +- Config.background.enabled = root.backgroundEnabled; +- Config.background.desktopClock.scale = root.desktopClockScale; +- Config.background.desktopClock.position = root.desktopClockPosition; +- Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; +- Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; +- Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; +- Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; +- Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; +- Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; +- Config.background.desktopClock.invertColors = root.desktopClockInvertColors; +- +- Config.background.visualiser.enabled = root.visualiserEnabled; +- Config.background.visualiser.autoHide = root.visualiserAutoHide; +- Config.background.visualiser.rounding = root.visualiserRounding; +- Config.background.visualiser.spacing = root.visualiserSpacing; +- +- Config.border.rounding = root.borderRounding; +- Config.border.thickness = root.borderThickness; +- +- Config.save(); ++ GlobalConfig.appearance.anim.durations.scale = root.animDurationsScale; ++ ++ GlobalConfig.appearance.font.family.material = root.fontFamilyMaterial; ++ GlobalConfig.appearance.font.family.mono = root.fontFamilyMono; ++ GlobalConfig.appearance.font.family.sans = root.fontFamilySans; ++ GlobalConfig.appearance.font.size.scale = root.fontSizeScale; ++ ++ GlobalConfig.appearance.padding.scale = root.paddingScale; ++ GlobalConfig.appearance.rounding.scale = root.roundingScale; ++ GlobalConfig.appearance.spacing.scale = root.spacingScale; ++ ++ GlobalConfig.appearance.transparency.enabled = root.transparencyEnabled; ++ GlobalConfig.appearance.transparency.base = root.transparencyBase; ++ GlobalConfig.appearance.transparency.layers = root.transparencyLayers; ++ ++ GlobalConfig.background.desktopClock.enabled = root.desktopClockEnabled; ++ GlobalConfig.background.enabled = root.backgroundEnabled; ++ GlobalConfig.background.desktopClock.scale = root.desktopClockScale; ++ GlobalConfig.background.desktopClock.position = root.desktopClockPosition; ++ GlobalConfig.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; ++ GlobalConfig.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; ++ GlobalConfig.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; ++ GlobalConfig.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; ++ GlobalConfig.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; ++ GlobalConfig.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; ++ GlobalConfig.background.desktopClock.invertColors = root.desktopClockInvertColors; ++ ++ GlobalConfig.background.wallpaperEnabled = root.wallpaperEnabled; ++ ++ GlobalConfig.background.visualiser.enabled = root.visualiserEnabled; ++ GlobalConfig.background.visualiser.autoHide = root.visualiserAutoHide; ++ GlobalConfig.background.visualiser.rounding = root.visualiserRounding; ++ GlobalConfig.background.visualiser.spacing = root.visualiserSpacing; ++ ++ GlobalConfig.border.rounding = root.borderRounding; ++ GlobalConfig.border.thickness = root.borderThickness; + } + ++ anchors.fill: parent ++ + Component { + id: appearanceRightContentComponent + +@@ -108,9 +109,9 @@ Item { + + StyledText { + Layout.alignment: Qt.AlignHCenter +- Layout.bottomMargin: Appearance.spacing.normal ++ Layout.bottomMargin: Tokens.spacing.normal + text: qsTr("Wallpaper") +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + font.weight: 600 + } + +@@ -119,8 +120,9 @@ Item { + + Layout.fillWidth: true + Layout.fillHeight: true +- Layout.bottomMargin: -Appearance.padding.large * 2 ++ Layout.bottomMargin: -Tokens.padding.large * 2 + ++ asynchronous: true + active: { + const isActive = root.session.activeIndex === 3; + const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; +@@ -132,7 +134,7 @@ Item { + + onStatusChanged: { + if (status === Loader.Error) { +- console.error("[AppearancePane] Wallpaper loader error!"); ++ console.error(lc, "Wallpaper loader error!"); + } + } + +@@ -148,10 +150,11 @@ Item { + anchors.fill: parent + + leftContent: Component { +- + StyledFlickable { + id: sidebarFlickable ++ + readonly property var rootPane: root ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: sidebarLayout.height + +@@ -161,20 +164,20 @@ Item { + + ColumnLayout { + id: sidebarLayout +- anchors.left: parent.left +- anchors.right: parent.right +- spacing: Appearance.spacing.small + + readonly property var rootPane: sidebarFlickable.rootPane +- + readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded + ++ anchors.left: parent.left ++ anchors.right: parent.right ++ spacing: Tokens.spacing.small ++ + RowLayout { +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Appearance") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -215,31 +218,37 @@ Item { + + AnimationsSection { + id: animationsSection ++ + rootPane: sidebarFlickable.rootPane + } + + FontsSection { + id: fontsSection ++ + rootPane: sidebarFlickable.rootPane + } + + ScalesSection { + id: scalesSection ++ + rootPane: sidebarFlickable.rootPane + } + + TransparencySection { + id: transparencySection ++ + rootPane: sidebarFlickable.rootPane + } + + BorderSection { + id: borderSection ++ + rootPane: sidebarFlickable.rootPane + } + + BackgroundSection { + id: backgroundSection ++ + rootPane: sidebarFlickable.rootPane + } + } +@@ -248,4 +257,11 @@ Item { + + rightContent: appearanceRightContentComponent + } ++ ++ LoggingCategory { ++ id: lc ++ ++ name: "caelestia.qml.controlcenter.appearance" ++ defaultLogLevel: LoggingCategory.Info ++ } + } +diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml +index 0cba5cec..412441ab 100644 +--- a/modules/controlcenter/appearance/sections/AnimationsSection.qml ++++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml +@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + id: root +@@ -19,7 +19,7 @@ CollapsibleSection { + showBackground: true + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml +index 2f75c9e0..3f31f10b 100644 +--- a/modules/controlcenter/appearance/sections/BackgroundSection.qml ++++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml +@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + id: root +@@ -27,10 +27,19 @@ CollapsibleSection { + } + } + ++ SwitchRow { ++ label: qsTr("Wallpaper enabled") ++ checked: rootPane.wallpaperEnabled ++ onToggled: checked => { ++ rootPane.wallpaperEnabled = checked; ++ rootPane.saveConfig(); ++ } ++ } ++ + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Desktop Clock") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -46,9 +55,6 @@ CollapsibleSection { + SectionContainer { + id: posContainer + +- contentSpacing: Appearance.spacing.small +- z: 1 +- + readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') + readonly property string currentV: pos[0] + readonly property string currentH: pos[1] +@@ -58,9 +64,12 @@ CollapsibleSection { + rootPane.saveConfig(); + } + ++ contentSpacing: Tokens.spacing.small ++ z: 1 ++ + StyledText { + text: qsTr("Positioning") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -70,19 +79,22 @@ CollapsibleSection { + + menuItems: [ + MenuItem { ++ property string val: "top" ++ + text: qsTr("Top") + icon: "vertical_align_top" +- property string val: "top" + }, + MenuItem { ++ property string val: "middle" ++ + text: qsTr("Middle") + icon: "vertical_align_center" +- property string val: "middle" + }, + MenuItem { ++ property string val: "bottom" ++ + text: qsTr("Bottom") + icon: "vertical_align_bottom" +- property string val: "bottom" + } + ] + +@@ -104,19 +116,22 @@ CollapsibleSection { + + menuItems: [ + MenuItem { ++ property string val: "left" ++ + text: qsTr("Left") + icon: "align_horizontal_left" +- property string val: "left" + }, + MenuItem { ++ property string val: "center" ++ + text: qsTr("Center") + icon: "align_horizontal_center" +- property string val: "center" + }, + MenuItem { ++ property string val: "right" ++ + text: qsTr("Right") + icon: "align_horizontal_right" +- property string val: "right" + } + ] + +@@ -141,11 +156,11 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small ++ contentSpacing: Tokens.spacing.small + + StyledText { + text: qsTr("Shadow") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -159,7 +174,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -184,7 +199,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -210,11 +225,11 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small ++ contentSpacing: Tokens.spacing.small + + StyledText { + text: qsTr("Background") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -237,7 +252,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -263,9 +278,9 @@ CollapsibleSection { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Visualiser") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -288,7 +303,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -313,7 +328,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml +index 9532d70d..7dbd2dbe 100644 +--- a/modules/controlcenter/appearance/sections/BorderSection.qml ++++ b/modules/controlcenter/appearance/sections/BorderSection.qml +@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + id: root +@@ -19,7 +19,7 @@ CollapsibleSection { + showBackground: true + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -43,14 +43,14 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Border thickness") + value: rootPane.borderThickness +- from: 0.1 ++ from: 0 + to: 100 + decimals: 1 + suffix: "px" +diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +index 95cb4b72..0a65cbdc 100644 +--- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml ++++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +@@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../../launcher/services" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + title: qsTr("Color scheme") +@@ -18,7 +18,7 @@ CollapsibleSection { + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + Repeater { + model: Schemes.list +@@ -32,12 +32,13 @@ CollapsibleSection { + readonly property bool isCurrent: schemeKey === Schemes.currentScheme + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary ++ implicitHeight: schemeRow.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { ++ onClicked: { + const name = modelData.name; + const flavour = modelData.flavour; + const schemeKey = `${name} ${flavour}`; +@@ -53,6 +54,7 @@ CollapsibleSection { + + Timer { + id: reloadTimer ++ + interval: 300 + onTriggered: { + Schemes.reload(); +@@ -63,9 +65,9 @@ CollapsibleSection { + id: schemeRow + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + id: preview +@@ -76,15 +78,16 @@ CollapsibleSection { + border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) + + color: `#${modelData.colours?.surface}` +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + implicitWidth: iconPlaceholder.implicitWidth + implicitHeight: iconPlaceholder.implicitWidth + + MaterialIcon { + id: iconPlaceholder ++ + visible: false + text: "circle" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + + Item { +@@ -102,7 +105,7 @@ CollapsibleSection { + + implicitWidth: preview.implicitWidth + color: `#${modelData.colours?.primary}` +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + } + } + } +@@ -113,12 +116,12 @@ CollapsibleSection { + + StyledText { + text: modelData.flavour ?? "" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledText { + text: modelData.name ?? "" +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + + elide: Text.ElideRight +@@ -128,17 +131,16 @@ CollapsibleSection { + } + + Loader { ++ asynchronous: true + active: isCurrent + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +- +- implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } +diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml +index 3aa17dd9..c612485d 100644 +--- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml ++++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml +@@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../../launcher/services" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + title: qsTr("Color variant") +@@ -18,7 +18,7 @@ CollapsibleSection { + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + Repeater { + model: M3Variants.list +@@ -29,12 +29,13 @@ CollapsibleSection { + Layout.fillWidth: true + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 + border.color: Colours.palette.m3primary ++ implicitHeight: variantRow.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { ++ onClicked: { + const variant = modelData.variant; + + Schemes.currentVariant = variant; +@@ -48,6 +49,7 @@ CollapsibleSection { + + Timer { + id: reloadTimer ++ + interval: 300 + onTriggered: { + Schemes.reload(); +@@ -60,13 +62,13 @@ CollapsibleSection { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: modelData.icon +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: modelData.variant === Schemes.currentVariant ? 1 : 0 + } + +@@ -80,11 +82,9 @@ CollapsibleSection { + visible: modelData.variant === Schemes.currentVariant + text: "check" + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } +- +- implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } +diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml +index 3988863a..10040c58 100644 +--- a/modules/controlcenter/appearance/sections/FontsSection.qml ++++ b/modules/controlcenter/appearance/sections/FontsSection.qml +@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + id: root +@@ -19,62 +19,64 @@ CollapsibleSection { + showBackground: true + + CollapsibleSection { +- id: materialFontSection +- title: qsTr("Material font family") ++ id: sansFontSection ++ ++ title: qsTr("Sans-serif font family") + expanded: true + showBackground: true + nested: true + + Loader { +- id: materialFontLoader + Layout.fillWidth: true + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 +- active: materialFontSection.expanded ++ asynchronous: true ++ active: sansFontSection.expanded + + sourceComponent: StyledListView { +- id: materialFontList +- property alias contentHeight: materialFontList.contentHeight ++ id: sansFontList ++ ++ property alias contentHeight: sansFontList.contentHeight + + clip: true +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { +- flickable: materialFontList ++ flickable: sansFontList + } + + delegate: StyledRect { + required property string modelData + required property int index ++ readonly property bool isCurrent: modelData === rootPane.fontFamilySans + + width: ListView.view.width +- +- readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary ++ implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { +- rootPane.fontFamilyMaterial = modelData; ++ onClicked: { ++ rootPane.fontFamilySans = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { +- id: fontFamilyMaterialRow ++ id: fontFamilySansRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: modelData +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + Item { +@@ -82,17 +84,16 @@ CollapsibleSection { + } + + Loader { ++ asynchronous: true + active: isCurrent + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +- +- implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } +@@ -100,6 +101,7 @@ CollapsibleSection { + + CollapsibleSection { + id: monoFontSection ++ + title: qsTr("Monospace font family") + expanded: false + showBackground: true +@@ -108,14 +110,16 @@ CollapsibleSection { + Loader { + Layout.fillWidth: true + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 ++ asynchronous: true + active: monoFontSection.expanded + + sourceComponent: StyledListView { + id: monoFontList ++ + property alias contentHeight: monoFontList.contentHeight + + clip: true +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + model: Qt.fontFamilies() + + StyledScrollBar.vertical: StyledScrollBar { +@@ -125,17 +129,17 @@ CollapsibleSection { + delegate: StyledRect { + required property string modelData + required property int index ++ readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + + width: ListView.view.width +- +- readonly property bool isCurrent: modelData === rootPane.fontFamilyMono + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary ++ implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { ++ onClicked: { + rootPane.fontFamilyMono = modelData; + rootPane.saveConfig(); + } +@@ -147,13 +151,13 @@ CollapsibleSection { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: modelData +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + Item { +@@ -161,78 +165,82 @@ CollapsibleSection { + } + + Loader { ++ asynchronous: true + active: isCurrent + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +- +- implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + CollapsibleSection { +- id: sansFontSection +- title: qsTr("Sans-serif font family") ++ id: materialFontSection ++ ++ title: qsTr("Material font family") + expanded: false + showBackground: true + nested: true + + Loader { ++ id: materialFontLoader ++ + Layout.fillWidth: true + Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 +- active: sansFontSection.expanded ++ asynchronous: true ++ active: materialFontSection.expanded + + sourceComponent: StyledListView { +- id: sansFontList +- property alias contentHeight: sansFontList.contentHeight ++ id: materialFontList ++ ++ property alias contentHeight: materialFontList.contentHeight + + clip: true +- spacing: Appearance.spacing.small / 2 +- model: Qt.fontFamilies() ++ spacing: Tokens.spacing.small / 2 ++ model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) + + StyledScrollBar.vertical: StyledScrollBar { +- flickable: sansFontList ++ flickable: materialFontList + } + + delegate: StyledRect { + required property string modelData + required property int index ++ readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial + + width: ListView.view.width +- +- readonly property bool isCurrent: modelData === rootPane.fontFamilySans + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + border.width: isCurrent ? 1 : 0 + border.color: Colours.palette.m3primary ++ implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { +- rootPane.fontFamilySans = modelData; ++ onClicked: { ++ rootPane.fontFamilyMaterial = modelData; + rootPane.saveConfig(); + } + } + + RowLayout { +- id: fontFamilySansRow ++ id: fontFamilyMaterialRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: modelData +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + Item { +@@ -240,24 +248,23 @@ CollapsibleSection { + } + + Loader { ++ asynchronous: true + active: isCurrent + + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +- +- implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 + } + } + } + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml +index b0e6e38b..dac6226e 100644 +--- a/modules/controlcenter/appearance/sections/ScalesSection.qml ++++ b/modules/controlcenter/appearance/sections/ScalesSection.qml +@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + id: root +@@ -19,7 +19,7 @@ CollapsibleSection { + showBackground: true + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -43,7 +43,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -67,7 +67,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml +index 04eed911..aab53b8b 100644 +--- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml ++++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml +@@ -1,12 +1,12 @@ + pragma ComponentBehavior: Bound + + import ".." ++import QtQuick ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick + + CollapsibleSection { + title: qsTr("Theme mode") +diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml +index 9a48629c..f2f15ba1 100644 +--- a/modules/controlcenter/appearance/sections/TransparencySection.qml ++++ b/modules/controlcenter/appearance/sections/TransparencySection.qml +@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound + + import ".." + import "../../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + CollapsibleSection { + id: root +@@ -28,7 +28,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +@@ -53,7 +53,7 @@ CollapsibleSection { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true +diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml +index 01d90be7..f3edb731 100644 +--- a/modules/controlcenter/audio/AudioPane.qml ++++ b/modules/controlcenter/audio/AudioPane.qml +@@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root +@@ -23,9 +23,9 @@ Item { + anchors.fill: parent + + leftContent: Component { +- + StyledFlickable { + id: leftAudioFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContent.height + +@@ -38,15 +38,15 @@ Item { + + anchors.left: parent.left + anchors.right: parent.right +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Audio") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -64,15 +64,15 @@ Item { + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Audio.sinks.length) +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + } + } +@@ -93,10 +93,11 @@ Item { + Layout.fillWidth: true + + color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal ++ implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { ++ onClicked: { + Audio.setAudioSink(modelData); + } + } +@@ -107,13 +108,13 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: Audio.sink?.id === modelData.id ? 1 : 0 + } + +@@ -126,8 +127,6 @@ Item { + font.weight: Audio.sink?.id === modelData.id ? 500 : 400 + } + } +- +- implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } +@@ -142,15 +141,15 @@ Item { + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Devices (%1)").arg(Audio.sources.length) +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + } + } +@@ -171,10 +170,11 @@ Item { + Layout.fillWidth: true + + color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal ++ implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- function onClicked(): void { ++ onClicked: { + Audio.setAudioSource(modelData); + } + } +@@ -185,13 +185,13 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: "mic" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: Audio.source?.id === modelData.id ? 1 : 0 + } + +@@ -204,8 +204,6 @@ Item { + font.weight: Audio.source?.id === modelData.id ? 500 : 400 + } + } +- +- implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } +@@ -217,6 +215,7 @@ Item { + rightContent: Component { + StyledFlickable { + id: rightAudioFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height + +@@ -230,7 +229,7 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "volume_up" +@@ -243,19 +242,19 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: qsTr("Volume") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + } + +@@ -265,6 +264,7 @@ Item { + + StyledInputField { + id: outputVolumeInput ++ + Layout.preferredWidth: 70 + validator: IntValidator { + bottom: 0 +@@ -276,15 +276,6 @@ Item { + text = Math.round(Audio.volume * 100).toString(); + } + +- Connections { +- target: Audio +- function onVolumeChanged() { +- if (!outputVolumeInput.hasFocus) { +- outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); +- } +- } +- } +- + onTextEdited: text => { + if (hasFocus) { + const val = parseInt(text); +@@ -300,24 +291,34 @@ Item { + text = Math.round(Audio.volume * 100).toString(); + } + } ++ ++ Connections { ++ function onVolumeChanged() { ++ if (!outputVolumeInput.hasFocus) { ++ outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); ++ } ++ } ++ ++ target: Audio ++ } + } + + StyledText { + text: "%" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + opacity: Audio.muted ? 0.5 : 1 + } + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: muteIcon.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { +- function onClicked(): void { ++ onClicked: { + if (Audio.sink?.audio) { + Audio.sink.audio.muted = !Audio.sink.audio.muted; + } +@@ -336,8 +337,9 @@ Item { + + StyledSlider { + id: outputVolumeSlider ++ + Layout.fillWidth: true +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + + value: Audio.volume + enabled: !Audio.muted +@@ -358,19 +360,19 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: qsTr("Volume") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + } + +@@ -380,6 +382,7 @@ Item { + + StyledInputField { + id: inputVolumeInput ++ + Layout.preferredWidth: 70 + validator: IntValidator { + bottom: 0 +@@ -391,15 +394,6 @@ Item { + text = Math.round(Audio.sourceVolume * 100).toString(); + } + +- Connections { +- target: Audio +- function onSourceVolumeChanged() { +- if (!inputVolumeInput.hasFocus) { +- inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); +- } +- } +- } +- + onTextEdited: text => { + if (hasFocus) { + const val = parseInt(text); +@@ -415,24 +409,34 @@ Item { + text = Math.round(Audio.sourceVolume * 100).toString(); + } + } ++ ++ Connections { ++ function onSourceVolumeChanged() { ++ if (!inputVolumeInput.hasFocus) { ++ inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); ++ } ++ } ++ ++ target: Audio ++ } + } + + StyledText { + text: "%" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + opacity: Audio.sourceMuted ? 0.5 : 1 + } + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: muteInputIcon.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { +- function onClicked(): void { ++ onClicked: { + if (Audio.source?.audio) { + Audio.source.audio.muted = !Audio.source.audio.muted; + } +@@ -451,8 +455,9 @@ Item { + + StyledSlider { + id: inputVolumeSlider ++ + Layout.fillWidth: true +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + + value: Audio.sourceVolume + enabled: !Audio.sourceMuted +@@ -473,11 +478,11 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Repeater { + model: Audio.streams +@@ -488,15 +493,15 @@ Item { + required property int index + + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: "apps" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + fill: 0 + } + +@@ -505,12 +510,13 @@ Item { + elide: Text.ElideRight + maximumLineCount: 1 + text: Audio.getStreamName(modelData) +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + } + + StyledInputField { + id: streamVolumeInput ++ + Layout.preferredWidth: 70 + validator: IntValidator { + bottom: 0 +@@ -522,15 +528,6 @@ Item { + text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); + } + +- Connections { +- target: modelData +- function onAudioChanged() { +- if (!streamVolumeInput.hasFocus && modelData?.audio) { +- streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); +- } +- } +- } +- + onTextEdited: text => { + if (hasFocus) { + const val = parseInt(text); +@@ -546,24 +543,34 @@ Item { + text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); + } + } ++ ++ Connections { ++ function onAudioChanged() { ++ if (!streamVolumeInput.hasFocus && modelData?.audio) { ++ streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); ++ } ++ } ++ ++ target: modelData ++ } + } + + StyledText { + text: "%" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 + } + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: streamMuteIcon.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + StateLayer { +- function onClicked(): void { ++ onClicked: { + Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); + } + } +@@ -580,7 +587,7 @@ Item { + + StyledSlider { + Layout.fillWidth: true +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + + value: Audio.getStreamVolume(modelData) + enabled: !Audio.getStreamMuted(modelData) +@@ -593,12 +600,13 @@ Item { + } + + Connections { +- target: modelData + function onAudioChanged() { + if (modelData?.audio) { + value = modelData.audio.volume; + } + } ++ ++ target: modelData + } + } + } +@@ -609,7 +617,7 @@ Item { + visible: Audio.streams.length === 0 + text: qsTr("No applications currently playing audio") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + horizontalAlignment: Text.AlignHCenter + } + } +diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml +index 7d3b9ca3..b2b9c0f6 100644 +--- a/modules/controlcenter/bluetooth/BtPane.qml ++++ b/modules/controlcenter/bluetooth/BtPane.qml +@@ -3,13 +3,13 @@ pragma ComponentBehavior: Bound + import ".." + import "../components" + import "." ++import QtQuick ++import Quickshell.Bluetooth ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers +-import qs.config +-import Quickshell.Widgets +-import Quickshell.Bluetooth +-import QtQuick ++import qs.components.controls + + SplitPaneWithDetails { + id: root +@@ -53,6 +53,7 @@ SplitPaneWithDetails { + rightSettingsComponent: Component { + StyledFlickable { + id: settingsFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + +diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml +index 52990454..41575226 100644 +--- a/modules/controlcenter/bluetooth/Details.qml ++++ b/modules/controlcenter/bluetooth/Details.qml +@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Bluetooth ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config + import qs.utils +-import Quickshell.Bluetooth +-import QtQuick +-import QtQuick.Layouts + + StyledFlickable { + id: root +@@ -54,12 +54,12 @@ StyledFlickable { + sections: [ + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Connection status") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -70,9 +70,9 @@ StyledFlickable { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: deviceStatus.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { +@@ -81,9 +81,9 @@ StyledFlickable { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.larger ++ spacing: Tokens.spacing.larger + + Toggle { + label: qsTr("Connected") +@@ -113,12 +113,12 @@ StyledFlickable { + }, + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Device properties") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -129,9 +129,9 @@ StyledFlickable { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: deviceProps.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { +@@ -140,19 +140,19 @@ StyledFlickable { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.larger ++ spacing: Tokens.spacing.larger + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Item { + id: renameDevice + + Layout.fillWidth: true +- Layout.rightMargin: Appearance.spacing.small ++ Layout.rightMargin: Tokens.spacing.small + + implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight + +@@ -167,15 +167,13 @@ StyledFlickable { + PropertyChanges { + renameDevice.implicitHeight: deviceNameEdit.implicitHeight + renameLabel.opacity: 0 +- deviceNameEdit.padding: Appearance.padding.normal ++ deviceNameEdit.padding: root.Tokens.padding.normal + } + } + + transitions: Transition { +- AnchorAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard ++ AnchorAnim { ++ type: AnchorAnim.Standard + } + Anim { + properties: "implicitHeight,opacity,padding" +@@ -189,7 +187,7 @@ StyledFlickable { + + text: qsTr("Device name") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledTextField { +@@ -198,7 +196,7 @@ StyledFlickable { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: renameLabel.bottom +- anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal ++ anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.normal + + text: root.device?.name ?? "" + readOnly: !root.session.bt.editingDeviceName +@@ -207,11 +205,11 @@ StyledFlickable { + root.device.name = text; + } + +- leftPadding: Appearance.padding.normal +- rightPadding: Appearance.padding.normal ++ leftPadding: Tokens.padding.normal ++ rightPadding: Tokens.padding.normal + + background: StyledRect { +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + border.width: 2 + border.color: Colours.palette.m3primary + opacity: root.session.bt.editingDeviceName ? 1 : 0 +@@ -233,21 +231,21 @@ StyledFlickable { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.palette.m3secondaryContainer + opacity: root.session.bt.editingDeviceName ? 1 : 0 + scale: root.session.bt.editingDeviceName ? 1 : 0.5 + + StateLayer { +- color: Colours.palette.m3onSecondaryContainer +- disabled: !root.session.bt.editingDeviceName +- +- function onClicked(): void { ++ onClicked: { + root.session.bt.editingDeviceName = false; + deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); + } ++ ++ color: Colours.palette.m3onSecondaryContainer ++ disabled: !root.session.bt.editingDeviceName + } + + MaterialIcon { +@@ -265,29 +263,28 @@ StyledFlickable { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) ++ radius: root.session.bt.editingDeviceName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) + + StateLayer { +- color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface +- +- function onClicked(): void { ++ onClicked: { + root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; + if (root.session.bt.editingDeviceName) + deviceNameEdit.forceActiveFocus(); + else + deviceNameEdit.accepted(); + } ++ ++ color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + } + + MaterialIcon { +@@ -322,12 +319,12 @@ StyledFlickable { + }, + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Device information") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -338,9 +335,9 @@ StyledFlickable { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: deviceInfo.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { +@@ -349,88 +346,83 @@ StyledFlickable { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { + text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") + } + + RowLayout { +- Layout.topMargin: Appearance.spacing.small / 2 +- Layout.fillWidth: true +- Layout.preferredHeight: Appearance.padding.smaller +- spacing: Appearance.spacing.small / 2 ++ id: batteryPercent + +- StyledRect { +- Layout.fillHeight: true +- implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 +- radius: Appearance.rounding.full +- color: Colours.palette.m3primary +- } ++ Layout.topMargin: Tokens.spacing.small / 2 ++ Layout.fillWidth: true ++ Layout.preferredHeight: Tokens.padding.smaller ++ spacing: Tokens.spacing.small / 2 + + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3secondaryContainer + + StyledRect { +- anchors.right: parent.right ++ anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.margins: parent.height * 0.25 + +- implicitWidth: height +- radius: Appearance.rounding.full ++ implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 ++ radius: Tokens.rounding.full + color: Colours.palette.m3primary + } + } + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Dbus path") + } + + StyledText { + text: root.device?.dbusPath ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("MAC address") + } + + StyledText { + text: root.device?.address ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Bonded") + } + + StyledText { + text: root.device?.bonded ? qsTr("Yes") : qsTr("No") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("System name") + } + + StyledText { + text: root.device?.deviceName ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + } +@@ -443,7 +435,7 @@ StyledFlickable { + ColumnLayout { + anchors.right: fabRoot.right + anchors.bottom: fabRoot.top +- anchors.bottomMargin: Appearance.padding.normal ++ anchors.bottomMargin: Tokens.padding.normal + + Repeater { + id: fabMenu +@@ -475,9 +467,9 @@ StyledFlickable { + + Layout.alignment: Qt.AlignRight + +- implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2 ++ implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.larger * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3primaryContainer + + opacity: 0 +@@ -487,7 +479,7 @@ StyledFlickable { + when: root.session.bt.fabMenuOpen + + PropertyChanges { +- fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2 ++ fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.large * 2 + fabMenuItem.opacity: 1 + fabMenuItemInner.opacity: 1 + } +@@ -499,17 +491,16 @@ StyledFlickable { + + SequentialAnimation { + PauseAnimation { +- duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8 ++ duration: (fabMenu.count - 1 - fabMenuItem.index) * Tokens.anim.durations.small / 8 + } + ParallelAnimation { + Anim { + property: "implicitWidth" +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + Anim { + property: "opacity" +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +@@ -519,17 +510,16 @@ StyledFlickable { + + SequentialAnimation { + PauseAnimation { +- duration: fabMenuItem.index * Appearance.anim.durations.small / 8 ++ duration: fabMenuItem.index * Tokens.anim.durations.small / 8 + } + ParallelAnimation { + Anim { + property: "implicitWidth" +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + Anim { + property: "opacity" +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +@@ -537,7 +527,7 @@ StyledFlickable { + ] + + StateLayer { +- function onClicked(): void { ++ onClicked: { + root.session.bt.fabMenuOpen = false; + + const name = fabMenuItem.modelData.name; +@@ -554,7 +544,7 @@ StyledFlickable { + id: fabMenuItemInner + + anchors.centerIn: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + opacity: 0 + + MaterialIcon { +@@ -572,7 +562,7 @@ StyledFlickable { + + Behavior on Layout.preferredWidth { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +@@ -599,7 +589,7 @@ StyledFlickable { + implicitWidth: 64 + implicitHeight: 64 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer + + states: State { +@@ -610,15 +600,14 @@ StyledFlickable { + fabBg.implicitWidth: 48 + fabBg.implicitHeight: 48 + fabBg.radius: 48 / 2 +- fab.font.pointSize: Appearance.font.size.larger ++ fab.font.pointSize: Tokens.font.size.larger + } + } + + transitions: Transition { + Anim { + properties: "implicitWidth,implicitHeight" +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + Anim { + properties: "radius,font.pointSize" +@@ -635,11 +624,11 @@ StyledFlickable { + StateLayer { + id: fabState + +- color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer +- +- function onClicked(): void { ++ onClicked: { + root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; + } ++ ++ color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer + } + + MaterialIcon { +@@ -649,7 +638,7 @@ StyledFlickable { + animate: true + text: root.session.bt.fabMenuOpen ? "close" : "settings" + color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: 1 + } + } +@@ -661,7 +650,7 @@ StyledFlickable { + property alias toggle: toggle + + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml +index 2a2bde93..419410ee 100644 +--- a/modules/controlcenter/bluetooth/DeviceList.qml ++++ b/modules/controlcenter/bluetooth/DeviceList.qml +@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Bluetooth ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import Quickshell.Bluetooth +-import QtQuick +-import QtQuick.Layouts + + DeviceList { + id: root +@@ -32,11 +32,11 @@ DeviceList { + + headerComponent: Component { + RowLayout { +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Bluetooth") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -48,9 +48,9 @@ DeviceList { + toggled: Bluetooth.defaultAdapter?.enabled ?? false + icon: "power" + accent: "Tertiary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Toggle Bluetooth") + + onClicked: { +@@ -64,9 +64,9 @@ DeviceList { + toggled: Bluetooth.defaultAdapter?.discoverable ?? false + icon: root.smallDiscoverable ? "group_search" : "" + label: root.smallDiscoverable ? "" : qsTr("Discoverable") +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Make discoverable") + + onClicked: { +@@ -80,9 +80,9 @@ DeviceList { + toggled: Bluetooth.defaultAdapter?.pairable ?? false + icon: "missing_controller" + label: root.smallPairable ? "" : qsTr("Pairable") +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Make pairable") + + onClicked: { +@@ -96,9 +96,9 @@ DeviceList { + toggled: Bluetooth.defaultAdapter?.discovering ?? false + icon: "bluetooth_searching" + accent: "Secondary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Scan for devices") + + onClicked: { +@@ -112,9 +112,9 @@ DeviceList { + toggled: !root.session.bt.active + icon: "settings" + accent: "Primary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Bluetooth settings") + + onClicked: { +@@ -137,15 +137,15 @@ DeviceList { + readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected + + width: ListView.view ? ListView.view.width : undefined +- implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: deviceInner.implicitHeight + Tokens.padding.normal * 2 + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + StateLayer { + id: stateLayer + +- function onClicked(): void { ++ onClicked: { + if (device.modelData) + root.session.bt.active = device.modelData; + } +@@ -155,15 +155,15 @@ DeviceList { + id: deviceInner + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh + + StyledRect { +@@ -178,7 +178,7 @@ DeviceList { + anchors.centerIn: parent + text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") + color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: device.connected ? 1 : 0 + + Behavior on fill { +@@ -202,7 +202,7 @@ DeviceList { + Layout.fillWidth: true + text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + } + } +@@ -211,9 +211,9 @@ DeviceList { + id: connectBtn + + implicitWidth: implicitHeight +- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) + + CircularIndicator { +@@ -222,10 +222,7 @@ DeviceList { + } + + StateLayer { +- color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface +- disabled: device.loading +- +- function onClicked(): void { ++ onClicked: { + if (device.loading) + return; + +@@ -239,6 +236,9 @@ DeviceList { + } + } + } ++ ++ color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface ++ disabled: device.loading + } + + MaterialIcon { +diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml +index c5472406..1202cc5b 100644 +--- a/modules/controlcenter/bluetooth/Settings.qml ++++ b/modules/controlcenter/bluetooth/Settings.qml +@@ -2,21 +2,21 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Bluetooth ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import Quickshell.Bluetooth +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property Session session + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "bluetooth" +@@ -24,9 +24,9 @@ ColumnLayout { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Adapter status") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -37,9 +37,9 @@ ColumnLayout { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: adapterStatus.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { +@@ -48,9 +48,9 @@ ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.larger ++ spacing: Tokens.spacing.larger + + Toggle { + label: qsTr("Powered") +@@ -85,9 +85,9 @@ ColumnLayout { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Adapter properties") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -98,9 +98,9 @@ ColumnLayout { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: adapterSettings.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { +@@ -109,13 +109,13 @@ ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.larger ++ spacing: Tokens.spacing.larger + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +@@ -127,28 +127,28 @@ ColumnLayout { + + property bool expanded + +- implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 ++ implicitWidth: adapterPicker.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: adapterPicker.implicitHeight + Tokens.padding.smaller * 2 + + StateLayer { +- radius: Appearance.rounding.small +- +- function onClicked(): void { ++ onClicked: { + adapterPickerButton.expanded = !adapterPickerButton.expanded; + } ++ ++ radius: Tokens.rounding.small + } + + RowLayout { + id: adapterPicker + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal +- anchors.topMargin: Appearance.padding.smaller +- anchors.bottomMargin: Appearance.padding.smaller +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.normal ++ anchors.topMargin: Tokens.padding.smaller ++ anchors.bottomMargin: Tokens.padding.smaller ++ spacing: Tokens.spacing.normal + + StyledText { +- Layout.leftMargin: Appearance.padding.small ++ Layout.leftMargin: Tokens.padding.small + text: Bluetooth.defaultAdapter?.name ?? qsTr("None") + } + +@@ -170,8 +170,7 @@ ColumnLayout { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +@@ -185,7 +184,7 @@ ColumnLayout { + implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight + + color: Colours.palette.m3secondaryContainer +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + opacity: adapterPickerButton.expanded ? 1 : 0 + scale: adapterPickerButton.expanded ? 1 : 0.7 + +@@ -207,15 +206,15 @@ ColumnLayout { + required property BluetoothAdapter modelData + + Layout.fillWidth: true +- implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: adapterInner.implicitHeight + Tokens.padding.normal * 2 + + StateLayer { +- disabled: !adapterPickerButton.expanded +- +- function onClicked(): void { ++ onClicked: { + adapterPickerButton.expanded = false; + root.session.bt.currentAdapter = adapter.modelData; + } ++ ++ disabled: !adapterPickerButton.expanded + } + + RowLayout { +@@ -224,12 +223,12 @@ ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +- Layout.leftMargin: Appearance.padding.small ++ Layout.leftMargin: Tokens.padding.small + text: adapter.modelData.name + color: Colours.palette.m3onSecondaryContainer + } +@@ -250,15 +249,13 @@ ColumnLayout { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +@@ -267,7 +264,7 @@ ColumnLayout { + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +@@ -287,13 +284,13 @@ ColumnLayout { + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Item { + id: renameAdapter + + Layout.fillWidth: true +- Layout.rightMargin: Appearance.spacing.small ++ Layout.rightMargin: Tokens.spacing.small + + implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight + +@@ -308,15 +305,13 @@ ColumnLayout { + PropertyChanges { + renameAdapter.implicitHeight: adapterNameEdit.implicitHeight + renameLabel.opacity: 0 +- adapterNameEdit.padding: Appearance.padding.normal ++ adapterNameEdit.padding: Tokens.padding.normal + } + } + + transitions: Transition { +- AnchorAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard ++ AnchorAnim { ++ type: AnchorAnim.Standard + } + Anim { + properties: "implicitHeight,opacity,padding" +@@ -330,7 +325,7 @@ ColumnLayout { + + text: qsTr("Rename adapter (currently does not work)") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledTextField { +@@ -339,7 +334,7 @@ ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: renameLabel.bottom +- anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal ++ anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Tokens.padding.normal + + text: root.session.bt.currentAdapter?.name ?? "" + readOnly: !root.session.bt.editingAdapterName +@@ -347,11 +342,11 @@ ColumnLayout { + root.session.bt.editingAdapterName = false; + } + +- leftPadding: Appearance.padding.normal +- rightPadding: Appearance.padding.normal ++ leftPadding: Tokens.padding.normal ++ rightPadding: Tokens.padding.normal + + background: StyledRect { +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + border.width: 2 + border.color: Colours.palette.m3primary + opacity: root.session.bt.editingAdapterName ? 1 : 0 +@@ -373,21 +368,21 @@ ColumnLayout { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.palette.m3secondaryContainer + opacity: root.session.bt.editingAdapterName ? 1 : 0 + scale: root.session.bt.editingAdapterName ? 1 : 0.5 + + StateLayer { +- color: Colours.palette.m3onSecondaryContainer +- disabled: !root.session.bt.editingAdapterName +- +- function onClicked(): void { ++ onClicked: { + root.session.bt.editingAdapterName = false; + adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); + } ++ ++ color: Colours.palette.m3onSecondaryContainer ++ disabled: !root.session.bt.editingAdapterName + } + + MaterialIcon { +@@ -405,29 +400,28 @@ ColumnLayout { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) ++ radius: root.session.bt.editingAdapterName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) + + StateLayer { +- color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface +- +- function onClicked(): void { ++ onClicked: { + root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; + if (root.session.bt.editingAdapterName) + adapterNameEdit.forceActiveFocus(); + else + adapterNameEdit.accepted(); + } ++ ++ color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + } + + MaterialIcon { +@@ -448,9 +442,9 @@ ColumnLayout { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Adapter information") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -461,9 +455,9 @@ ColumnLayout { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: adapterInfo.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { +@@ -472,9 +466,9 @@ ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { + text: qsTr("Adapter state") +@@ -483,29 +477,29 @@ ColumnLayout { + StyledText { + text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Dbus path") + } + + StyledText { + text: Bluetooth.defaultAdapter?.dbusPath ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Adapter id") + } + + StyledText { + text: Bluetooth.defaultAdapter?.adapterId ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + } +@@ -516,7 +510,7 @@ ColumnLayout { + property alias toggle: toggle + + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml +similarity index 71% +rename from modules/controlcenter/taskbar/ConnectedButtonGroup.qml +rename to modules/controlcenter/components/ConnectedButtonGroup.qml +index 01cd612c..f782838d 100644 +--- a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml ++++ b/modules/controlcenter/components/ConnectedButtonGroup.qml +@@ -1,22 +1,23 @@ + import ".." ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + StyledRect { + id: root + +- property var options: [] // Array of {label: string, propertyName: string, onToggled: function} ++ property var options: [] // Array of {label: string, propertyName: string, onToggled: function, state: bool?} + property var rootItem: null // The root item that contains the properties we want to bind to + property string title: "" // Optional title text ++ property int rows: 1 // Number of rows + + Layout.fillWidth: true +- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal ++ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 ++ radius: Tokens.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + clip: true + +@@ -28,41 +29,48 @@ StyledRect { + id: layout + + anchors.fill: parent +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + StyledText { + visible: root.title !== "" + text: root.title +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + +- RowLayout { +- id: buttonRow ++ GridLayout { ++ id: buttonGrid ++ + Layout.alignment: Qt.AlignHCenter +- spacing: Appearance.spacing.small ++ rowSpacing: Tokens.spacing.small ++ columnSpacing: Tokens.spacing.small ++ rows: root.rows ++ columns: Math.ceil(root.options.length / root.rows) + + Repeater { + id: repeater ++ + model: root.options + + delegate: TextButton { + id: button ++ + required property int index + required property var modelData + +- Layout.fillWidth: true +- text: modelData.label +- + property bool _checked: false + ++ Layout.fillWidth: true ++ text: modelData.label + checked: _checked + toggle: false + type: TextButton.Tonal + + // Create binding in Component.onCompleted + Component.onCompleted: { +- if (root.rootItem && modelData.propertyName) { ++ if (modelData.state !== undefined && modelData.state) { ++ _checked = modelData.state; ++ } else if (root.rootItem && modelData.propertyName) { + const propName = modelData.propertyName; + const rootItem = root.rootItem; + _checked = Qt.binding(function () { +@@ -73,13 +81,13 @@ StyledRect { + + // Match utilities Toggles radius styling + // Each button has full rounding (not connected) since they have spacing +- radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal ++ radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal + + // Match utilities Toggles inactive color + inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Adjust width similar to utilities toggles +- Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) ++ Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) + + onClicked: { + if (modelData.onToggled && root.rootItem && modelData.propertyName) { +@@ -90,15 +98,13 @@ StyledRect { + + Behavior on Layout.preferredWidth { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + Behavior on radius { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml +index a5d06471..075024f6 100644 +--- a/modules/controlcenter/components/DeviceDetails.qml ++++ b/modules/controlcenter/components/DeviceDetails.qml +@@ -1,13 +1,13 @@ + pragma ComponentBehavior: Bound + + import ".." ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root +@@ -30,12 +30,13 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Loader { + id: headerLoader + + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: root.headerComponent + visible: root.headerComponent !== null + } +@@ -44,6 +45,7 @@ Item { + id: topContentLoader + + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: root.topContent + visible: root.topContent !== null + } +@@ -55,6 +57,7 @@ Item { + required property Component modelData + + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: modelData + } + } +@@ -63,6 +66,7 @@ Item { + id: bottomContentLoader + + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: root.bottomContent + visible: root.bottomContent !== null + } +diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml +index 722f9a16..f02cc3c0 100644 +--- a/modules/controlcenter/components/DeviceList.qml ++++ b/modules/controlcenter/components/DeviceList.qml +@@ -1,14 +1,14 @@ + pragma ComponentBehavior: Bound + + import ".." ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root +@@ -23,15 +23,17 @@ ColumnLayout { + property Component headerComponent: null + property Component titleSuffix: null + property bool showHeader: true ++ property alias view: view + + signal itemSelected(var item) + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Loader { + id: headerLoader + + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: root.headerComponent + visible: root.headerComponent !== null && root.showHeader + } +@@ -39,17 +41,18 @@ ColumnLayout { + RowLayout { + Layout.fillWidth: true + Layout.topMargin: root.headerComponent ? 0 : 0 +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + visible: root.title !== "" || root.description !== "" + + StyledText { + visible: root.title !== "" + text: root.title +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + + Loader { ++ asynchronous: true + sourceComponent: root.titleSuffix + visible: root.titleSuffix !== null + } +@@ -59,8 +62,6 @@ ColumnLayout { + } + } + +- property alias view: view +- + StyledText { + visible: root.description !== "" + Layout.fillWidth: true +@@ -77,7 +78,7 @@ ColumnLayout { + model: root.model + delegate: root.delegate + +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + interactive: false + clip: false + } +diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml +index 5d80dbec..489d3c78 100644 +--- a/modules/controlcenter/components/PaneTransition.qml ++++ b/modules/controlcenter/components/PaneTransition.qml +@@ -1,7 +1,7 @@ + pragma ComponentBehavior: Bound + +-import qs.config + import QtQuick ++import Caelestia.Config + + SequentialAnimation { + id: root +@@ -20,9 +20,8 @@ SequentialAnimation { + property: "opacity" + from: root.opacityFrom + to: root.opacityTo +- duration: Appearance.anim.durations.normal / 2 +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ duration: Tokens.anim.durations.normal / 2 ++ easing: Tokens.anim.standardAccel + } + + NumberAnimation { +@@ -30,9 +29,8 @@ SequentialAnimation { + property: "scale" + from: root.scaleFrom + to: root.scaleTo +- duration: Appearance.anim.durations.normal / 2 +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ duration: Tokens.anim.durations.normal / 2 ++ easing: Tokens.anim.standardAccel + } + } + +@@ -53,9 +51,8 @@ SequentialAnimation { + property: "opacity" + from: root.opacityTo + to: root.opacityFrom +- duration: Appearance.anim.durations.normal / 2 +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ duration: Tokens.anim.durations.normal / 2 ++ easing: Tokens.anim.standardDecel + } + + NumberAnimation { +@@ -63,9 +60,8 @@ SequentialAnimation { + property: "scale" + from: root.scaleTo + to: root.scaleFrom +- duration: Appearance.anim.durations.normal / 2 +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ duration: Tokens.anim.durations.normal / 2 ++ easing: Tokens.anim.standardDecel + } + } + } +diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml +new file mode 100644 +index 00000000..8cd493f4 +--- /dev/null ++++ b/modules/controlcenter/components/ReadonlySlider.qml +@@ -0,0 +1,67 @@ ++import ".." ++import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.components.controls ++import qs.services ++ ++ColumnLayout { ++ id: root ++ ++ property string label: "" ++ property real value: 0 ++ property real from: 0 ++ property real to: 100 ++ property string suffix: "" ++ property bool readonly: false ++ ++ spacing: Tokens.spacing.small ++ ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ ++ StyledText { ++ visible: root.label !== "" ++ text: root.label ++ font.pointSize: Tokens.font.size.normal ++ color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ ++ MaterialIcon { ++ visible: root.readonly ++ text: "lock" ++ color: Colours.palette.m3outline ++ font.pointSize: Tokens.font.size.small ++ } ++ ++ StyledText { ++ text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") ++ font.pointSize: Tokens.font.size.normal ++ color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface ++ } ++ } ++ ++ StyledRect { ++ Layout.fillWidth: true ++ implicitHeight: Tokens.padding.normal ++ radius: Tokens.rounding.full ++ color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) ++ opacity: root.readonly ? 0.5 : 1.0 ++ ++ StyledRect { ++ anchors.left: parent.left ++ anchors.top: parent.top ++ anchors.bottom: parent.bottom ++ width: parent.width * ((root.value - root.from) / (root.to - root.from)) ++ radius: parent.radius ++ color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary ++ } ++ } ++} +diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml +index 0dc190c0..c6ccbb67 100644 +--- a/modules/controlcenter/components/SettingsHeader.qml ++++ b/modules/controlcenter/components/SettingsHeader.qml +@@ -1,9 +1,9 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components + + Item { + id: root +@@ -18,19 +18,19 @@ Item { + id: column + + anchors.centerIn: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: root.icon +- font.pointSize: Appearance.font.size.extraLarge * 3 ++ font.pointSize: Tokens.font.size.extraLarge * 3 + font.bold: true + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: root.title +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.bold: true + } + } +diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml +index 11b3f70d..73fd00ae 100644 +--- a/modules/controlcenter/components/SliderInput.qml ++++ b/modules/controlcenter/components/SliderInput.qml +@@ -1,12 +1,12 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root +@@ -21,6 +21,9 @@ ColumnLayout { + property int decimals: 1 // Number of decimal places to show (default: 1) + property var formatValueFunction: null // Optional custom format function + property var parseValueFunction: null // Optional custom parse function ++ property bool _initialized: false ++ ++ signal valueModified(real newValue) + + function formatValue(val: real): string { + if (formatValueFunction) { +@@ -49,11 +52,7 @@ ColumnLayout { + return parseFloat(text); + } + +- signal valueModified(real newValue) +- +- property bool _initialized: false +- +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Component.onCompleted: { + // Set initialized flag after a brief delay to allow component to fully load +@@ -62,14 +61,22 @@ ColumnLayout { + }); + } + ++ // Update input field when value changes externally (slider is already bound) ++ onValueChanged: { ++ // Only update if component is initialized to avoid issues during creation ++ if (root._initialized && !inputField.hasFocus) { ++ inputField.text = root.formatValue(root.value); ++ } ++ } ++ + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + visible: root.label !== "" + text: root.label +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + Item { +@@ -78,6 +85,7 @@ ColumnLayout { + + StyledInputField { + id: inputField ++ + Layout.preferredWidth: 70 + validator: root.validator + +@@ -130,7 +138,7 @@ ColumnLayout { + visible: root.suffix !== "" + text: root.suffix + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + } + +@@ -138,20 +146,12 @@ ColumnLayout { + id: slider + + Layout.fillWidth: true +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + + from: root.from + to: root.to + stepSize: root.stepSize + +- // Use Binding to allow slider to move freely during dragging +- Binding { +- target: slider +- property: "value" +- value: root.value +- when: !slider.pressed +- } +- + onValueChanged: { + // Update input field text in real-time as slider moves during dragging + // Always update when slider value changes (during dragging or external updates) +@@ -168,13 +168,13 @@ ColumnLayout { + inputField.text = root.formatValue(newValue); + } + } +- } + +- // Update input field when value changes externally (slider is already bound) +- onValueChanged: { +- // Only update if component is initialized to avoid issues during creation +- if (root._initialized && !inputField.hasFocus) { +- inputField.text = root.formatValue(root.value); ++ // Use Binding to allow slider to move freely during dragging ++ Binding { ++ target: slider ++ property: "value" ++ value: root.value ++ when: !slider.pressed + } + } + } +diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml +index 89504a0b..64c7c9fc 100644 +--- a/modules/controlcenter/components/SplitPaneLayout.qml ++++ b/modules/controlcenter/components/SplitPaneLayout.qml +@@ -1,28 +1,26 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.components.effects +-import qs.config +-import Quickshell.Widgets + import QtQuick + import QtQuick.Layouts ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components ++import qs.components.effects + + RowLayout { + id: root + +- spacing: 0 +- + property Component leftContent: null + property Component rightContent: null +- + property real leftWidthRatio: 0.4 + property int leftMinimumWidth: 420 + property var leftLoaderProperties: ({}) + property var rightLoaderProperties: ({}) +- + property alias leftLoader: leftLoader + property alias rightLoader: rightLoader + ++ spacing: 0 ++ + Item { + id: leftPane + +@@ -34,9 +32,9 @@ RowLayout { + id: leftClippingRect + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + anchors.leftMargin: 0 +- anchors.rightMargin: Appearance.padding.normal / 2 ++ anchors.rightMargin: Tokens.padding.normal / 2 + + radius: leftBorder.innerRadius + color: "transparent" +@@ -45,10 +43,11 @@ RowLayout { + id: leftLoader + + anchors.fill: parent +- anchors.margins: Appearance.padding.large + Appearance.padding.normal +- anchors.leftMargin: Appearance.padding.large +- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 ++ anchors.margins: Tokens.padding.large + Tokens.padding.normal ++ anchors.leftMargin: Tokens.padding.large ++ anchors.rightMargin: Tokens.padding.large + Tokens.padding.normal / 2 + ++ asynchronous: true + sourceComponent: root.leftContent + + Component.onCompleted: { +@@ -63,7 +62,7 @@ RowLayout { + id: leftBorder + + leftThickness: 0 +- rightThickness: Appearance.padding.normal / 2 ++ rightThickness: Tokens.padding.normal / 2 + } + } + +@@ -77,9 +76,9 @@ RowLayout { + id: rightClippingRect + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + anchors.leftMargin: 0 +- anchors.rightMargin: Appearance.padding.normal / 2 ++ anchors.rightMargin: Tokens.padding.normal / 2 + + radius: rightBorder.innerRadius + color: "transparent" +@@ -88,8 +87,9 @@ RowLayout { + id: rightLoader + + anchors.fill: parent +- anchors.margins: Appearance.padding.large * 2 ++ anchors.margins: Tokens.padding.large * 2 + ++ asynchronous: true + sourceComponent: root.rightContent + + Component.onCompleted: { +@@ -103,7 +103,7 @@ RowLayout { + InnerBorder { + id: rightBorder + +- leftThickness: Appearance.padding.normal / 2 ++ leftThickness: Tokens.padding.normal / 2 + } + } + } +diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml +index ce8c9d07..ad1c444d 100644 +--- a/modules/controlcenter/components/SplitPaneWithDetails.qml ++++ b/modules/controlcenter/components/SplitPaneWithDetails.qml +@@ -1,13 +1,13 @@ + pragma ComponentBehavior: Bound + + import ".." +-import qs.components +-import qs.components.effects +-import qs.components.containers +-import qs.config +-import Quickshell.Widgets + import QtQuick + import QtQuick.Layouts ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components ++import qs.components.containers ++import qs.components.effects + + Item { + id: root +@@ -48,11 +48,17 @@ Item { + nextComponent = targetComponent; + } + ++ onPaneChanged: { ++ nextComponent = getComponentForPane(); ++ paneId = root.paneIdGenerator(pane); ++ } ++ + Loader { + id: rightLoader + + anchors.fill: parent + ++ asynchronous: true + opacity: 1 + scale: 1 + transformOrigin: Item.Center +@@ -73,11 +79,6 @@ Item { + ] + } + } +- +- onPaneChanged: { +- nextComponent = getComponentForPane(); +- paneId = root.paneIdGenerator(pane); +- } + } + } + } +@@ -86,6 +87,7 @@ Item { + id: overlayLoader + + anchors.fill: parent ++ asynchronous: true + z: 1000 + sourceComponent: root.overlayComponent + active: root.overlayComponent !== null +diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml +index ed6bb40a..44a615c6 100644 +--- a/modules/controlcenter/components/WallpaperGrid.qml ++++ b/modules/controlcenter/components/WallpaperGrid.qml +@@ -1,25 +1,25 @@ + pragma ComponentBehavior: Bound + + import ".." ++import QtQuick ++import Caelestia.Config ++import Caelestia.Models + import qs.components + import qs.components.controls + import qs.components.effects + import qs.components.images + import qs.services +-import qs.config +-import Caelestia.Models +-import QtQuick + + GridView { + id: root + + required property Session session + +- readonly property int minCellWidth: 200 + Appearance.spacing.normal ++ readonly property int minCellWidth: 200 + Tokens.spacing.normal + readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) + + cellWidth: width / columnsCount +- cellHeight: 140 + Appearance.spacing.normal ++ cellHeight: 140 + Tokens.spacing.normal + + model: Wallpapers.list + +@@ -32,25 +32,24 @@ GridView { + delegate: Item { + required property var modelData + required property int index ++ readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent ++ readonly property real itemMargin: Tokens.spacing.normal / 2 ++ readonly property real itemRadius: Tokens.rounding.normal + + width: root.cellWidth + height: root.cellHeight + +- readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent +- readonly property real itemMargin: Appearance.spacing.normal / 2 +- readonly property real itemRadius: Appearance.rounding.normal +- + StateLayer { ++ onClicked: { ++ Wallpapers.setWallpaper(modelData.path); ++ } ++ + anchors.fill: parent + anchors.leftMargin: itemMargin + anchors.rightMargin: itemMargin + anchors.topMargin: itemMargin + anchors.bottomMargin: itemMargin + radius: itemRadius +- +- function onClicked(): void { +- Wallpapers.setWallpaper(modelData.path); +- } + } + + StyledClippingRect { +@@ -117,6 +116,7 @@ GridView { + id: fallbackTimer + + property bool triggered: false ++ + interval: 800 + running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null + onTriggered: triggered = true +@@ -130,7 +130,7 @@ GridView { + anchors.right: parent.right + anchors.bottom: parent.bottom + +- implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 ++ implicitHeight: filenameText.implicitHeight + Tokens.padding.normal * 1.5 + radius: 0 + + gradient: Gradient { +@@ -154,16 +154,16 @@ GridView { + + opacity: 0 + ++ Component.onCompleted: { ++ opacity = 1; ++ } ++ + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } + } +- +- Component.onCompleted: { +- opacity = 1; +- } + } + } + +@@ -190,26 +190,27 @@ GridView { + MaterialIcon { + anchors.right: parent.right + anchors.top: parent.top +- anchors.margins: Appearance.padding.small ++ anchors.margins: Tokens.padding.small + + visible: isCurrent + text: "check_circle" + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + + StyledText { + id: filenameText ++ + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom +- anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 +- anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 +- anchors.bottomMargin: Appearance.padding.normal ++ anchors.leftMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 ++ anchors.rightMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 ++ anchors.bottomMargin: Tokens.padding.normal + + text: modelData.name +- font.pointSize: Appearance.font.size.smaller ++ font.pointSize: Tokens.font.size.smaller + font.weight: 500 + color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface + elide: Text.ElideMiddle +@@ -218,16 +219,16 @@ GridView { + + opacity: 0 + ++ Component.onCompleted: { ++ opacity = 1; ++ } ++ + Behavior on opacity { + NumberAnimation { + duration: 1000 + easing.type: Easing.OutCubic + } + } +- +- Component.onCompleted: { +- opacity = 1; +- } + } + } + } +diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml +new file mode 100644 +index 00000000..e9b6e568 +--- /dev/null ++++ b/modules/controlcenter/dashboard/DashboardPane.qml +@@ -0,0 +1,527 @@ ++pragma ComponentBehavior: Bound ++ ++import ".." ++import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Services.UPower ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components ++import qs.components.containers ++import qs.components.controls ++import qs.components.effects ++import qs.services ++import qs.utils ++ ++Item { ++ id: root ++ ++ required property Session session ++ property string activeSection: "general" ++ ++ property bool enabled: Config.dashboard.enabled ?? true ++ property bool showOnHover: Config.dashboard.showOnHover ?? true ++ property int mediaUpdateInterval: GlobalConfig.dashboard.mediaUpdateInterval ?? 1000 ++ property int resourceUpdateInterval: GlobalConfig.dashboard.resourceUpdateInterval ?? 1000 ++ property int dragThreshold: Config.dashboard.dragThreshold ?? 50 ++ ++ property bool showDashboard: Config.dashboard.showDashboard ?? true ++ property bool showMedia: Config.dashboard.showMedia ?? true ++ property bool showPerformance: Config.dashboard.showPerformance ?? true ++ property bool showWeather: Config.dashboard.showWeather ?? true ++ ++ property bool showBattery: Config.dashboard.performance.showBattery ?? false ++ property bool showGpu: Config.dashboard.performance.showGpu ?? true ++ property bool showCpu: Config.dashboard.performance.showCpu ?? true ++ property bool showMemory: Config.dashboard.performance.showMemory ?? true ++ property bool showStorage: Config.dashboard.performance.showStorage ?? true ++ property bool showNetwork: Config.dashboard.performance.showNetwork ?? true ++ ++ readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" ++ readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery ++ readonly property var sections: [ ++ { ++ id: "general", ++ title: qsTr("General"), ++ description: qsTr("Visibility and timing"), ++ icon: "dashboard" ++ }, ++ { ++ id: "tabs", ++ title: qsTr("Tabs"), ++ description: qsTr("Dashboard pages"), ++ icon: "tab" ++ }, ++ { ++ id: "performance", ++ title: qsTr("Performance"), ++ description: qsTr("Resource cards"), ++ icon: "monitoring" ++ } ++ ] ++ ++ function componentForSection(sectionId) { ++ switch (sectionId) { ++ case "tabs": ++ return tabsComponent; ++ case "performance": ++ return performanceComponent; ++ case "general": ++ default: ++ return generalComponent; ++ } ++ } ++ ++ function performanceOptions() { ++ const options = []; ++ ++ if (root.batteryAvailable) { ++ options.push({ ++ label: qsTr("Battery"), ++ propertyName: "showBattery", ++ onToggled: function (checked) { ++ root.showBattery = checked; ++ root.saveConfig(); ++ } ++ }); ++ } ++ ++ if (root.gpuAvailable) { ++ options.push({ ++ label: qsTr("GPU"), ++ propertyName: "showGpu", ++ onToggled: function (checked) { ++ root.showGpu = checked; ++ root.saveConfig(); ++ } ++ }); ++ } ++ ++ options.push({ ++ label: qsTr("CPU"), ++ propertyName: "showCpu", ++ onToggled: function (checked) { ++ root.showCpu = checked; ++ root.saveConfig(); ++ } ++ }, { ++ label: qsTr("Memory"), ++ propertyName: "showMemory", ++ onToggled: function (checked) { ++ root.showMemory = checked; ++ root.saveConfig(); ++ } ++ }, { ++ label: qsTr("Storage"), ++ propertyName: "showStorage", ++ onToggled: function (checked) { ++ root.showStorage = checked; ++ root.saveConfig(); ++ } ++ }, { ++ label: qsTr("Network"), ++ propertyName: "showNetwork", ++ onToggled: function (checked) { ++ root.showNetwork = checked; ++ root.saveConfig(); ++ } ++ }); ++ ++ return options; ++ } ++ ++ function saveConfig() { ++ GlobalConfig.dashboard.enabled = root.enabled; ++ GlobalConfig.dashboard.showOnHover = root.showOnHover; ++ GlobalConfig.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; ++ GlobalConfig.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; ++ GlobalConfig.dashboard.dragThreshold = root.dragThreshold; ++ GlobalConfig.dashboard.showDashboard = root.showDashboard; ++ GlobalConfig.dashboard.showMedia = root.showMedia; ++ GlobalConfig.dashboard.showPerformance = root.showPerformance; ++ GlobalConfig.dashboard.showWeather = root.showWeather; ++ GlobalConfig.dashboard.performance.showBattery = root.showBattery; ++ GlobalConfig.dashboard.performance.showGpu = root.showGpu; ++ GlobalConfig.dashboard.performance.showCpu = root.showCpu; ++ GlobalConfig.dashboard.performance.showMemory = root.showMemory; ++ GlobalConfig.dashboard.performance.showStorage = root.showStorage; ++ GlobalConfig.dashboard.performance.showNetwork = root.showNetwork; ++ } ++ ++ anchors.fill: parent ++ ++ SplitPaneLayout { ++ anchors.fill: parent ++ leftWidthRatio: 0.32 ++ leftMinimumWidth: 300 ++ ++ leftContent: Component { ++ StyledFlickable { ++ id: leftFlickable ++ ++ flickableDirection: Flickable.VerticalFlick ++ contentHeight: leftContentLayout.height ++ ++ StyledScrollBar.vertical: StyledScrollBar { ++ flickable: leftFlickable ++ } ++ ++ ColumnLayout { ++ id: leftContentLayout ++ ++ anchors.left: parent.left ++ anchors.right: parent.right ++ spacing: Tokens.spacing.normal ++ ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.smaller ++ ++ StyledText { ++ text: qsTr("Dashboard") ++ font.pointSize: Tokens.font.size.large ++ font.weight: 500 ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ } ++ ++ Repeater { ++ model: root.sections ++ ++ delegate: SectionNavButton { ++ required property var modelData ++ ++ Layout.fillWidth: true ++ section: modelData ++ active: root.activeSection === modelData.id ++ onClicked: root.activeSection = modelData.id ++ } ++ } ++ } ++ } ++ } ++ ++ rightContent: Component { ++ Item { ++ id: rightPaneItem ++ ++ property string paneId: root.activeSection ++ property Component targetComponent: root.componentForSection(root.activeSection) ++ property Component nextComponent: root.componentForSection(root.activeSection) ++ ++ onPaneIdChanged: { ++ nextComponent = root.componentForSection(root.activeSection); ++ } ++ ++ Loader { ++ id: rightLoader ++ ++ anchors.fill: parent ++ asynchronous: true ++ opacity: 1 ++ scale: 1 ++ transformOrigin: Item.Center ++ sourceComponent: rightPaneItem.targetComponent ++ } ++ ++ Behavior on paneId { ++ PaneTransition { ++ target: rightLoader ++ propertyActions: [ ++ PropertyAction { ++ target: rightPaneItem ++ property: "targetComponent" ++ value: rightPaneItem.nextComponent ++ } ++ ] ++ } ++ } ++ } ++ } ++ } ++ ++ Component { ++ id: generalComponent ++ ++ SectionPage { ++ title: qsTr("General") ++ subtitle: qsTr("Control dashboard visibility and interaction timing.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SwitchRow { ++ label: qsTr("Enabled") ++ checked: root.enabled ++ onToggled: checked => { ++ root.enabled = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ label: qsTr("Show on hover") ++ checked: root.showOnHover ++ onToggled: checked => { ++ root.showOnHover = checked; ++ root.saveConfig(); ++ } ++ } ++ } ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ contentSpacing: Tokens.spacing.normal ++ ++ SliderInput { ++ Layout.fillWidth: true ++ label: qsTr("Media update interval") ++ value: root.mediaUpdateInterval ++ from: 100 ++ to: 10000 ++ stepSize: 100 ++ suffix: "ms" ++ validator: IntValidator { ++ bottom: 100 ++ top: 10000 ++ } ++ formatValueFunction: val => Math.round(val).toString() ++ parseValueFunction: text => parseInt(text) ++ onValueModified: newValue => { ++ root.mediaUpdateInterval = Math.round(newValue); ++ root.saveConfig(); ++ } ++ } ++ ++ SliderInput { ++ Layout.fillWidth: true ++ label: qsTr("Drag threshold") ++ value: root.dragThreshold ++ from: 0 ++ to: 100 ++ suffix: "px" ++ validator: IntValidator { ++ bottom: 0 ++ top: 100 ++ } ++ formatValueFunction: val => Math.round(val).toString() ++ parseValueFunction: text => parseInt(text) ++ onValueModified: newValue => { ++ root.dragThreshold = Math.round(newValue); ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } ++ ++ Component { ++ id: tabsComponent ++ ++ SectionPage { ++ title: qsTr("Tabs") ++ subtitle: qsTr("Choose which dashboard pages are available.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ ConnectedButtonGroup { ++ rootItem: root ++ rows: 2 ++ options: [ ++ { ++ label: qsTr("Dashboard"), ++ propertyName: "showDashboard", ++ onToggled: function (checked) { ++ root.showDashboard = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Media"), ++ propertyName: "showMedia", ++ onToggled: function (checked) { ++ root.showMedia = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Performance"), ++ propertyName: "showPerformance", ++ onToggled: function (checked) { ++ root.showPerformance = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Weather"), ++ propertyName: "showWeather", ++ onToggled: function (checked) { ++ root.showWeather = checked; ++ root.saveConfig(); ++ } ++ } ++ ] ++ } ++ } ++ } ++ } ++ ++ Component { ++ id: performanceComponent ++ ++ SectionPage { ++ title: qsTr("Performance") ++ subtitle: qsTr("Configure which resource cards the dashboard shows.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ ConnectedButtonGroup { ++ rootItem: root ++ options: root.performanceOptions() ++ } ++ } ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ contentSpacing: Tokens.spacing.normal ++ ++ SliderInput { ++ Layout.fillWidth: true ++ label: qsTr("Resource update interval") ++ value: root.resourceUpdateInterval ++ from: 100 ++ to: 10000 ++ stepSize: 100 ++ suffix: "ms" ++ validator: IntValidator { ++ bottom: 100 ++ top: 10000 ++ } ++ formatValueFunction: val => Math.round(val).toString() ++ parseValueFunction: text => parseInt(text) ++ onValueModified: newValue => { ++ root.resourceUpdateInterval = Math.round(newValue); ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } ++ ++ component SectionPage: StyledFlickable { ++ id: sectionPage ++ ++ required property string title ++ property string subtitle: "" ++ default property alias contentItems: contentLayout.data ++ ++ flickableDirection: Flickable.VerticalFlick ++ contentHeight: contentLayout.height ++ ++ StyledScrollBar.vertical: StyledScrollBar { ++ flickable: sectionPage ++ } ++ ++ ColumnLayout { ++ id: contentLayout ++ ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.top: parent.top ++ spacing: Tokens.spacing.normal ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: sectionPage.title ++ font.pointSize: Tokens.font.size.extraLarge ++ font.weight: 600 ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ Layout.bottomMargin: Tokens.spacing.small ++ text: sectionPage.subtitle ++ color: Colours.palette.m3outline ++ visible: text.length > 0 ++ wrapMode: Text.WordWrap ++ } ++ } ++ } ++ ++ component SectionNavButton: StyledRect { ++ id: navButton ++ ++ required property var section ++ property bool active: false ++ ++ signal clicked ++ ++ implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 ++ color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" ++ radius: Tokens.rounding.normal ++ ++ Behavior on color { ++ CAnim {} ++ } ++ ++ StateLayer { ++ onClicked: navButton.clicked() ++ } ++ ++ RowLayout { ++ id: navRow ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.normal ++ ++ MaterialIcon { ++ Layout.alignment: Qt.AlignVCenter ++ text: navButton.section.icon ++ fill: navButton.active ? 1 : 0 ++ color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant ++ font.pointSize: Tokens.font.size.large ++ } ++ ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: 0 ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: navButton.section.title ++ font.weight: navButton.active ? 500 : 400 ++ elide: Text.ElideRight ++ maximumLineCount: 1 ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: navButton.section.description ++ color: Colours.palette.m3outline ++ font.pointSize: Tokens.font.size.small ++ elide: Text.ElideRight ++ maximumLineCount: 1 ++ } ++ } ++ ++ MaterialIcon { ++ Layout.alignment: Qt.AlignVCenter ++ text: "chevron_right" ++ opacity: navButton.active ? 1 : 0 ++ color: Colours.palette.m3primary ++ } ++ } ++ } ++} +diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml +new file mode 100644 +index 00000000..be67eab2 +--- /dev/null ++++ b/modules/controlcenter/dashboard/GeneralSection.qml +@@ -0,0 +1,128 @@ ++import ".." ++import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.components.controls ++import qs.services ++ ++SectionContainer { ++ id: root ++ ++ required property var rootItem ++ ++ Layout.fillWidth: true ++ alignTop: true ++ ++ StyledText { ++ text: qsTr("General Settings") ++ font.pointSize: Tokens.font.size.normal ++ } ++ ++ SwitchRow { ++ label: qsTr("Enabled") ++ checked: root.rootItem.enabled ++ onToggled: checked => { ++ root.rootItem.enabled = checked; ++ root.rootItem.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ label: qsTr("Show on hover") ++ checked: root.rootItem.showOnHover ++ onToggled: checked => { ++ root.rootItem.showOnHover = checked; ++ root.rootItem.saveConfig(); ++ } ++ } ++ ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Show Dashboard tab") ++ checked: root.rootItem.showDashboard ++ onToggled: checked => { ++ root.rootItem.showDashboard = checked; ++ root.rootItem.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Show Media tab") ++ checked: root.rootItem.showMedia ++ onToggled: checked => { ++ root.rootItem.showMedia = checked; ++ root.rootItem.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Show Performance tab") ++ checked: root.rootItem.showPerformance ++ onToggled: checked => { ++ root.rootItem.showPerformance = checked; ++ root.rootItem.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Show Weather tab") ++ checked: root.rootItem.showWeather ++ onToggled: checked => { ++ root.rootItem.showWeather = checked; ++ root.rootItem.saveConfig(); ++ } ++ } ++ } ++ ++ SliderInput { ++ Layout.fillWidth: true ++ ++ label: qsTr("Media update interval") ++ value: root.rootItem.mediaUpdateInterval ++ from: 100 ++ to: 10000 ++ stepSize: 100 ++ suffix: "ms" ++ validator: IntValidator { ++ bottom: 100 ++ top: 10000 ++ } ++ formatValueFunction: val => Math.round(val).toString() ++ parseValueFunction: text => parseInt(text) ++ ++ onValueModified: newValue => { ++ root.rootItem.mediaUpdateInterval = Math.round(newValue); ++ root.rootItem.saveConfig(); ++ } ++ } ++ ++ SliderInput { ++ Layout.fillWidth: true ++ ++ label: qsTr("Drag threshold") ++ value: root.rootItem.dragThreshold ++ from: 0 ++ to: 100 ++ suffix: "px" ++ validator: IntValidator { ++ bottom: 0 ++ top: 100 ++ } ++ formatValueFunction: val => Math.round(val).toString() ++ parseValueFunction: text => parseInt(text) ++ ++ onValueModified: newValue => { ++ root.rootItem.dragThreshold = Math.round(newValue); ++ root.rootItem.saveConfig(); ++ } ++ } ++} +diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml +new file mode 100644 +index 00000000..ea6c68b3 +--- /dev/null ++++ b/modules/controlcenter/dashboard/PerformanceSection.qml +@@ -0,0 +1,106 @@ ++import ".." ++import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Services.UPower ++import Caelestia.Config ++import qs.components ++import qs.components.controls ++import qs.services ++ ++SectionContainer { ++ id: root ++ ++ required property var rootItem ++ // GPU toggle is hidden when gpuType is "NONE" (no GPU data available) ++ readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" ++ // Battery toggle is hidden when no laptop battery is present ++ readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery ++ ++ Layout.fillWidth: true ++ alignTop: true ++ ++ StyledText { ++ text: qsTr("Performance Resources") ++ font.pointSize: Tokens.font.size.normal ++ } ++ ++ ConnectedButtonGroup { ++ rootItem: root.rootItem ++ options: { ++ let opts = []; ++ if (root.batteryAvailable) ++ opts.push({ ++ "label": qsTr("Battery"), ++ "propertyName": "showBattery", ++ "onToggled": function (checked) { ++ root.rootItem.showBattery = checked; ++ root.rootItem.saveConfig(); ++ } ++ }); ++ ++ if (root.gpuAvailable) ++ opts.push({ ++ "label": qsTr("GPU"), ++ "propertyName": "showGpu", ++ "onToggled": function (checked) { ++ root.rootItem.showGpu = checked; ++ root.rootItem.saveConfig(); ++ } ++ }); ++ ++ opts.push({ ++ "label": qsTr("CPU"), ++ "propertyName": "showCpu", ++ "onToggled": function (checked) { ++ root.rootItem.showCpu = checked; ++ root.rootItem.saveConfig(); ++ } ++ }, { ++ "label": qsTr("Memory"), ++ "propertyName": "showMemory", ++ "onToggled": function (checked) { ++ root.rootItem.showMemory = checked; ++ root.rootItem.saveConfig(); ++ } ++ }, { ++ "label": qsTr("Storage"), ++ "propertyName": "showStorage", ++ "onToggled": function (checked) { ++ root.rootItem.showStorage = checked; ++ root.rootItem.saveConfig(); ++ } ++ }, { ++ "label": qsTr("Network"), ++ "propertyName": "showNetwork", ++ "onToggled": function (checked) { ++ root.rootItem.showNetwork = checked; ++ root.rootItem.saveConfig(); ++ } ++ }); ++ return opts; ++ } ++ } ++ ++ SliderInput { ++ Layout.fillWidth: true ++ ++ label: qsTr("Resource update interval") ++ value: root.rootItem.resourceUpdateInterval ++ from: 100 ++ to: 10000 ++ stepSize: 100 ++ suffix: "ms" ++ validator: IntValidator { ++ bottom: 100 ++ top: 10000 ++ } ++ formatValueFunction: val => Math.round(val).toString() ++ parseValueFunction: text => parseInt(text) ++ ++ onValueModified: newValue => { ++ root.rootItem.resourceUpdateInterval = Math.round(newValue); ++ root.rootItem.saveConfig(); ++ } ++ } ++} +diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml +index 0dd464f1..f2372dbe 100644 +--- a/modules/controlcenter/launcher/LauncherPane.qml ++++ b/modules/controlcenter/launcher/LauncherPane.qml +@@ -3,19 +3,19 @@ pragma ComponentBehavior: Bound + import ".." + import "../components" + import "../../launcher/services" ++import "../../../utils/scripts/fuzzysort.js" as Fuzzy ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config + import qs.utils +-import Caelestia +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts +-import "../../../utils/scripts/fuzzysort.js" as Fuzzy + + Item { + id: root +@@ -24,35 +24,21 @@ Item { + + property var selectedApp: root.session.launcher.active + property bool hideFromLauncherChecked: false +- +- anchors.fill: parent +- +- onSelectedAppChanged: { +- root.session.launcher.active = root.selectedApp; +- updateToggleState(); +- } +- +- Connections { +- target: root.session.launcher +- function onActiveChanged() { +- root.selectedApp = root.session.launcher.active; +- updateToggleState(); +- } +- } ++ property bool favouriteChecked: false ++ property string searchText: "" ++ property list filteredApps: [] + + function updateToggleState() { + if (!root.selectedApp) { + root.hideFromLauncherChecked = false; ++ root.favouriteChecked = false; + return; + } + + const appId = root.selectedApp.id || root.selectedApp.entry?.id; + +- if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { +- root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); +- } else { +- root.hideFromLauncherChecked = false; +- } ++ root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); ++ root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); + } + + function saveHiddenApps(isHidden) { +@@ -62,7 +48,7 @@ Item { + + const appId = root.selectedApp.id || root.selectedApp.entry?.id; + +- const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; ++ const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; + + if (isHidden) { + if (!hiddenApps.includes(appId)) { +@@ -75,19 +61,9 @@ Item { + } + } + +- Config.launcher.hiddenApps = hiddenApps; +- Config.save(); +- } +- +- AppDb { +- id: allAppsDb +- +- path: `${Paths.state}/apps.sqlite` +- entries: DesktopEntries.applications.values ++ GlobalConfig.launcher.hiddenApps = hiddenApps; + } + +- property string searchText: "" +- + function filterApps(search: string): list { + if (!search || search.trim() === "") { + const apps = []; +@@ -120,12 +96,17 @@ Item { + return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); + } + +- property list filteredApps: [] +- + function updateFilteredApps() { + filteredApps = filterApps(searchText); + } + ++ anchors.fill: parent ++ ++ onSelectedAppChanged: { ++ root.session.launcher.active = root.selectedApp; ++ updateToggleState(); ++ } ++ + onSearchTextChanged: { + updateFilteredApps(); + } +@@ -135,29 +116,47 @@ Item { + } + + Connections { +- target: allAppsDb ++ function onActiveChanged() { ++ root.selectedApp = root.session.launcher.active; ++ updateToggleState(); ++ } ++ ++ target: root.session.launcher ++ } ++ ++ AppDb { ++ id: allAppsDb ++ ++ path: `${Paths.state}/apps.sqlite` ++ favouriteApps: GlobalConfig.launcher.favouriteApps ++ entries: DesktopEntries.applications.values ++ } ++ ++ Connections { + function onAppsChanged() { + updateFilteredApps(); + } ++ ++ target: allAppsDb + } + + SplitPaneLayout { + anchors.fill: parent + + leftContent: Component { +- + ColumnLayout { + id: leftLauncherLayout ++ + anchors.fill: parent + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + RowLayout { +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Launcher") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -169,9 +168,9 @@ Item { + toggled: !root.session.launcher.active + icon: "settings" + accent: "Primary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Launcher settings") + + onClicked: { +@@ -187,9 +186,9 @@ Item { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + } + +@@ -200,11 +199,11 @@ Item { + + StyledRect { + Layout.fillWidth: true +- Layout.topMargin: Appearance.spacing.normal +- Layout.bottomMargin: Appearance.spacing.small ++ Layout.topMargin: Tokens.spacing.normal ++ Layout.bottomMargin: Tokens.spacing.small + + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) + +@@ -213,7 +212,7 @@ Item { + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left +- anchors.leftMargin: Appearance.padding.normal ++ anchors.leftMargin: Tokens.padding.normal + + text: "search" + color: Colours.palette.m3onSurfaceVariant +@@ -224,11 +223,11 @@ Item { + + anchors.left: searchIcon.right + anchors.right: clearIcon.left +- anchors.leftMargin: Appearance.spacing.small +- anchors.rightMargin: Appearance.spacing.small ++ anchors.leftMargin: Tokens.spacing.small ++ anchors.rightMargin: Tokens.spacing.small + +- topPadding: Appearance.padding.normal +- bottomPadding: Appearance.padding.normal ++ topPadding: Tokens.padding.normal ++ bottomPadding: Tokens.padding.normal + + placeholderText: qsTr("Search applications...") + +@@ -242,7 +241,7 @@ Item { + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right +- anchors.rightMargin: Appearance.padding.normal ++ anchors.rightMargin: Tokens.padding.normal + + width: searchField.text ? implicitWidth : implicitWidth / 2 + opacity: { +@@ -270,13 +269,13 @@ Item { + + Behavior on width { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +@@ -284,8 +283,10 @@ Item { + + Loader { + id: appsListLoader ++ + Layout.fillWidth: true + Layout.fillHeight: true ++ asynchronous: true + active: true + + sourceComponent: StyledListView { +@@ -295,7 +296,7 @@ Item { + Layout.fillHeight: true + + model: root.filteredApps +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + clip: true + + StyledScrollBar.vertical: StyledScrollBar { +@@ -305,15 +306,20 @@ Item { + delegate: StyledRect { + required property var modelData + +- width: parent ? parent.width : 0 +- + readonly property bool isSelected: root.selectedApp === modelData + ++ width: parent ? parent.width : 0 ++ implicitHeight: 40 ++ + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + opacity: 0 + ++ Component.onCompleted: { ++ opacity = 1; ++ } ++ + Behavior on opacity { + NumberAnimation { + duration: 1000 +@@ -321,12 +327,8 @@ Item { + } + } + +- Component.onCompleted: { +- opacity = 1; +- } +- + StateLayer { +- function onClicked(): void { ++ onClicked: { + root.session.launcher.active = modelData; + } + } +@@ -335,11 +337,12 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + IconImage { ++ asynchronous: true + Layout.alignment: Qt.AlignVCenter + implicitSize: 32 + source: { +@@ -351,11 +354,40 @@ Item { + StyledText { + Layout.fillWidth: true + text: modelData.name || modelData.entry?.name || qsTr("Unknown") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } +- } + +- implicitHeight: 40 ++ Loader { ++ readonly property bool isHidden: modelData ? Strings.testRegexList(GlobalConfig.launcher.hiddenApps, modelData.id) : false ++ readonly property bool isFav: modelData ? Strings.testRegexList(GlobalConfig.launcher.favouriteApps, modelData.id) : false ++ ++ Layout.alignment: Qt.AlignVCenter ++ asynchronous: true ++ active: isHidden || isFav ++ ++ sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) ++ } ++ ++ Component { ++ id: hiddenIcon ++ ++ MaterialIcon { ++ text: "visibility_off" ++ fill: 1 ++ color: Colours.palette.m3primary ++ } ++ } ++ ++ Component { ++ id: favouriteIcon ++ ++ MaterialIcon { ++ text: "favorite" ++ fill: 1 ++ color: Colours.palette.m3primary ++ } ++ } ++ } + } + } + } +@@ -382,11 +414,30 @@ Item { + nextComponent = targetComponent; + } + ++ onPaneChanged: { ++ nextComponent = getComponentForPane(); ++ paneId = pane ? (pane.id || pane.entry?.id || "") : ""; ++ } ++ ++ onDisplayedAppChanged: { ++ if (displayedApp) { ++ const appId = displayedApp.id || displayedApp.entry?.id; ++ root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); ++ root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); ++ } else { ++ root.hideFromLauncherChecked = false; ++ root.favouriteChecked = false; ++ } ++ } ++ + Loader { + id: rightLauncherLoader + ++ property var displayedApp: rightLauncherPane.displayedApp ++ + anchors.fill: parent + ++ asynchronous: true + opacity: 1 + scale: 1 + transformOrigin: Item.Center +@@ -395,8 +446,6 @@ Item { + sourceComponent: rightLauncherPane.targetComponent + active: true + +- property var displayedApp: rightLauncherPane.displayedApp +- + onItemChanged: { + if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { + rightLauncherPane.displayedApp = rightLauncherPane.pane; +@@ -431,24 +480,6 @@ Item { + ] + } + } +- +- onPaneChanged: { +- nextComponent = getComponentForPane(); +- paneId = pane ? (pane.id || pane.entry?.id || "") : ""; +- } +- +- onDisplayedAppChanged: { +- if (displayedApp) { +- const appId = displayedApp.id || displayedApp.entry?.id; +- if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { +- root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); +- } else { +- root.hideFromLauncherChecked = false; +- } +- } else { +- root.hideFromLauncherChecked = false; +- } +- } + } + } + } +@@ -458,6 +489,7 @@ Item { + + StyledFlickable { + id: settingsFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + +@@ -481,16 +513,16 @@ Item { + + ColumnLayout { + id: appDetailsLayout +- anchors.fill: parent + + readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null + +- spacing: Appearance.spacing.normal ++ anchors.fill: parent ++ spacing: Tokens.spacing.normal + + SettingsHeader { +- Layout.leftMargin: Appearance.padding.large * 2 +- Layout.rightMargin: Appearance.padding.large * 2 +- Layout.topMargin: Appearance.padding.large * 2 ++ Layout.leftMargin: Tokens.padding.large * 2 ++ Layout.rightMargin: Tokens.padding.large * 2 ++ Layout.topMargin: Tokens.padding.large * 2 + visible: displayedApp === null + icon: "apps" + title: qsTr("Launcher Applications") +@@ -498,21 +530,23 @@ Item { + + Item { + Layout.alignment: Qt.AlignHCenter +- Layout.leftMargin: Appearance.padding.large * 2 +- Layout.rightMargin: Appearance.padding.large * 2 +- Layout.topMargin: Appearance.padding.large * 2 ++ Layout.leftMargin: Tokens.padding.large * 2 ++ Layout.rightMargin: Tokens.padding.large * 2 ++ Layout.topMargin: Tokens.padding.large * 2 + visible: displayedApp !== null + implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) +- implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight ++ implicitHeight: appIconImage.implicitHeight + Tokens.spacing.normal + appTitleText.implicitHeight + + ColumnLayout { + anchors.centerIn: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + IconImage { + id: appIconImage ++ ++ asynchronous: true + Layout.alignment: Qt.AlignHCenter +- implicitSize: Appearance.font.size.extraLarge * 3 * 2 ++ implicitSize: Tokens.font.size.extraLarge * 3 * 2 + source: { + const app = appDetailsLayout.displayedApp; + if (!app) +@@ -527,9 +561,10 @@ Item { + + StyledText { + id: appTitleText ++ + Layout.alignment: Qt.AlignHCenter + text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.bold: true + } + } +@@ -538,12 +573,13 @@ Item { + Item { + Layout.fillWidth: true + Layout.fillHeight: true +- Layout.topMargin: Appearance.spacing.large +- Layout.leftMargin: Appearance.padding.large * 2 +- Layout.rightMargin: Appearance.padding.large * 2 ++ Layout.topMargin: Tokens.spacing.large ++ Layout.leftMargin: Tokens.padding.large * 2 ++ Layout.rightMargin: Tokens.padding.large * 2 + + StyledFlickable { + id: detailsFlickable ++ + anchors.fill: parent + flickableDirection: Flickable.VerticalFlick + contentHeight: debugLayout.height +@@ -554,23 +590,62 @@ Item { + + ColumnLayout { + id: debugLayout ++ + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SwitchRow { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal ++ visible: appDetailsLayout.displayedApp !== null ++ label: qsTr("Mark as favourite") ++ checked: root.favouriteChecked ++ // disabled if: ++ // * app is hidden ++ // * app isn't in favouriteApps array but marked as favourite anyway ++ // ^^^ This means that this app is favourited because of a regex check ++ // this button can not toggle regexed apps ++ enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (GlobalConfig.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) ++ opacity: enabled ? 1 : 0.6 ++ onToggled: checked => { ++ root.favouriteChecked = checked; ++ const app = appDetailsLayout.displayedApp; ++ if (app) { ++ const appId = app.id || app.entry?.id; ++ const favouriteApps = GlobalConfig.launcher.favouriteApps ? [...GlobalConfig.launcher.favouriteApps] : []; ++ if (checked) { ++ if (!favouriteApps.includes(appId)) { ++ favouriteApps.push(appId); ++ } ++ } else { ++ const index = favouriteApps.indexOf(appId); ++ if (index !== -1) { ++ favouriteApps.splice(index, 1); ++ } ++ } ++ GlobalConfig.launcher.favouriteApps = favouriteApps; ++ } ++ } ++ } ++ SwitchRow { ++ Layout.topMargin: Tokens.spacing.normal + visible: appDetailsLayout.displayedApp !== null + label: qsTr("Hide from launcher") + checked: root.hideFromLauncherChecked +- enabled: appDetailsLayout.displayedApp !== null ++ // disabled if: ++ // * app is favourited ++ // * app isn't in hiddenApps array but marked as hidden anyway ++ // ^^^ This means that this app is hidden because of a regex check ++ // this button can not toggle regexed apps ++ enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (GlobalConfig.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) ++ opacity: enabled ? 1 : 0.6 + onToggled: checked => { + root.hideFromLauncherChecked = checked; + const app = appDetailsLayout.displayedApp; + if (app) { + const appId = app.id || app.entry?.id; +- const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; ++ const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; + if (checked) { + if (!hiddenApps.includes(appId)) { + hiddenApps.push(appId); +@@ -581,8 +656,7 @@ Item { + hiddenApps.splice(index, 1); + } + } +- Config.launcher.hiddenApps = hiddenApps; +- Config.save(); ++ GlobalConfig.launcher.hiddenApps = hiddenApps; + } + } + } +diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml +index 5eaf6e0e..f0181230 100644 +--- a/modules/controlcenter/launcher/Settings.qml ++++ b/modules/controlcenter/launcher/Settings.qml +@@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property Session session + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "apps" +@@ -23,7 +23,7 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("General") + description: qsTr("General launcher settings") + } +@@ -33,8 +33,7 @@ ColumnLayout { + label: qsTr("Enabled") + checked: Config.launcher.enabled + toggle.onToggled: { +- Config.launcher.enabled = checked; +- Config.save(); ++ GlobalConfig.launcher.enabled = checked; + } + } + +@@ -42,38 +41,35 @@ ColumnLayout { + label: qsTr("Show on hover") + checked: Config.launcher.showOnHover + toggle.onToggled: { +- Config.launcher.showOnHover = checked; +- Config.save(); ++ GlobalConfig.launcher.showOnHover = checked; + } + } + + ToggleRow { + label: qsTr("Vim keybinds") +- checked: Config.launcher.vimKeybinds ++ checked: GlobalConfig.launcher.vimKeybinds + toggle.onToggled: { +- Config.launcher.vimKeybinds = checked; +- Config.save(); ++ GlobalConfig.launcher.vimKeybinds = checked; + } + } + + ToggleRow { + label: qsTr("Enable dangerous actions") +- checked: Config.launcher.enableDangerousActions ++ checked: GlobalConfig.launcher.enableDangerousActions + toggle.onToggled: { +- Config.launcher.enableDangerousActions = checked; +- Config.save(); ++ GlobalConfig.launcher.enableDangerousActions = checked; + } + } + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Display") + description: qsTr("Display and appearance settings") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Max shown items") +@@ -94,28 +90,28 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Prefixes") + description: qsTr("Command prefix settings") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Special prefix") +- value: Config.launcher.specialPrefix || qsTr("None") ++ value: GlobalConfig.launcher.specialPrefix || qsTr("None") + } + + PropertyRow { + showTopMargin: true + label: qsTr("Action prefix") +- value: Config.launcher.actionPrefix || qsTr("None") ++ value: GlobalConfig.launcher.actionPrefix || qsTr("None") + } + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Fuzzy search") + description: qsTr("Fuzzy search settings") + } +@@ -123,95 +119,90 @@ ColumnLayout { + SectionContainer { + ToggleRow { + label: qsTr("Apps") +- checked: Config.launcher.useFuzzy.apps ++ checked: GlobalConfig.launcher.useFuzzy.apps + toggle.onToggled: { +- Config.launcher.useFuzzy.apps = checked; +- Config.save(); ++ GlobalConfig.launcher.useFuzzy.apps = checked; + } + } + + ToggleRow { + label: qsTr("Actions") +- checked: Config.launcher.useFuzzy.actions ++ checked: GlobalConfig.launcher.useFuzzy.actions + toggle.onToggled: { +- Config.launcher.useFuzzy.actions = checked; +- Config.save(); ++ GlobalConfig.launcher.useFuzzy.actions = checked; + } + } + + ToggleRow { + label: qsTr("Schemes") +- checked: Config.launcher.useFuzzy.schemes ++ checked: GlobalConfig.launcher.useFuzzy.schemes + toggle.onToggled: { +- Config.launcher.useFuzzy.schemes = checked; +- Config.save(); ++ GlobalConfig.launcher.useFuzzy.schemes = checked; + } + } + + ToggleRow { + label: qsTr("Variants") +- checked: Config.launcher.useFuzzy.variants ++ checked: GlobalConfig.launcher.useFuzzy.variants + toggle.onToggled: { +- Config.launcher.useFuzzy.variants = checked; +- Config.save(); ++ GlobalConfig.launcher.useFuzzy.variants = checked; + } + } + + ToggleRow { + label: qsTr("Wallpapers") +- checked: Config.launcher.useFuzzy.wallpapers ++ checked: GlobalConfig.launcher.useFuzzy.wallpapers + toggle.onToggled: { +- Config.launcher.useFuzzy.wallpapers = checked; +- Config.save(); ++ GlobalConfig.launcher.useFuzzy.wallpapers = checked; + } + } + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Sizes") + description: qsTr("Size settings for launcher items") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Item width") +- value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth) ++ value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemWidth) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Item height") +- value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight) ++ value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemHeight) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Wallpaper width") +- value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth) ++ value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperWidth) + } + + PropertyRow { + showTopMargin: true + label: qsTr("Wallpaper height") +- value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight) ++ value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperHeight) + } + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Hidden apps") + description: qsTr("Applications hidden from launcher") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Total hidden") +- value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) ++ value: qsTr("%1").arg(GlobalConfig.launcher.hiddenApps ? GlobalConfig.launcher.hiddenApps.length : 0) + } + } + } +diff --git a/modules/controlcenter/monitors/MonitorsPane.qml b/modules/controlcenter/monitors/MonitorsPane.qml +index 8cdc49e9..82809fd9 100644 +--- a/modules/controlcenter/monitors/MonitorsPane.qml ++++ b/modules/controlcenter/monitors/MonitorsPane.qml +@@ -8,7 +8,7 @@ import qs.components.controls + import qs.components.effects + import qs.components.containers + import qs.services +-import qs.config ++import Caelestia.Config + import Quickshell + import Quickshell.Hyprland + import QtQuick +@@ -18,9 +18,47 @@ Item { + id: root + + required property Session session ++ readonly property var monitorModel: Hyprctl.monitors ++ ++ function selectMonitor(monitor: var): void { ++ if (!monitor) ++ return; ++ root.session.monitor.active = { ++ id: monitor.id, ++ name: monitor.name ++ }; ++ } ++ ++ function selectedMonitor(): var { ++ const active = root.session.monitor.active; ++ if (!active) ++ return null; ++ ++ for (const monitor of root.monitorModel) { ++ if (monitor.name === active.name || monitor.id === active.id) ++ return monitor; ++ } ++ ++ return null; ++ } ++ ++ function ensureSingleMonitorSelected(): void { ++ if (!root.session.monitor.active && (root.monitorModel?.length ?? 0) === 1) ++ root.selectMonitor(root.monitorModel[0]); ++ } + + anchors.fill: parent + ++ Component.onCompleted: Qt.callLater(root.ensureSingleMonitorSelected) ++ ++ Connections { ++ target: Hyprctl ++ ++ function onMonitorsChanged(): void { ++ root.ensureSingleMonitorSelected(); ++ } ++ } ++ + // ── Two-column split (mirrors NetworkingPane) ────────────────────── + SplitPaneLayout { + id: splitLayout +@@ -44,16 +82,16 @@ Item { + + anchors.left: parent.left + anchors.right: parent.right +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + // Header row + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Monitors") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -64,9 +102,9 @@ Item { + toggled: Monitors.identifying + icon: "tv_signin" + accent: "Secondary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Identify monitors") + + onClicked: Monitors.toggleIdentification() +@@ -76,14 +114,14 @@ Item { + // Subtitle + StyledText { + Layout.fillWidth: true +- text: qsTr("%1 display(s) connected").arg(Hyprland.monitors.length) ++ text: qsTr("%1 display(s) connected").arg(root.monitorModel.length) + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + +- // Monitor list — use Hyprland.monitors directly as model ++ // Monitor list — use hyprctl data so refresh rate and modes are available + Repeater { +- model: Hyprland.monitors ++ model: root.monitorModel + + delegate: MonitorListItem { + required property var modelData +@@ -94,9 +132,10 @@ Item { + monitor: modelData + active: root.session.monitor.active !== null + && root.session.monitor.active !== undefined +- && root.session.monitor.active.id === modelData.id ++ && (root.session.monitor.active.id === modelData.id ++ || root.session.monitor.active.name === modelData.name) + +- onClicked: root.session.monitor.active = modelData ++ onClicked: root.selectMonitor(modelData) + } + } + } +@@ -108,7 +147,7 @@ Item { + Item { + id: rightPaneItem + +- property var selectedMonitor: root.session.monitor.active ++ property var selectedMonitor: root.selectedMonitor() + property string paneId: selectedMonitor + ? ("mon:" + (selectedMonitor.name ?? "")) + : "overview" +@@ -127,6 +166,15 @@ Item { + Connections { + target: root.session.monitor + function onActiveChanged(): void { ++ rightPaneItem.selectedMonitor = root.selectedMonitor(); ++ rightPaneItem.nextComponent = rightPaneItem.resolveComponent(); ++ } ++ } ++ ++ Connections { ++ target: Hyprctl ++ function onMonitorsChanged(): void { ++ rightPaneItem.selectedMonitor = root.selectedMonitor(); + rightPaneItem.nextComponent = rightPaneItem.resolveComponent(); + } + } +@@ -166,11 +214,11 @@ Item { + + signal clicked() + +- implicitHeight: itemRow.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: itemRow.implicitHeight + Tokens.padding.normal * 2 + + StyledRect { + anchors.fill: parent +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Qt.alpha( + Colours.tPalette.m3surfaceContainer, + listItem.active ? Colours.tPalette.m3surfaceContainer.a : 0 +@@ -186,14 +234,14 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.normal + + // Monitor icon badge + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: monIcon.implicitHeight + Appearance.padding.normal * 2 +- radius: Appearance.rounding.normal ++ implicitHeight: monIcon.implicitHeight + Tokens.padding.normal * 2 ++ radius: Tokens.rounding.normal + color: listItem.active + ? Colours.palette.m3primaryContainer + : Colours.tPalette.m3surfaceContainerHigh +@@ -202,7 +250,7 @@ Item { + id: monIcon + anchors.centerIn: parent + text: "monitor" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: listItem.active ? 1 : 0 + color: listItem.active + ? Colours.palette.m3onPrimaryContainer +@@ -226,7 +274,7 @@ Item { + StyledText { + Layout.fillWidth: true + elide: Text.ElideRight +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + text: { + const m = listItem.monitor; +@@ -240,16 +288,16 @@ Item { + // Focused badge + StyledRect { + visible: listItem.monitor?.focused ?? false +- implicitWidth: focusedLabel.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: focusedLabel.implicitHeight + Appearance.padding.small * 2 +- radius: Appearance.rounding.full ++ implicitWidth: focusedLabel.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: focusedLabel.implicitHeight + Tokens.padding.small * 2 ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, 0.9) + + StyledText { + id: focusedLabel + anchors.centerIn: parent + text: qsTr("Active") +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onPrimaryContainer + } + } +@@ -284,7 +332,7 @@ Item { + + anchors.left: parent.left + anchors.right: parent.right +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "monitor" +@@ -297,10 +345,10 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small ++ contentSpacing: Tokens.spacing.small + + Repeater { +- model: Hyprland.monitors ++ model: root.monitorModel + + delegate: PropertyRow { + required property var modelData +@@ -329,15 +377,15 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: "tv_signin" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + color: Colours.palette.m3onSurfaceVariant + } + +@@ -346,12 +394,12 @@ Item { + spacing: 0 + StyledText { + text: qsTr("Identify displays") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + StyledText { + text: qsTr("Show monitor IDs on each screen") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + +@@ -381,7 +429,7 @@ Item { + flickable: detailFlickable + } + +- readonly property var mon: root.session.monitor.active ++ readonly property var mon: root.selectedMonitor() + readonly property var brightnessMon: mon ? Brightness.getMonitor(mon.name) : null + + ColumnLayout { +@@ -389,7 +437,7 @@ Item { + + anchors.left: parent.left + anchors.right: parent.right +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + // ── Header ────────────────────────────────────────── + ConnectionHeader { +@@ -402,7 +450,7 @@ Item { + Layout.fillWidth: true + visible: detailFlickable.brightnessMon !== null + && detailFlickable.brightnessMon !== undefined +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Brightness") +@@ -410,22 +458,22 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: (detailFlickable.brightnessMon?.brightness ?? 0) > 0.5 + ? "brightness_high" : "brightness_low" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + StyledSlider { + Layout.fillWidth: true +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + from: 0; to: 1; stepSize: 0.01 + value: detailFlickable.brightnessMon?.brightness ?? 0 + onMoved: detailFlickable.brightnessMon?.setBrightness(value) +@@ -435,17 +483,96 @@ Item { + text: qsTr("%1%").arg( + Math.round((detailFlickable.brightnessMon?.brightness ?? 0) * 100)) + Layout.preferredWidth: 38 +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + } + } + } + } + ++ // ── Refresh rate ───────────────────────────────────── ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ ++ SectionHeader { ++ title: qsTr("Refresh rate") ++ description: qsTr("Set the display refresh rate") ++ } ++ ++ SectionContainer { ++ contentSpacing: Tokens.spacing.normal ++ ++ SpinBoxRow { ++ Layout.fillWidth: true ++ label: qsTr("Refresh rate") ++ value: detailFlickable.mon?.refreshRate ?? 60 ++ min: 10 ++ max: 1000 ++ step: 0.01 ++ onValueModified: value => { ++ if (detailFlickable.mon) ++ Monitors.setRefreshRate(detailFlickable.mon.name, value); ++ } ++ } ++ ++ RowLayout { ++ Layout.fillWidth: true ++ visible: (detailFlickable.mon?.availableModes?.length ?? 0) > 0 ++ spacing: Tokens.spacing.small ++ ++ Repeater { ++ model: detailFlickable.mon?.availableModes ?? [] ++ ++ delegate: StyledRect { ++ required property string modelData ++ required property int index ++ ++ Layout.fillWidth: true ++ implicitHeight: modeLabel.implicitHeight + Tokens.padding.normal * 2 ++ radius: Tokens.rounding.full ++ ++ readonly property real modeRate: { ++ const match = modelData.match(/@(\d+(?:\.\d+)?)Hz/); ++ return match ? parseFloat(match[1]) : 0; ++ } ++ readonly property bool isActive: Math.abs((detailFlickable.mon?.refreshRate ?? 0) - modeRate) < 0.1 ++ ++ color: isActive ++ ? Colours.palette.m3secondaryContainer ++ : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) ++ ++ StateLayer { ++ color: parent.isActive ++ ? Colours.palette.m3onSecondaryContainer ++ : Colours.palette.m3onSurfaceVariant ++ onClicked: { ++ if (detailFlickable.mon && parent.modeRate > 0) ++ Monitors.setRefreshRate(detailFlickable.mon.name, parent.modeRate); ++ } ++ } ++ ++ StyledText { ++ id: modeLabel ++ anchors.centerIn: parent ++ text: modelData ++ font.pointSize: Tokens.font.size.small ++ color: parent.isActive ++ ? Colours.palette.m3onSecondaryContainer ++ : Colours.palette.m3onSurfaceVariant ++ } ++ ++ Behavior on color { CAnim {} } ++ } ++ } ++ } ++ } ++ } ++ + // ── Rotation ───────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Rotation") +@@ -453,11 +580,11 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small ++ contentSpacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Repeater { + model: [ +@@ -488,7 +615,7 @@ Item { + // ── Scale ──────────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Scale") +@@ -496,22 +623,22 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: "zoom_in" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + StyledSlider { + id: scaleSlider + Layout.fillWidth: true +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + from: 0.5; to: 3.0; stepSize: 0.25 + value: detailFlickable.mon?.scale ?? 1 + +@@ -524,7 +651,7 @@ Item { + StyledText { + text: qsTr("×%1").arg((detailFlickable.mon?.scale ?? 1).toFixed(2)) + Layout.preferredWidth: 42 +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + } + } +@@ -532,7 +659,7 @@ Item { + // Quick-pick chips: 1×, 1.25×, 1.5×, 2× + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Repeater { + model: [1.0, 1.25, 1.5, 2.0] +@@ -542,8 +669,8 @@ Item { + required property int index + + Layout.fillWidth: true +- implicitHeight: scaleChipLabel.implicitHeight + Appearance.padding.normal * 2 +- radius: Appearance.rounding.full ++ implicitHeight: scaleChipLabel.implicitHeight + Tokens.padding.normal * 2 ++ radius: Tokens.rounding.full + + readonly property bool isActive: + Math.abs((detailFlickable.mon?.scale ?? 1) - modelData) < 0.01 +@@ -566,7 +693,7 @@ Item { + id: scaleChipLabel + anchors.centerIn: parent + text: qsTr("×%1").arg(modelData.toFixed(2)) +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: parent.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant +@@ -582,8 +709,8 @@ Item { + // ── Arrangement ────────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true +- visible: Hyprland.monitors.length > 1 +- spacing: Appearance.spacing.normal ++ visible: root.monitorModel.length > 1 ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Arrangement") +@@ -592,14 +719,16 @@ Item { + + // One card per OTHER monitor — use visible to skip self + Repeater { +- model: Hyprland.monitors ++ model: root.monitorModel + + delegate: SectionContainer { ++ id: targetSection ++ + required property var modelData + required property int index + + Layout.fillWidth: true +- contentSpacing: Appearance.spacing.small ++ contentSpacing: Tokens.spacing.small + + // Hide the current monitor's own entry without JS filter + visible: detailFlickable.mon !== null +@@ -609,11 +738,11 @@ Item { + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + MaterialIcon { + text: "tv" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + +@@ -622,15 +751,15 @@ Item { + text: qsTr("Relative to Monitor %1 (%2)") + .arg(modelData.id ?? 0) + .arg(modelData.name ?? "") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + } + + GridLayout { + Layout.fillWidth: true + columns: 4 +- columnSpacing: Appearance.spacing.small +- rowSpacing: Appearance.spacing.small ++ columnSpacing: Tokens.spacing.small ++ rowSpacing: Tokens.spacing.small + + Repeater { + model: [ +@@ -652,7 +781,7 @@ Item { + Monitors.arrange( + detailFlickable.mon.name, + modelData.pos, +- parent.parent.parent.modelData.id ++ targetSection.modelData.id + ); + } + } +@@ -665,7 +794,7 @@ Item { + // ── Display information ─────────────────────────────── + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Display information") +@@ -673,7 +802,7 @@ Item { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Name") +@@ -756,11 +885,11 @@ Item { + required property bool isActive + signal clicked() + +- implicitHeight: chipContent.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: chipContent.implicitHeight + Tokens.padding.normal * 2 + + StyledRect { + anchors.fill: parent +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: chip.isActive + ? Colours.palette.m3secondaryContainer + : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) +@@ -781,7 +910,7 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: "screen_rotation" + rotation: chip.chipAngle +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + color: chip.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant +@@ -791,7 +920,7 @@ Item { + StyledText { + Layout.alignment: Qt.AlignHCenter + text: chip.chipLabel +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: chip.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant +@@ -808,11 +937,11 @@ Item { + required property string btnLabel + signal clicked() + +- implicitHeight: btnContent.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: btnContent.implicitHeight + Tokens.padding.normal * 2 + + StyledRect { + anchors.fill: parent +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) + + StateLayer { +@@ -828,14 +957,14 @@ Item { + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: arrangeBtn.btnIcon +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: arrangeBtn.btnLabel +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } +diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml +index 4e60b3d4..1daeb4a2 100644 +--- a/modules/controlcenter/network/EthernetDetails.qml ++++ b/modules/controlcenter/network/EthernetDetails.qml +@@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + DeviceDetails { + id: root +@@ -43,7 +43,7 @@ DeviceDetails { + sections: [ + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Connection status") +@@ -69,7 +69,7 @@ DeviceDetails { + }, + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Device properties") +@@ -77,7 +77,7 @@ DeviceDetails { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Interface") +@@ -100,7 +100,7 @@ DeviceDetails { + }, + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Connection information") +diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml +index d1eb9579..3a947c86 100644 +--- a/modules/controlcenter/network/EthernetList.qml ++++ b/modules/controlcenter/network/EthernetList.qml +@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + DeviceList { + id: root +@@ -23,11 +23,11 @@ DeviceList { + + headerComponent: Component { + RowLayout { +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Settings") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -39,9 +39,9 @@ DeviceList { + toggled: !root.session.ethernet.active + icon: "settings" + accent: "Primary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + + onClicked: { + if (root.session.ethernet.active) +@@ -62,15 +62,15 @@ DeviceList { + readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface + + width: ListView.view ? ListView.view.width : undefined +- implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + StateLayer { + id: stateLayer + +- function onClicked(): void { ++ onClicked: { + root.session.ethernet.active = modelData; + } + } +@@ -79,15 +79,15 @@ DeviceList { + id: rowLayout + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + StyledRect { +@@ -101,7 +101,7 @@ DeviceList { + + anchors.centerIn: parent + text: "cable" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: modelData.connected ? 1 : 0 + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + +@@ -124,13 +124,13 @@ DeviceList { + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") + color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + font.weight: modelData.connected ? 500 : 400 + elide: Text.ElideRight + } +@@ -141,21 +141,21 @@ DeviceList { + id: connectBtn + + implicitWidth: implicitHeight +- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) + + StateLayer { +- color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface +- +- function onClicked(): void { ++ onClicked: { + if (modelData.connected && modelData.connection) { + Nmcli.disconnectEthernet(modelData.connection, () => {}); + } else { + Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); + } + } ++ ++ color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } + + MaterialIcon { +diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml +index 59d82bb0..d66620f1 100644 +--- a/modules/controlcenter/network/EthernetPane.qml ++++ b/modules/controlcenter/network/EthernetPane.qml +@@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components + import qs.components.containers +-import qs.config +-import Quickshell.Widgets +-import QtQuick + + SplitPaneWithDetails { + id: root +diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml +index 90bfcf46..f13b6fca 100644 +--- a/modules/controlcenter/network/EthernetSettings.qml ++++ b/modules/controlcenter/network/EthernetSettings.qml +@@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property Session session + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "cable" +@@ -23,9 +23,9 @@ ColumnLayout { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + text: qsTr("Ethernet devices") +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + +@@ -36,9 +36,9 @@ ColumnLayout { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: ethernetInfo.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { +@@ -47,9 +47,9 @@ ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { + text: qsTr("Total devices") +@@ -58,18 +58,18 @@ ColumnLayout { + StyledText { + text: qsTr("%1").arg(Nmcli.ethernetDevices.length) + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("Connected devices") + } + + StyledText { + text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + } +diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml +index bda7cb18..d8700ecd 100644 +--- a/modules/controlcenter/network/NetworkSettings.qml ++++ b/modules/controlcenter/network/NetworkSettings.qml +@@ -2,22 +2,22 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Controls ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Controls +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property Session session + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "router" +@@ -25,13 +25,13 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Ethernet") + description: qsTr("Ethernet device information") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Total devices") +@@ -46,7 +46,7 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Wireless") + description: qsTr("WiFi network settings") + } +@@ -62,34 +62,33 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("VPN") + description: qsTr("VPN provider settings") +- visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 ++ visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 + } + + SectionContainer { +- visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 ++ visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 + + ToggleRow { + label: qsTr("VPN enabled") +- checked: Config.utilities.vpn.enabled ++ checked: GlobalConfig.utilities.vpn.enabled + toggle.onToggled: { +- Config.utilities.vpn.enabled = checked; +- Config.save(); ++ GlobalConfig.utilities.vpn.enabled = checked; + } + } + + PropertyRow { + showTopMargin: true + label: qsTr("Providers") +- value: qsTr("%1").arg(Config.utilities.vpn.provider.length) ++ value: qsTr("%1").arg(GlobalConfig.utilities.vpn.provider.length) + } + + TextButton { + Layout.fillWidth: true +- Layout.topMargin: Appearance.spacing.normal +- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 ++ Layout.topMargin: Tokens.spacing.normal ++ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + text: qsTr("⚙ Manage VPN Providers") + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer +@@ -101,13 +100,13 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Current connection") + description: qsTr("Active network connection information") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Network") +@@ -141,20 +140,20 @@ ColumnLayout { + + parent: Overlay.overlay + anchors.centerIn: parent +- width: Math.min(600, parent.width - Appearance.padding.large * 2) +- height: Math.min(700, parent.height - Appearance.padding.large * 2) ++ width: Math.min(600, parent.width - Tokens.padding.large * 2) ++ height: Math.min(700, parent.height - Tokens.padding.large * 2) + + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + + background: StyledRect { + color: Colours.palette.m3surface +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + } + + StyledFlickable { + anchors.fill: parent +- anchors.margins: Appearance.padding.large * 1.5 ++ anchors.margins: Tokens.padding.large * 1.5 + flickableDirection: Flickable.VerticalFlick + contentHeight: vpnSettingsContent.height + clip: true +diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml +index 26cdbfac..e02bfafb 100644 +--- a/modules/controlcenter/network/NetworkingPane.qml ++++ b/modules/controlcenter/network/NetworkingPane.qml +@@ -3,17 +3,17 @@ pragma ComponentBehavior: Bound + import ".." + import "../components" + import "." ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root +@@ -43,15 +43,15 @@ Item { + + anchors.left: parent.left + anchors.right: parent.right +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Network") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -63,9 +63,9 @@ Item { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Toggle WiFi") + + onClicked: { +@@ -77,9 +77,9 @@ Item { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Scan for networks") + + onClicked: { +@@ -91,9 +91,9 @@ Item { + toggled: !root.session.ethernet.active && !root.session.network.active + icon: "settings" + accent: "Primary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + tooltip: qsTr("Network settings") + + onClicked: { +@@ -120,6 +120,7 @@ Item { + + Loader { + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: Component { + VpnList { + session: root.session +@@ -138,6 +139,7 @@ Item { + + Loader { + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: Component { + EthernetList { + session: root.session +@@ -156,6 +158,7 @@ Item { + + Loader { + Layout.fillWidth: true ++ asynchronous: true + sourceComponent: Component { + WirelessList { + session: root.session +@@ -196,9 +199,6 @@ Item { + } + + Connections { +- target: root.session && root.session.vpn ? root.session.vpn : null +- enabled: target !== null +- + function onActiveChanged() { + // Clear others when VPN is selected + if (root.session && root.session.vpn && root.session.vpn.active) { +@@ -209,12 +209,12 @@ Item { + } + rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); + } +- } + +- Connections { +- target: root.session && root.session.ethernet ? root.session.ethernet : null ++ target: root.session && root.session.vpn ? root.session.vpn : null + enabled: target !== null ++ } + ++ Connections { + function onActiveChanged() { + // Clear others when ethernet is selected + if (root.session && root.session.ethernet && root.session.ethernet.active) { +@@ -225,12 +225,12 @@ Item { + } + rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); + } +- } + +- Connections { +- target: root.session && root.session.network ? root.session.network : null ++ target: root.session && root.session.ethernet ? root.session.ethernet : null + enabled: target !== null ++ } + ++ Connections { + function onActiveChanged() { + // Clear others when wireless is selected + if (root.session && root.session.network && root.session.network.active) { +@@ -241,6 +241,9 @@ Item { + } + rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); + } ++ ++ target: root.session && root.session.network ? root.session.network : null ++ enabled: target !== null + } + + Loader { +@@ -278,6 +281,7 @@ Item { + + StyledFlickable { + id: settingsFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: settingsInner.height + +@@ -301,6 +305,7 @@ Item { + + StyledFlickable { + id: ethernetFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: ethernetDetailsInner.height + +@@ -324,6 +329,7 @@ Item { + + StyledFlickable { + id: wirelessFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: wirelessDetailsInner.height + +@@ -347,6 +353,7 @@ Item { + + StyledFlickable { + id: vpnFlickable ++ + flickableDirection: Flickable.VerticalFlick + contentHeight: vpnDetailsInner.height + +diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml +index 1c71cd71..7ac2dc50 100644 +--- a/modules/controlcenter/network/VpnDetails.qml ++++ b/modules/controlcenter/network/VpnDetails.qml +@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Controls ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config + import qs.utils +-import QtQuick +-import QtQuick.Controls +-import QtQuick.Layouts + + DeviceDetails { + id: root +@@ -21,7 +21,7 @@ DeviceDetails { + readonly property bool providerEnabled: { + if (!vpnProvider || vpnProvider.index === undefined) + return false; +- const provider = Config.utilities.vpn.provider[vpnProvider.index]; ++ const provider = GlobalConfig.utilities.vpn.provider[vpnProvider.index]; + return provider && typeof provider === "object" && provider.enabled === true; + } + +@@ -37,7 +37,7 @@ DeviceDetails { + sections: [ + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Connection status") +@@ -55,8 +55,8 @@ DeviceDetails { + const index = root.vpnProvider.index; + + // Copy providers and update enabled state +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { +- const p = Config.utilities.vpn.provider[i]; ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ const p = GlobalConfig.utilities.vpn.provider[i]; + if (typeof p === "object") { + const newProvider = { + name: p.name, +@@ -72,25 +72,31 @@ DeviceDetails { + newProvider.enabled = (i === index) ? false : (p.enabled !== false); + } + ++ if (p.connectCmd && p.connectCmd.length > 0) { ++ newProvider.connectCmd = p.connectCmd; ++ } ++ if (p.disconnectCmd && p.disconnectCmd.length > 0) { ++ newProvider.disconnectCmd = p.disconnectCmd; ++ } ++ + providers.push(newProvider); + } else { + providers.push(p); + } + } + +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + } + } + + RowLayout { + Layout.fillWidth: true +- Layout.topMargin: Appearance.spacing.normal +- spacing: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal ++ spacing: Tokens.spacing.normal + + TextButton { + Layout.fillWidth: true +- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 ++ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + visible: root.providerEnabled + enabled: !VPN.connecting + inactiveColour: Colours.palette.m3primaryContainer +@@ -109,10 +115,13 @@ DeviceDetails { + inactiveOnColour: Colours.palette.m3onSecondaryContainer + + onClicked: { ++ const provider = GlobalConfig.utilities.vpn.provider[root.vpnProvider.index]; + editVpnDialog.editIndex = root.vpnProvider.index; + editVpnDialog.providerName = root.vpnProvider.name; + editVpnDialog.displayName = root.vpnProvider.displayName; + editVpnDialog.interfaceName = root.vpnProvider.interface; ++ editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : ""; ++ editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : ""; + editVpnDialog.open(); + } + } +@@ -125,23 +134,46 @@ DeviceDetails { + + onClicked: { + const providers = []; +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + if (i !== root.vpnProvider.index) { +- providers.push(Config.utilities.vpn.provider[i]); ++ providers.push(GlobalConfig.utilities.vpn.provider[i]); + } + } +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + root.session.vpn.active = null; + } + } + } ++ ++ TextButton { ++ Layout.fillWidth: true ++ Layout.topMargin: Tokens.spacing.normal ++ visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" ++ text: qsTr("Open Login Page") ++ inactiveColour: Colours.palette.m3tertiaryContainer ++ inactiveOnColour: Colours.palette.m3onTertiaryContainer ++ ++ onClicked: { ++ Qt.openUrlExternally(VPN.status.authUrl); ++ } ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ Layout.topMargin: Tokens.spacing.normal ++ visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" ++ text: qsTr("Click 'Connect' to generate authentication URL") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ horizontalAlignment: Text.AlignHCenter ++ wrapMode: Text.WordWrap ++ } + } + } + }, + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Provider details") +@@ -149,7 +181,7 @@ DeviceDetails { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Provider") +@@ -176,12 +208,31 @@ DeviceDetails { + return qsTr("Disabled"); + if (VPN.connecting) + return qsTr("Connecting..."); +- if (VPN.connected) ++ ++ switch (VPN.status.state) { ++ case "connected": + return qsTr("Connected"); +- return qsTr("Enabled (Not connected)"); ++ case "disconnected": ++ return qsTr("Disconnected"); ++ case "connecting": ++ return qsTr("Connecting..."); ++ case "needs-auth": ++ return qsTr("Authentication required"); ++ case "error": ++ return qsTr("Error"); ++ default: ++ return qsTr("Unknown"); ++ } + } + } + ++ PropertyRow { ++ visible: VPN.status.reason !== "" ++ showTopMargin: true ++ label: qsTr("Details") ++ value: VPN.status.reason ++ } ++ + PropertyRow { + showTopMargin: true + label: qsTr("Enabled") +@@ -200,11 +251,17 @@ DeviceDetails { + property string providerName: "" + property string displayName: "" + property string interfaceName: "" ++ property string connectCmd: "" ++ property string disconnectCmd: "" ++ ++ function closeWithAnimation(): void { ++ close(); ++ } + + parent: Overlay.overlay + anchors.centerIn: parent +- width: Math.min(400, parent.width - Appearance.padding.large * 2) +- padding: Appearance.padding.large * 1.5 ++ width: Math.min(400, parent.width - Tokens.padding.large * 2) ++ padding: Tokens.padding.large * 1.5 + + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside +@@ -217,15 +274,13 @@ DeviceDetails { + property: "opacity" + from: 0 + to: 1 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + Anim { + property: "scale" + from: 0.7 + to: 1 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + +@@ -234,29 +289,23 @@ DeviceDetails { + property: "opacity" + from: 1 + to: 0 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + Anim { + property: "scale" + from: 1 + to: 0.7 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + +- function closeWithAnimation(): void { +- close(); +- } +- + Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) + } + + background: StyledRect { + color: Colours.palette.m3surfaceContainerHigh +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + + Elevation { + anchors.fill: parent +@@ -267,21 +316,21 @@ DeviceDetails { + } + + contentItem: ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + text: qsTr("Edit VPN Provider") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller / 2 ++ spacing: Tokens.spacing.smaller / 2 + + StyledText { + text: qsTr("Display Name") +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + +@@ -289,7 +338,7 @@ DeviceDetails { + Layout.fillWidth: true + implicitHeight: 40 + color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + border.width: 1 + border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + +@@ -302,8 +351,9 @@ DeviceDetails { + + StyledTextField { + id: displayNameField ++ + anchors.centerIn: parent +- width: parent.width - Appearance.padding.normal ++ width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: editVpnDialog.displayName + onTextChanged: editVpnDialog.displayName = text +@@ -313,11 +363,11 @@ DeviceDetails { + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller / 2 ++ spacing: Tokens.spacing.smaller / 2 + + StyledText { + text: qsTr("Interface (e.g., wg0, torguard)") +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + +@@ -325,7 +375,7 @@ DeviceDetails { + Layout.fillWidth: true + implicitHeight: 40 + color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + border.width: 1 + border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + +@@ -338,8 +388,9 @@ DeviceDetails { + + StyledTextField { + id: interfaceNameField ++ + anchors.centerIn: parent +- width: parent.width - Appearance.padding.normal ++ width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: editVpnDialog.interfaceName + onTextChanged: editVpnDialog.interfaceName = text +@@ -347,10 +398,86 @@ DeviceDetails { + } + } + ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.smaller / 2 ++ visible: editVpnDialog.connectCmd.length > 0 ++ ++ StyledText { ++ text: qsTr("Connect Command") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ ++ StyledRect { ++ Layout.fillWidth: true ++ implicitHeight: 40 ++ color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) ++ radius: Tokens.rounding.small ++ border.width: 1 ++ border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) ++ ++ Behavior on color { ++ CAnim {} ++ } ++ Behavior on border.color { ++ CAnim {} ++ } ++ ++ StyledTextField { ++ id: connectCmdFieldEdit ++ ++ anchors.centerIn: parent ++ width: parent.width - Tokens.padding.normal ++ horizontalAlignment: TextInput.AlignLeft ++ text: editVpnDialog.connectCmd ++ onTextChanged: editVpnDialog.connectCmd = text ++ } ++ } ++ } ++ ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.smaller / 2 ++ visible: editVpnDialog.disconnectCmd.length > 0 ++ ++ StyledText { ++ text: qsTr("Disconnect Command") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ ++ StyledRect { ++ Layout.fillWidth: true ++ implicitHeight: 40 ++ color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) ++ radius: Tokens.rounding.small ++ border.width: 1 ++ border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) ++ ++ Behavior on color { ++ CAnim {} ++ } ++ Behavior on border.color { ++ CAnim {} ++ } ++ ++ StyledTextField { ++ id: disconnectCmdFieldEdit ++ ++ anchors.centerIn: parent ++ width: parent.width - Tokens.padding.normal ++ horizontalAlignment: TextInput.AlignLeft ++ text: editVpnDialog.disconnectCmd ++ onTextChanged: editVpnDialog.disconnectCmd = text ++ } ++ } ++ } ++ + RowLayout { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + TextButton { + Layout.fillWidth: true +@@ -369,24 +496,44 @@ DeviceDetails { + + onClicked: { + const providers = []; +- const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex]; ++ const oldProvider = GlobalConfig.utilities.vpn.provider[editVpnDialog.editIndex]; + const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true; + +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + if (i === editVpnDialog.editIndex) { +- providers.push({ +- name: editVpnDialog.providerName, ++ const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0; ++ const newProvider = { + displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, ++ enabled: wasEnabled, + interface: editVpnDialog.interfaceName, +- enabled: wasEnabled +- }); ++ name: editVpnDialog.providerName ++ }; ++ ++ if (hasCommands) { ++ newProvider.connectCmd = editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0); ++ newProvider.disconnectCmd = editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); ++ } ++ ++ providers.push(newProvider); + } else { +- providers.push(Config.utilities.vpn.provider[i]); ++ const p = GlobalConfig.utilities.vpn.provider[i]; ++ const reconstructed = { ++ displayName: p.displayName, ++ enabled: p.enabled, ++ interface: p.interface, ++ name: p.name ++ }; ++ if (p.connectCmd && p.connectCmd.length > 0) { ++ reconstructed.connectCmd = p.connectCmd; ++ } ++ if (p.disconnectCmd && p.disconnectCmd.length > 0) { ++ reconstructed.disconnectCmd = p.disconnectCmd; ++ } ++ providers.push(reconstructed); + } + } + +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + editVpnDialog.closeWithAnimation(); + } + } +diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml +index 81f4a45a..d9266b34 100644 +--- a/modules/controlcenter/network/VpnList.qml ++++ b/modules/controlcenter/network/VpnList.qml +@@ -1,15 +1,15 @@ + pragma ComponentBehavior: Bound + + import ".." ++import QtQuick ++import QtQuick.Controls ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Controls +-import QtQuick.Layouts + + ColumnLayout { + id: root +@@ -18,18 +18,17 @@ ColumnLayout { + property bool showHeader: true + property int pendingSwitchIndex: -1 + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Connections { +- target: VPN + function onConnectedChanged() { + if (!VPN.connected && root.pendingSwitchIndex >= 0) { + const targetIndex = root.pendingSwitchIndex; + root.pendingSwitchIndex = -1; + + const providers = []; +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { +- const p = Config.utilities.vpn.provider[i]; ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ const p = GlobalConfig.utilities.vpn.provider[i]; + if (typeof p === "object") { + const newProvider = { + name: p.name, +@@ -37,19 +36,26 @@ ColumnLayout { + interface: p.interface, + enabled: (i === targetIndex) + }; ++ if (p.connectCmd && p.connectCmd.length > 0) { ++ newProvider.connectCmd = p.connectCmd; ++ } ++ if (p.disconnectCmd && p.disconnectCmd.length > 0) { ++ newProvider.disconnectCmd = p.disconnectCmd; ++ } + providers.push(newProvider); + } else { + providers.push(p); + } + } +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + + Qt.callLater(function () { + VPN.toggle(); + }); + } + } ++ ++ target: VPN + } + + TextButton { +@@ -70,10 +76,10 @@ ColumnLayout { + Layout.preferredHeight: contentHeight + + interactive: false +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + model: ScriptModel { +- values: Config.utilities.vpn.provider.map((provider, index) => { ++ values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { + const isObject = typeof provider === "object"; + const name = isObject ? (provider.name || "custom") : String(provider); + const displayName = isObject ? (provider.displayName || name) : name; +@@ -97,12 +103,13 @@ ColumnLayout { + required property int index + + width: ListView.view ? ListView.view.width : undefined ++ implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + StateLayer { +- function onClicked(): void { ++ onClicked: { + if (root.session && root.session.vpn) { + root.session.vpn.active = modelData; + } +@@ -115,15 +122,15 @@ ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { +@@ -131,7 +138,7 @@ ColumnLayout { + + anchors.centerIn: parent + text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: modelData.enabled && VPN.connected ? 1 : 0 + color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } +@@ -152,21 +159,44 @@ ColumnLayout { + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: { +- if (modelData.enabled && VPN.connected) ++ if (!modelData.enabled) ++ return qsTr("Disabled"); ++ ++ if (VPN.connecting) ++ return qsTr("Connecting..."); ++ ++ switch (VPN.status.state) { ++ case "connected": + return qsTr("Connected"); +- if (modelData.enabled && VPN.connecting) ++ case "disconnected": ++ return qsTr("Enabled"); ++ case "connecting": + return qsTr("Connecting..."); +- if (modelData.enabled) ++ case "needs-auth": ++ return qsTr("Auth required"); ++ case "error": ++ return qsTr("Error"); ++ default: + return qsTr("Enabled"); +- return qsTr("Disabled"); ++ } ++ } ++ color: { ++ if (!modelData.enabled) ++ return Colours.palette.m3outline; ++ if (VPN.status.state === "connected") ++ return Colours.palette.m3primary; ++ if (VPN.status.state === "error") ++ return Colours.palette.m3error; ++ if (VPN.status.state === "needs-auth") ++ return Colours.palette.m3tertiary; ++ return Colours.palette.m3onSurface; + } +- color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + font.weight: modelData.enabled && VPN.connected ? 500 : 400 + elide: Text.ElideRight + } +@@ -175,14 +205,13 @@ ColumnLayout { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) + + StateLayer { +- enabled: !VPN.connecting +- function onClicked(): void { ++ onClicked: { + const clickedIndex = modelData.index; + + if (modelData.enabled) { +@@ -193,8 +222,8 @@ ColumnLayout { + VPN.toggle(); + } else { + const providers = []; +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { +- const p = Config.utilities.vpn.provider[i]; ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ const p = GlobalConfig.utilities.vpn.provider[i]; + if (typeof p === "object") { + const newProvider = { + name: p.name, +@@ -202,13 +231,18 @@ ColumnLayout { + interface: p.interface, + enabled: (i === clickedIndex) + }; ++ if (p.connectCmd && p.connectCmd.length > 0) { ++ newProvider.connectCmd = p.connectCmd; ++ } ++ if (p.disconnectCmd && p.disconnectCmd.length > 0) { ++ newProvider.disconnectCmd = p.disconnectCmd; ++ } + providers.push(newProvider); + } else { + providers.push(p); + } + } +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + + Qt.callLater(function () { + VPN.toggle(); +@@ -216,6 +250,8 @@ ColumnLayout { + } + } + } ++ ++ enabled: !VPN.connecting + } + + MaterialIcon { +@@ -229,21 +265,33 @@ ColumnLayout { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: deleteIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: "transparent" + + StateLayer { +- function onClicked(): void { ++ onClicked: { + const providers = []; +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + if (i !== modelData.index) { +- providers.push(Config.utilities.vpn.provider[i]); ++ const p = GlobalConfig.utilities.vpn.provider[i]; ++ const reconstructed = { ++ name: p.name, ++ displayName: p.displayName, ++ interface: p.interface, ++ enabled: p.enabled ++ }; ++ if (p.connectCmd && p.connectCmd.length > 0) { ++ reconstructed.connectCmd = p.connectCmd; ++ } ++ if (p.disconnectCmd && p.disconnectCmd.length > 0) { ++ reconstructed.disconnectCmd = p.disconnectCmd; ++ } ++ providers.push(reconstructed); + } + } +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + } + } + +@@ -256,8 +304,6 @@ ColumnLayout { + } + } + } +- +- implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + } +@@ -270,56 +316,8 @@ ColumnLayout { + property string providerName: "" + property string displayName: "" + property string interfaceName: "" +- +- parent: Overlay.overlay +- x: Math.round((parent.width - width) / 2) +- y: Math.round((parent.height - height) / 2) +- implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) +- padding: Appearance.padding.large * 1.5 +- +- modal: true +- closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside +- +- opacity: 0 +- scale: 0.7 +- +- enter: Transition { +- ParallelAnimation { +- Anim { +- property: "opacity" +- from: 0 +- to: 1 +- duration: Appearance.anim.durations.normal +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- Anim { +- property: "scale" +- from: 0.7 +- to: 1 +- duration: Appearance.anim.durations.normal +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- } +- } +- +- exit: Transition { +- ParallelAnimation { +- Anim { +- property: "opacity" +- from: 1 +- to: 0 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- Anim { +- property: "scale" +- from: 1 +- to: 0.7 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- } +- } ++ property string connectCmd: "" ++ property string disconnectCmd: "" + + function showProviderSelection(): void { + currentState = "selection"; +@@ -335,6 +333,8 @@ ColumnLayout { + providerName = providerType; + displayName = defaultDisplayName; + interfaceName = ""; ++ connectCmd = ""; ++ disconnectCmd = ""; + + if (currentState === "selection") { + transitionToForm.start(); +@@ -346,59 +346,77 @@ ColumnLayout { + } + + function showEditForm(index: int): void { +- const provider = Config.utilities.vpn.provider[index]; ++ const provider = GlobalConfig.utilities.vpn.provider[index]; + const isObject = typeof provider === "object"; + + editIndex = index; + providerName = isObject ? (provider.name || "custom") : String(provider); + displayName = isObject ? (provider.displayName || providerName) : providerName; + interfaceName = isObject ? (provider.interface || "") : ""; ++ connectCmd = isObject && provider.connectCmd ? provider.connectCmd.join(" ") : ""; ++ disconnectCmd = isObject && provider.disconnectCmd ? provider.disconnectCmd.join(" ") : ""; + + currentState = "form"; + open(); + } + +- Overlay.modal: Rectangle { +- color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) +- } ++ parent: Overlay.overlay ++ x: Math.round((parent.width - width) / 2) ++ y: Math.round((parent.height - height) / 2) ++ implicitWidth: Math.min(400, parent.width - Tokens.padding.large * 2) ++ padding: Tokens.padding.large * 1.5 + +- onClosed: { +- currentState = "selection"; +- } ++ modal: true ++ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside + +- SequentialAnimation { +- id: transitionToForm ++ opacity: 0 ++ scale: 0.7 + ++ enter: Transition { + ParallelAnimation { + Anim { +- target: selectionContent + property: "opacity" +- to: 0 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ from: 0 ++ to: 1 ++ type: Anim.Emphasized + } +- } +- +- ScriptAction { +- script: { +- vpnDialog.currentState = "form"; ++ Anim { ++ property: "scale" ++ from: 0.7 ++ to: 1 ++ type: Anim.Emphasized + } + } ++ } + ++ exit: Transition { + ParallelAnimation { + Anim { +- target: formContent + property: "opacity" +- to: 1 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ from: 1 ++ to: 0 ++ type: Anim.EmphasizedSmall ++ } ++ Anim { ++ property: "scale" ++ from: 1 ++ to: 0.7 ++ type: Anim.EmphasizedSmall + } + } + } + ++ Overlay.modal: Rectangle { ++ color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) ++ } ++ ++ onClosed: { ++ currentState = "selection"; ++ } ++ + background: StyledRect { + color: Colours.palette.m3surfaceContainerHigh +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + + Elevation { + anchors.fill: parent +@@ -409,8 +427,7 @@ ColumnLayout { + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.normal +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + } +@@ -420,8 +437,7 @@ ColumnLayout { + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.normal +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + +@@ -429,20 +445,19 @@ ColumnLayout { + id: selectionContent + + anchors.fill: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + visible: vpnDialog.currentState === "selection" + opacity: vpnDialog.currentState === "selection" ? 1 : 0 + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.EmphasizedSmall + } + } + + StyledText { + text: qsTr("Add VPN Provider") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -451,27 +466,26 @@ ColumnLayout { + text: qsTr("Choose a provider to add") + wrapMode: Text.WordWrap + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + TextButton { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + Layout.fillWidth: true + text: qsTr("NetBird") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + const providers = []; +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { +- providers.push(Config.utilities.vpn.provider[i]); ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ providers.push(GlobalConfig.utilities.vpn.provider[i]); + } + providers.push({ + name: "netbird", + displayName: "NetBird", + interface: "wt0" + }); +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + vpnDialog.closeWithAnimation(); + } + } +@@ -483,16 +497,15 @@ ColumnLayout { + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + const providers = []; +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { +- providers.push(Config.utilities.vpn.provider[i]); ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ providers.push(GlobalConfig.utilities.vpn.provider[i]); + } + providers.push({ + name: "tailscale", + displayName: "Tailscale", + interface: "tailscale0" + }); +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + vpnDialog.closeWithAnimation(); + } + } +@@ -504,23 +517,22 @@ ColumnLayout { + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + const providers = []; +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { +- providers.push(Config.utilities.vpn.provider[i]); ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ providers.push(GlobalConfig.utilities.vpn.provider[i]); + } + providers.push({ + name: "warp", + displayName: "Cloudflare WARP", + interface: "CloudflareWARP" + }); +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + vpnDialog.closeWithAnimation(); + } + } + + TextButton { + Layout.fillWidth: true +- text: qsTr("WireGuard (Custom)") ++ text: qsTr("WireGuard") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { +@@ -529,7 +541,17 @@ ColumnLayout { + } + + TextButton { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.fillWidth: true ++ text: qsTr("Custom") ++ inactiveColour: Colours.tPalette.m3surfaceContainerHigh ++ inactiveOnColour: Colours.palette.m3onSurface ++ onClicked: { ++ vpnDialog.showAddForm("custom", "Custom VPN"); ++ } ++ } ++ ++ TextButton { ++ Layout.topMargin: Tokens.spacing.normal + Layout.fillWidth: true + text: qsTr("Cancel") + inactiveColour: Colours.palette.m3secondaryContainer +@@ -542,30 +564,29 @@ ColumnLayout { + id: formContent + + anchors.fill: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + visible: vpnDialog.currentState === "form" + opacity: vpnDialog.currentState === "form" ? 1 : 0 + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.EmphasizedSmall + } + } + + StyledText { + text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller / 2 ++ spacing: Tokens.spacing.smaller / 2 + + StyledText { + text: qsTr("Display Name") +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + +@@ -573,7 +594,7 @@ ColumnLayout { + Layout.fillWidth: true + implicitHeight: 40 + color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + border.width: 1 + border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + +@@ -586,8 +607,9 @@ ColumnLayout { + + StyledTextField { + id: displayNameField ++ + anchors.centerIn: parent +- width: parent.width - Appearance.padding.normal ++ width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.displayName + onTextChanged: vpnDialog.displayName = text +@@ -597,11 +619,11 @@ ColumnLayout { + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller / 2 ++ spacing: Tokens.spacing.smaller / 2 + + StyledText { + text: qsTr("Interface (e.g., wg0, torguard)") +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + +@@ -609,7 +631,7 @@ ColumnLayout { + Layout.fillWidth: true + implicitHeight: 40 + color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + border.width: 1 + border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + +@@ -622,8 +644,9 @@ ColumnLayout { + + StyledTextField { + id: interfaceNameField ++ + anchors.centerIn: parent +- width: parent.width - Appearance.padding.normal ++ width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.interfaceName + onTextChanged: vpnDialog.interfaceName = text +@@ -631,10 +654,86 @@ ColumnLayout { + } + } + ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.smaller / 2 ++ visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") ++ ++ StyledText { ++ text: qsTr("Connect Command (e.g., wg-quick up wg0)") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ ++ StyledRect { ++ Layout.fillWidth: true ++ implicitHeight: 40 ++ color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) ++ radius: Tokens.rounding.small ++ border.width: 1 ++ border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) ++ ++ Behavior on color { ++ CAnim {} ++ } ++ Behavior on border.color { ++ CAnim {} ++ } ++ ++ StyledTextField { ++ id: connectCmdField ++ ++ anchors.centerIn: parent ++ width: parent.width - Tokens.padding.normal ++ horizontalAlignment: TextInput.AlignLeft ++ text: vpnDialog.connectCmd ++ onTextChanged: vpnDialog.connectCmd = text ++ } ++ } ++ } ++ ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.smaller / 2 ++ visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") ++ ++ StyledText { ++ text: qsTr("Disconnect Command (e.g., wg-quick down wg0)") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ ++ StyledRect { ++ Layout.fillWidth: true ++ implicitHeight: 40 ++ color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) ++ radius: Tokens.rounding.small ++ border.width: 1 ++ border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) ++ ++ Behavior on color { ++ CAnim {} ++ } ++ Behavior on border.color { ++ CAnim {} ++ } ++ ++ StyledTextField { ++ id: disconnectCmdField ++ ++ anchors.centerIn: parent ++ width: parent.width - Tokens.padding.normal ++ horizontalAlignment: TextInput.AlignLeft ++ text: vpnDialog.disconnectCmd ++ onTextChanged: vpnDialog.disconnectCmd = text ++ } ++ } ++ } ++ + RowLayout { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + TextButton { + Layout.fillWidth: true +@@ -647,40 +746,85 @@ ColumnLayout { + TextButton { + Layout.fillWidth: true + text: qsTr("Save") +- enabled: vpnDialog.interfaceName.length > 0 ++ enabled: { ++ const hasCommands = vpnDialog.connectCmd.length > 0 || vpnDialog.disconnectCmd.length > 0; ++ if (hasCommands) { ++ return vpnDialog.interfaceName.length > 0 && vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; ++ } ++ return vpnDialog.interfaceName.length > 0; ++ } + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer + + onClicked: { + const providers = []; ++ const hasCommands = vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; + const newProvider = { +- name: vpnDialog.providerName, + displayName: vpnDialog.displayName || vpnDialog.interfaceName, +- interface: vpnDialog.interfaceName ++ enabled: false, ++ interface: vpnDialog.interfaceName, ++ name: vpnDialog.providerName + }; + ++ if (hasCommands) { ++ newProvider.connectCmd = vpnDialog.connectCmd.split(" ").filter(s => s.length > 0); ++ newProvider.disconnectCmd = vpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); ++ } ++ + if (vpnDialog.editIndex >= 0) { +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { ++ const oldProvider = GlobalConfig.utilities.vpn.provider[vpnDialog.editIndex]; ++ if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) { ++ newProvider.enabled = oldProvider.enabled; ++ } ++ ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + if (i === vpnDialog.editIndex) { + providers.push(newProvider); + } else { +- providers.push(Config.utilities.vpn.provider[i]); ++ providers.push(GlobalConfig.utilities.vpn.provider[i]); + } + } + } else { +- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { +- providers.push(Config.utilities.vpn.provider[i]); ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ providers.push(GlobalConfig.utilities.vpn.provider[i]); + } + providers.push(newProvider); + } + +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + vpnDialog.closeWithAnimation(); + } + } + } + } + } ++ ++ SequentialAnimation { ++ id: transitionToForm ++ ++ ParallelAnimation { ++ Anim { ++ target: selectionContent ++ property: "opacity" ++ to: 0 ++ type: Anim.EmphasizedSmall ++ } ++ } ++ ++ ScriptAction { ++ script: { ++ vpnDialog.currentState = "form"; ++ } ++ } ++ ++ ParallelAnimation { ++ Anim { ++ target: formContent ++ property: "opacity" ++ to: 1 ++ type: Anim.EmphasizedSmall ++ } ++ } ++ } + } + } +diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml +index 49d801d9..064f32a1 100644 +--- a/modules/controlcenter/network/VpnSettings.qml ++++ b/modules/controlcenter/network/VpnSettings.qml +@@ -2,23 +2,23 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Controls ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Controls +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property Session session + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "vpn_key" +@@ -26,7 +26,7 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("General") + description: qsTr("VPN configuration") + } +@@ -34,32 +34,31 @@ ColumnLayout { + SectionContainer { + ToggleRow { + label: qsTr("VPN enabled") +- checked: Config.utilities.vpn.enabled ++ checked: GlobalConfig.utilities.vpn.enabled + toggle.onToggled: { +- Config.utilities.vpn.enabled = checked; +- Config.save(); ++ GlobalConfig.utilities.vpn.enabled = checked; + } + } + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Providers") + description: qsTr("Manage VPN providers") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ contentSpacing: Tokens.spacing.normal + + ListView { + Layout.fillWidth: true + Layout.preferredHeight: contentHeight + + interactive: false +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + model: ScriptModel { +- values: Config.utilities.vpn.provider.map((provider, index) => { ++ values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { + const isObject = typeof provider === "object"; + const name = isObject ? (provider.name || "custom") : String(provider); + const displayName = isObject ? (provider.displayName || name) : name; +@@ -82,19 +81,20 @@ ColumnLayout { + required property int index + + width: ListView.view ? ListView.view.width : undefined ++ implicitHeight: 60 + color: Colours.tPalette.m3surfaceContainerHigh +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: modelData.isActive ? "vpn_key" : "vpn_key_off" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline + } + +@@ -109,53 +109,81 @@ ColumnLayout { + + StyledText { + text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface")) +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + } + } + + IconButton { + icon: modelData.isActive ? "arrow_downward" : "arrow_upward" +- visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1 ++ visible: !modelData.isActive || GlobalConfig.utilities.vpn.provider.length > 1 + onClicked: { +- if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) { ++ const providers = []; ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ const p = GlobalConfig.utilities.vpn.provider[i]; ++ const reconstructed = { ++ name: p.name, ++ displayName: p.displayName, ++ interface: p.interface, ++ enabled: p.enabled ++ }; ++ if (p.connectCmd && p.connectCmd.length > 0) { ++ reconstructed.connectCmd = p.connectCmd; ++ } ++ if (p.disconnectCmd && p.disconnectCmd.length > 0) { ++ reconstructed.disconnectCmd = p.disconnectCmd; ++ } ++ providers.push(reconstructed); ++ } ++ ++ if (modelData.isActive && index < providers.length - 1) { + // Move down +- const providers = [...Config.utilities.vpn.provider]; + const temp = providers[index]; + providers[index] = providers[index + 1]; + providers[index + 1] = temp; +- Config.utilities.vpn.provider = providers; +- Config.save(); + } else if (!modelData.isActive) { + // Make active (move to top) +- const providers = [...Config.utilities.vpn.provider]; + const provider = providers.splice(index, 1)[0]; + providers.unshift(provider); +- Config.utilities.vpn.provider = providers; +- Config.save(); + } ++ ++ GlobalConfig.utilities.vpn.provider = providers; + } + } + + IconButton { + icon: "delete" + onClicked: { +- const providers = [...Config.utilities.vpn.provider]; +- providers.splice(index, 1); +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ const providers = []; ++ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { ++ if (i !== index) { ++ const p = GlobalConfig.utilities.vpn.provider[i]; ++ const reconstructed = { ++ name: p.name, ++ displayName: p.displayName, ++ interface: p.interface, ++ enabled: p.enabled ++ }; ++ if (p.connectCmd && p.connectCmd.length > 0) { ++ reconstructed.connectCmd = p.connectCmd; ++ } ++ if (p.disconnectCmd && p.disconnectCmd.length > 0) { ++ reconstructed.disconnectCmd = p.disconnectCmd; ++ } ++ providers.push(reconstructed); ++ } ++ } ++ GlobalConfig.utilities.vpn.provider = providers; + } + } + } +- +- implicitHeight: 60 + } + } + } + + TextButton { + Layout.fillWidth: true +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + text: qsTr("+ Add Provider") + inactiveColour: Colours.palette.m3primaryContainer + inactiveOnColour: Colours.palette.m3onPrimaryContainer +@@ -167,13 +195,13 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Quick Add") + description: qsTr("Add common VPN providers") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.smaller ++ contentSpacing: Tokens.spacing.smaller + + TextButton { + Layout.fillWidth: true +@@ -182,14 +210,13 @@ ColumnLayout { + inactiveOnColour: Colours.palette.m3onSurface + + onClicked: { +- const providers = [...Config.utilities.vpn.provider]; ++ const providers = [...GlobalConfig.utilities.vpn.provider]; + providers.push({ + name: "netbird", + displayName: "NetBird", + interface: "wt0" + }); +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + } + } + +@@ -200,14 +227,13 @@ ColumnLayout { + inactiveOnColour: Colours.palette.m3onSurface + + onClicked: { +- const providers = [...Config.utilities.vpn.provider]; ++ const providers = [...GlobalConfig.utilities.vpn.provider]; + providers.push({ + name: "tailscale", + displayName: "Tailscale", + interface: "tailscale0" + }); +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + } + } + +@@ -218,14 +244,13 @@ ColumnLayout { + inactiveOnColour: Colours.palette.m3onSurface + + onClicked: { +- const providers = [...Config.utilities.vpn.provider]; ++ const providers = [...GlobalConfig.utilities.vpn.provider]; + providers.push({ + name: "warp", + displayName: "Cloudflare WARP", + interface: "CloudflareWARP" + }); +- Config.utilities.vpn.provider = providers; +- Config.save(); ++ GlobalConfig.utilities.vpn.provider = providers; + } + } + } +diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml +index e8777cdf..e39744ca 100644 +--- a/modules/controlcenter/network/WirelessDetails.qml ++++ b/modules/controlcenter/network/WirelessDetails.qml +@@ -3,15 +3,15 @@ pragma ComponentBehavior: Bound + import ".." + import "../components" + import "." ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config + import qs.utils +-import QtQuick +-import QtQuick.Layouts + + DeviceDetails { + id: root +@@ -19,66 +19,12 @@ DeviceDetails { + required property Session session + readonly property var network: root.session.network.active + +- device: network +- +- Component.onCompleted: { +- updateDeviceDetails(); +- checkSavedProfile(); +- } +- +- onNetworkChanged: { +- connectionUpdateTimer.stop(); +- if (network && network.ssid) { +- connectionUpdateTimer.start(); +- } +- updateDeviceDetails(); +- checkSavedProfile(); +- } +- + function checkSavedProfile(): void { + if (network && network.ssid) { + Nmcli.loadSavedConnections(() => {}); + } + } + +- Connections { +- target: Nmcli +- function onActiveChanged() { +- updateDeviceDetails(); +- } +- function onWirelessDeviceDetailsChanged() { +- if (network && network.ssid) { +- const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); +- if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { +- connectionUpdateTimer.stop(); +- } +- } +- } +- } +- +- Timer { +- id: connectionUpdateTimer +- interval: 500 +- repeat: true +- running: network && network.ssid +- onTriggered: { +- if (network) { +- const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); +- if (isActive) { +- if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { +- Nmcli.getWirelessDeviceDetails("", () => {}); +- } else { +- connectionUpdateTimer.stop(); +- } +- } else { +- if (Nmcli.wirelessDeviceDetails !== null) { +- Nmcli.wirelessDeviceDetails = null; +- } +- } +- } +- } +- } +- + function updateDeviceDetails(): void { + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); +@@ -92,6 +38,22 @@ DeviceDetails { + } + } + ++ device: network ++ ++ Component.onCompleted: { ++ updateDeviceDetails(); ++ checkSavedProfile(); ++ } ++ ++ onNetworkChanged: { ++ connectionUpdateTimer.stop(); ++ if (network && network.ssid) { ++ connectionUpdateTimer.start(); ++ } ++ updateDeviceDetails(); ++ checkSavedProfile(); ++ } ++ + headerComponent: Component { + ConnectionHeader { + icon: root.network?.isSecure ? "lock" : "wifi" +@@ -102,7 +64,7 @@ DeviceDetails { + sections: [ + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Connection status") +@@ -124,8 +86,8 @@ DeviceDetails { + + TextButton { + Layout.fillWidth: true +- Layout.topMargin: Appearance.spacing.normal +- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 ++ Layout.topMargin: Tokens.spacing.normal ++ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + visible: { + if (!root.network || !root.network.ssid) { + return false; +@@ -150,7 +112,7 @@ DeviceDetails { + }, + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Network properties") +@@ -158,7 +120,7 @@ DeviceDetails { + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("SSID") +@@ -193,7 +155,7 @@ DeviceDetails { + }, + Component { + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Connection information") +@@ -208,4 +170,44 @@ DeviceDetails { + } + } + ] ++ ++ Connections { ++ function onActiveChanged() { ++ updateDeviceDetails(); ++ } ++ function onWirelessDeviceDetailsChanged() { ++ if (network && network.ssid) { ++ const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); ++ if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { ++ connectionUpdateTimer.stop(); ++ } ++ } ++ } ++ ++ target: Nmcli ++ } ++ ++ Timer { ++ id: connectionUpdateTimer ++ ++ interval: 500 ++ repeat: true ++ running: network && network.ssid ++ onTriggered: { ++ if (network) { ++ const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); ++ if (isActive) { ++ if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { ++ Nmcli.getWirelessDeviceDetails("", () => {}); ++ } else { ++ connectionUpdateTimer.stop(); ++ } ++ } else { ++ if (Nmcli.wirelessDeviceDetails !== null) { ++ Nmcli.wirelessDeviceDetails = null; ++ } ++ } ++ } ++ } ++ } + } +diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml +index 57a155fd..b6acd079 100644 +--- a/modules/controlcenter/network/WirelessList.qml ++++ b/modules/controlcenter/network/WirelessList.qml +@@ -3,22 +3,28 @@ pragma ComponentBehavior: Bound + import ".." + import "../components" + import "." ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + DeviceList { + id: root + + required property Session session + ++ function checkSavedProfileForNetwork(ssid: string): void { ++ if (ssid && ssid.length > 0) { ++ Nmcli.loadSavedConnections(() => {}); ++ } ++ } ++ + title: qsTr("Networks (%1)").arg(Nmcli.networks.length) + description: qsTr("All available WiFi networks") + activeItem: session.network.active +@@ -28,7 +34,7 @@ DeviceList { + visible: Nmcli.scanning + text: qsTr("Scanning...") + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + +@@ -42,11 +48,11 @@ DeviceList { + + headerComponent: Component { + RowLayout { +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Settings") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -58,9 +64,9 @@ DeviceList { + toggled: Nmcli.wifiEnabled + icon: "wifi" + accent: "Tertiary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + + onClicked: { + Nmcli.toggleWifi(null); +@@ -71,9 +77,9 @@ DeviceList { + toggled: Nmcli.scanning + icon: "wifi_find" + accent: "Secondary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + + onClicked: { + Nmcli.rescanWifi(); +@@ -84,9 +90,9 @@ DeviceList { + toggled: !root.session.network.active + icon: "settings" + accent: "Primary" +- iconSize: Appearance.font.size.normal +- horizontalPadding: Appearance.padding.normal +- verticalPadding: Appearance.padding.smaller ++ iconSize: Tokens.font.size.normal ++ horizontalPadding: Tokens.padding.normal ++ verticalPadding: Tokens.padding.smaller + + onClicked: { + if (root.session.network.active) +@@ -104,12 +110,13 @@ DeviceList { + required property var modelData + + width: ListView.view ? ListView.view.width : undefined ++ implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + StateLayer { +- function onClicked(): void { ++ onClicked: { + root.session.network.active = modelData; + if (modelData && modelData.ssid) { + root.checkSavedProfileForNetwork(modelData.ssid); +@@ -123,15 +130,15 @@ DeviceList { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh + + MaterialIcon { +@@ -139,7 +146,7 @@ DeviceList { + + anchors.centerIn: parent + text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: modelData.active ? 1 : 0 + color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + } +@@ -160,7 +167,7 @@ DeviceList { + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + Layout.fillWidth: true +@@ -175,7 +182,7 @@ DeviceList { + return qsTr("Open"); + } + color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + font.weight: modelData.active ? 500 : 400 + elide: Text.ElideRight + } +@@ -184,13 +191,13 @@ DeviceList { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) + + StateLayer { +- function onClicked(): void { ++ onClicked: { + if (modelData.active) { + Nmcli.disconnectFromNetwork(); + } else { +@@ -208,8 +215,6 @@ DeviceList { + } + } + } +- +- implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + } + } + +@@ -219,10 +224,4 @@ DeviceList { + checkSavedProfileForNetwork(item.ssid); + } + } +- +- function checkSavedProfileForNetwork(ssid: string): void { +- if (ssid && ssid.length > 0) { +- Nmcli.loadSavedConnections(() => {}); +- } +- } + } +diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml +index 8150af9c..43689176 100644 +--- a/modules/controlcenter/network/WirelessPane.qml ++++ b/modules/controlcenter/network/WirelessPane.qml +@@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components + import qs.components.containers +-import qs.config +-import Quickshell.Widgets +-import QtQuick + + SplitPaneWithDetails { + id: root +diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml +index 7ad5204a..ac88f9fc 100644 +--- a/modules/controlcenter/network/WirelessPasswordDialog.qml ++++ b/modules/controlcenter/network/WirelessPasswordDialog.qml +@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound + + import ".." + import "." ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root +@@ -29,6 +29,47 @@ Item { + } + + property bool isClosing: false ++ ++ function checkConnectionStatus(): void { ++ if (!root.visible || !connectButton.connecting) { ++ return; ++ } ++ ++ const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); ++ ++ if (isConnected) { ++ connectionSuccessTimer.start(); ++ return; ++ } ++ ++ if (Nmcli.pendingConnection === null && connectButton.connecting) { ++ if (connectionMonitor.repeatCount > 10) { ++ connectionMonitor.stop(); ++ connectButton.connecting = false; ++ connectButton.hasError = true; ++ connectButton.enabled = true; ++ connectButton.text = qsTr("Connect"); ++ passwordContainer.passwordBuffer = ""; ++ if (root.network && root.network.ssid) { ++ Nmcli.forgetNetwork(root.network.ssid); ++ } ++ } ++ } ++ } ++ ++ function closeDialog(): void { ++ if (isClosing) { ++ return; ++ } ++ ++ isClosing = true; ++ passwordContainer.passwordBuffer = ""; ++ connectButton.connecting = false; ++ connectButton.hasError = false; ++ connectButton.text = qsTr("Connect"); ++ connectionMonitor.stop(); ++ } ++ + visible: session.network.showPasswordDialog || isClosing + enabled: session.network.showPasswordDialog && !isClosing + focus: enabled +@@ -58,12 +99,13 @@ Item { + anchors.centerIn: parent + + implicitWidth: 400 +- implicitHeight: content.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: content.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surface + opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 + scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 ++ Keys.onEscapePressed: closeDialog() + + Behavior on opacity { + Anim {} +@@ -94,28 +136,26 @@ Item { + } + } + +- Keys.onEscapePressed: closeDialog() +- + ColumnLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "lock" +- font.pointSize: Appearance.font.size.extraLarge * 2 ++ font.pointSize: Tokens.font.size.extraLarge * 2 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enter password") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -123,14 +163,14 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { + id: statusText + + Layout.alignment: Qt.AlignHCenter +- Layout.topMargin: Appearance.spacing.small ++ Layout.topMargin: Tokens.spacing.small + visible: connectButton.connecting || connectButton.hasError + text: { + if (connectButton.hasError) { +@@ -142,24 +182,31 @@ Item { + return ""; + } + color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + font.weight: 400 + wrapMode: Text.WordWrap +- Layout.maximumWidth: parent.width - Appearance.padding.large * 2 ++ Layout.maximumWidth: parent.width - Tokens.padding.large * 2 + } + + Item { + id: passwordContainer +- Layout.topMargin: Appearance.spacing.large +- Layout.fillWidth: true +- implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + ++ property string passwordBuffer: "" ++ ++ Layout.topMargin: Tokens.spacing.large ++ Layout.fillWidth: true ++ implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) + focus: true + Keys.onPressed: event => { + if (!activeFocus) { + forceActiveFocus(); + } + ++ if (event.key === Qt.Key_Escape) { ++ event.accepted = false; ++ closeDialog(); ++ } ++ + if (connectButton.hasError && event.text && event.text.length > 0) { + connectButton.hasError = false; + } +@@ -177,15 +224,16 @@ Item { + } + event.accepted = true; + } else if (event.text && event.text.length > 0) { ++ if (event.key === Qt.Key_Tab) { ++ event.accepted = false; ++ return; ++ } + passwordBuffer += event.text; + event.accepted = true; + } + } + +- property string passwordBuffer: "" +- + Connections { +- target: root.session.network + function onShowPasswordDialogChanged(): void { + if (root.session.network.showPasswordDialog) { + Qt.callLater(() => { +@@ -195,10 +243,11 @@ Item { + }); + } + } ++ ++ target: root.session.network + } + + Connections { +- target: root + function onVisibleChanged(): void { + if (root.visible) { + Qt.callLater(() => { +@@ -206,11 +255,13 @@ Item { + }); + } + } ++ ++ target: root + } + + StyledRect { + anchors.fill: parent +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer + border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) + border.color: { +@@ -237,21 +288,22 @@ Item { + } + + StateLayer { +- hoverEnabled: false +- cursorShape: Qt.IBeamCursor +- +- function onClicked(): void { ++ onClicked: { + passwordContainer.forceActiveFocus(); + } ++ ++ hoverEnabled: false ++ cursorShape: Qt.IBeamCursor + } + + StyledText { + id: placeholder ++ + anchors.centerIn: parent + text: qsTr("Password") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.normal ++ font.family: Tokens.font.family.mono + opacity: passwordContainer.passwordBuffer ? 0 : 1 + + Behavior on opacity { +@@ -266,10 +318,10 @@ Item { + + anchors.centerIn: parent + implicitWidth: fullWidth +- implicitHeight: Appearance.font.size.normal ++ implicitHeight: Tokens.font.size.normal + + orientation: Qt.Horizontal +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + interactive: false + + model: ScriptModel { +@@ -283,7 +335,7 @@ Item { + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface +- radius: Appearance.rounding.small / 2 ++ radius: Tokens.rounding.small / 2 + + opacity: 0 + scale: 0 +@@ -326,8 +378,7 @@ Item { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +@@ -339,15 +390,15 @@ Item { + } + + RowLayout { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + TextButton { + id: cancelButton + + Layout.fillWidth: true +- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 ++ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + inactiveColour: Colours.palette.m3secondaryContainer + inactiveOnColour: Colours.palette.m3onSecondaryContainer + text: qsTr("Cancel") +@@ -362,7 +413,7 @@ Item { + property bool hasError: false + + Layout.fillWidth: true +- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 ++ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 + inactiveColour: Colours.palette.m3primary + inactiveOnColour: Colours.palette.m3onPrimary + text: qsTr("Connect") +@@ -414,40 +465,14 @@ Item { + } + } + +- function checkConnectionStatus(): void { +- if (!root.visible || !connectButton.connecting) { +- return; +- } +- +- const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); +- +- if (isConnected) { +- connectionSuccessTimer.start(); +- return; +- } +- +- if (Nmcli.pendingConnection === null && connectButton.connecting) { +- if (connectionMonitor.repeatCount > 10) { +- connectionMonitor.stop(); +- connectButton.connecting = false; +- connectButton.hasError = true; +- connectButton.enabled = true; +- connectButton.text = qsTr("Connect"); +- passwordContainer.passwordBuffer = ""; +- if (root.network && root.network.ssid) { +- Nmcli.forgetNetwork(root.network.ssid); +- } +- } +- } +- } +- + Timer { + id: connectionMonitor ++ ++ property int repeatCount: 0 ++ + interval: 1000 + repeat: true + triggeredOnStart: false +- property int repeatCount: 0 +- + onTriggered: { + repeatCount++; + checkConnectionStatus(); +@@ -462,6 +487,7 @@ Item { + + Timer { + id: connectionSuccessTimer ++ + interval: 500 + onTriggered: { + if (root.visible && Nmcli.active && Nmcli.active.ssid) { +@@ -477,7 +503,6 @@ Item { + } + + Connections { +- target: Nmcli + function onActiveChanged() { + if (root.visible) { + checkConnectionStatus(); +@@ -494,18 +519,7 @@ Item { + Nmcli.forgetNetwork(ssid); + } + } +- } +- +- function closeDialog(): void { +- if (isClosing) { +- return; +- } + +- isClosing = true; +- passwordContainer.passwordBuffer = ""; +- connectButton.connecting = false; +- connectButton.hasError = false; +- connectButton.text = qsTr("Connect"); +- connectionMonitor.stop(); ++ target: Nmcli + } + } +diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml +index b4eb391d..f3f1fd4b 100644 +--- a/modules/controlcenter/network/WirelessSettings.qml ++++ b/modules/controlcenter/network/WirelessSettings.qml +@@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property Session session + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + SettingsHeader { + icon: "wifi" +@@ -23,7 +23,7 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("WiFi status") + description: qsTr("General WiFi settings") + } +@@ -39,13 +39,13 @@ ColumnLayout { + } + + SectionHeader { +- Layout.topMargin: Appearance.spacing.large ++ Layout.topMargin: Tokens.spacing.large + title: qsTr("Network information") + description: qsTr("Current network connection") + } + + SectionContainer { +- contentSpacing: Appearance.spacing.small / 2 ++ contentSpacing: Tokens.spacing.small / 2 + + PropertyRow { + label: qsTr("Connected network") +diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml +new file mode 100644 +index 00000000..8f04bc3b +--- /dev/null ++++ b/modules/controlcenter/notifications/NotificationsPane.qml +@@ -0,0 +1,607 @@ ++pragma ComponentBehavior: Bound ++ ++import ".." ++import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components ++import qs.components.containers ++import qs.components.controls ++import qs.components.effects ++import qs.services ++ ++Item { ++ id: root ++ ++ required property Session session ++ property string activeSection: "notifications" ++ ++ property bool notificationsExpire: GlobalConfig.notifs.expire ?? true ++ property string notificationsFullscreen: GlobalConfig.notifs.fullscreen ?? "on" ++ property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false ++ property int notificationsDefaultExpireTimeout: GlobalConfig.notifs.defaultExpireTimeout ?? 5000 ++ property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3 ++ ++ property int maxToasts: Config.utilities.maxToasts ?? 4 ++ property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off" ++ property bool chargingChanged: GlobalConfig.utilities.toasts.chargingChanged ?? true ++ property bool gameModeChanged: GlobalConfig.utilities.toasts.gameModeChanged ?? true ++ property bool dndChanged: GlobalConfig.utilities.toasts.dndChanged ?? true ++ property bool audioOutputChanged: GlobalConfig.utilities.toasts.audioOutputChanged ?? true ++ property bool audioInputChanged: GlobalConfig.utilities.toasts.audioInputChanged ?? true ++ property bool capsLockChanged: GlobalConfig.utilities.toasts.capsLockChanged ?? true ++ property bool numLockChanged: GlobalConfig.utilities.toasts.numLockChanged ?? true ++ property bool kbLayoutChanged: GlobalConfig.utilities.toasts.kbLayoutChanged ?? true ++ property bool vpnChanged: GlobalConfig.utilities.toasts.vpnChanged ?? true ++ property bool nowPlaying: GlobalConfig.utilities.toasts.nowPlaying ?? false ++ ++ readonly property var sections: [ ++ { ++ id: "notifications", ++ title: qsTr("Notifications"), ++ description: qsTr("Popup behavior"), ++ icon: "notifications" ++ }, ++ { ++ id: "toastSettings", ++ title: qsTr("Toast Settings"), ++ description: qsTr("Toast visibility"), ++ icon: "toast" ++ }, ++ { ++ id: "toastEvents", ++ title: qsTr("Toast Events"), ++ description: qsTr("Event triggers"), ++ icon: "rule" ++ } ++ ] ++ ++ function componentForSection(sectionId) { ++ switch (sectionId) { ++ case "toastSettings": ++ return toastSettingsComponent; ++ case "toastEvents": ++ return toastEventsComponent; ++ case "notifications": ++ default: ++ return notificationsComponent; ++ } ++ } ++ ++ function saveConfig(): void { ++ GlobalConfig.notifs.expire = root.notificationsExpire; ++ GlobalConfig.notifs.fullscreen = root.notificationsFullscreen; ++ GlobalConfig.notifs.openExpanded = root.notificationsOpenExpanded; ++ GlobalConfig.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; ++ GlobalConfig.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; ++ ++ GlobalConfig.utilities.maxToasts = root.maxToasts; ++ GlobalConfig.utilities.toasts.fullscreen = root.toastsFullscreen; ++ GlobalConfig.utilities.toasts.chargingChanged = root.chargingChanged; ++ GlobalConfig.utilities.toasts.gameModeChanged = root.gameModeChanged; ++ GlobalConfig.utilities.toasts.dndChanged = root.dndChanged; ++ GlobalConfig.utilities.toasts.audioOutputChanged = root.audioOutputChanged; ++ GlobalConfig.utilities.toasts.audioInputChanged = root.audioInputChanged; ++ GlobalConfig.utilities.toasts.capsLockChanged = root.capsLockChanged; ++ GlobalConfig.utilities.toasts.numLockChanged = root.numLockChanged; ++ GlobalConfig.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; ++ GlobalConfig.utilities.toasts.vpnChanged = root.vpnChanged; ++ GlobalConfig.utilities.toasts.nowPlaying = root.nowPlaying; ++ } ++ ++ anchors.fill: parent ++ ++ SplitPaneLayout { ++ anchors.fill: parent ++ leftWidthRatio: 0.32 ++ leftMinimumWidth: 300 ++ ++ leftContent: Component { ++ StyledFlickable { ++ id: leftFlickable ++ ++ flickableDirection: Flickable.VerticalFlick ++ contentHeight: leftContentLayout.height ++ ++ StyledScrollBar.vertical: StyledScrollBar { ++ flickable: leftFlickable ++ } ++ ++ ColumnLayout { ++ id: leftContentLayout ++ anchors.left: parent.left ++ anchors.right: parent.right ++ spacing: Tokens.spacing.normal ++ ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.smaller ++ ++ StyledText { ++ text: qsTr("Notifications") ++ font.pointSize: Tokens.font.size.large ++ font.weight: 500 ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ } ++ ++ Repeater { ++ model: root.sections ++ ++ delegate: SectionNavButton { ++ required property var modelData ++ ++ Layout.fillWidth: true ++ section: modelData ++ active: root.activeSection === modelData.id ++ onClicked: root.activeSection = modelData.id ++ } ++ } ++ } ++ } ++ } ++ ++ rightContent: Component { ++ Item { ++ id: rightPaneItem ++ ++ property string paneId: root.activeSection ++ property Component targetComponent: root.componentForSection(root.activeSection) ++ property Component nextComponent: root.componentForSection(root.activeSection) ++ ++ onPaneIdChanged: { ++ nextComponent = root.componentForSection(root.activeSection); ++ } ++ ++ Loader { ++ id: rightLoader ++ anchors.fill: parent ++ asynchronous: true ++ opacity: 1 ++ scale: 1 ++ transformOrigin: Item.Center ++ sourceComponent: rightPaneItem.targetComponent ++ } ++ ++ Behavior on paneId { ++ PaneTransition { ++ target: rightLoader ++ propertyActions: [ ++ PropertyAction { ++ target: rightPaneItem ++ property: "targetComponent" ++ value: rightPaneItem.nextComponent ++ } ++ ] ++ } ++ } ++ } ++ } ++ } ++ ++ Component { ++ id: notificationsComponent ++ ++ SectionPage { ++ title: qsTr("Notifications") ++ subtitle: qsTr("Configure notification popup behavior.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SplitButtonRow { ++ id: notificationsFullscreenSelector ++ ++ function syncActiveItem(): void { ++ active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem; ++ } ++ ++ label: qsTr("Show in fullscreen") ++ menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem] ++ ++ Component.onCompleted: syncActiveItem() ++ ++ Connections { ++ function onNotificationsFullscreenChanged(): void { ++ notificationsFullscreenSelector.syncActiveItem(); ++ } ++ ++ target: root ++ } ++ ++ MenuItem { ++ id: notificationsFullscreenOffItem ++ ++ text: qsTr("Off") ++ icon: "notifications_off" ++ activeText: qsTr("Off") ++ onClicked: { ++ root.notificationsFullscreen = "off"; ++ root.saveConfig(); ++ } ++ } ++ ++ MenuItem { ++ id: notificationsFullscreenOnItem ++ ++ text: qsTr("On") ++ icon: "notifications" ++ activeText: qsTr("On") ++ onClicked: { ++ root.notificationsFullscreen = "on"; ++ root.saveConfig(); ++ } ++ } ++ } ++ ++ SwitchRow { ++ label: qsTr("Expire automatically") ++ checked: root.notificationsExpire ++ onToggled: checked => { ++ root.notificationsExpire = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ label: qsTr("Open expanded") ++ checked: root.notificationsOpenExpanded ++ onToggled: checked => { ++ root.notificationsOpenExpanded = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SpinBoxRow { ++ label: qsTr("Default timeout") ++ value: root.notificationsDefaultExpireTimeout ++ min: 1000 ++ max: 60000 ++ step: 500 ++ onValueModified: value => { ++ root.notificationsDefaultExpireTimeout = value; ++ root.saveConfig(); ++ } ++ } ++ ++ SpinBoxRow { ++ label: qsTr("Group preview count") ++ value: root.notificationsGroupPreviewNum ++ min: 1 ++ max: 10 ++ step: 1 ++ onValueModified: value => { ++ root.notificationsGroupPreviewNum = value; ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } ++ ++ Component { ++ id: toastSettingsComponent ++ ++ SectionPage { ++ title: qsTr("Toast Settings") ++ subtitle: qsTr("Control when toast notifications are shown.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SplitButtonRow { ++ id: toastFullscreenSelector ++ ++ function syncActiveItem(): void { ++ if (root.toastsFullscreen === "all") { ++ active = toastFullscreenAllItem; ++ return; ++ } ++ ++ if (root.toastsFullscreen === "important") { ++ active = toastFullscreenImportantItem; ++ return; ++ } ++ ++ active = toastFullscreenOffItem; ++ } ++ ++ Layout.fillWidth: true ++ z: expanded ? 100 : 0 ++ label: qsTr("Show in fullscreen") ++ menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem] ++ ++ Component.onCompleted: syncActiveItem() ++ Connections { ++ function onToastsFullscreenChanged(): void { ++ toastFullscreenSelector.syncActiveItem(); ++ } ++ ++ target: root ++ } ++ ++ MenuItem { ++ id: toastFullscreenOffItem ++ text: qsTr("Off") ++ icon: "notifications_off" ++ activeText: qsTr("Off") ++ onClicked: { ++ root.toastsFullscreen = "off"; ++ root.saveConfig(); ++ } ++ } ++ ++ MenuItem { ++ id: toastFullscreenImportantItem ++ text: qsTr("Important") ++ icon: "priority_high" ++ activeText: qsTr("Important") ++ onClicked: { ++ root.toastsFullscreen = "important"; ++ root.saveConfig(); ++ } ++ } ++ ++ MenuItem { ++ id: toastFullscreenAllItem ++ text: qsTr("On") ++ icon: "notifications" ++ activeText: qsTr("On") ++ onClicked: { ++ root.toastsFullscreen = "all"; ++ root.saveConfig(); ++ } ++ } ++ } ++ ++ SpinBoxRow { ++ Layout.fillWidth: true ++ label: qsTr("Visible toasts") ++ value: root.maxToasts ++ min: 1 ++ max: 10 ++ step: 1 ++ onValueModified: value => { ++ root.maxToasts = value; ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } ++ ++ Component { ++ id: toastEventsComponent ++ ++ SectionPage { ++ title: qsTr("Toast Events") ++ subtitle: qsTr("Choose which system events create toasts.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ GridLayout { ++ Layout.fillWidth: true ++ columns: 2 ++ columnSpacing: Tokens.spacing.normal ++ rowSpacing: Tokens.spacing.normal ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Charging changes") ++ checked: root.chargingChanged ++ onToggled: checked => { ++ root.chargingChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Game mode changes") ++ checked: root.gameModeChanged ++ onToggled: checked => { ++ root.gameModeChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Do not disturb") ++ checked: root.dndChanged ++ onToggled: checked => { ++ root.dndChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Audio output changes") ++ checked: root.audioOutputChanged ++ onToggled: checked => { ++ root.audioOutputChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Audio input changes") ++ checked: root.audioInputChanged ++ onToggled: checked => { ++ root.audioInputChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Caps lock changes") ++ checked: root.capsLockChanged ++ onToggled: checked => { ++ root.capsLockChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Num lock changes") ++ checked: root.numLockChanged ++ onToggled: checked => { ++ root.numLockChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Keyboard layout changes") ++ checked: root.kbLayoutChanged ++ onToggled: checked => { ++ root.kbLayoutChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("VPN changes") ++ checked: root.vpnChanged ++ onToggled: checked => { ++ root.vpnChanged = checked; ++ root.saveConfig(); ++ } ++ } ++ ++ SwitchRow { ++ Layout.fillWidth: true ++ label: qsTr("Now playing") ++ checked: root.nowPlaying ++ onToggled: checked => { ++ root.nowPlaying = checked; ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ component SectionPage: StyledFlickable { ++ id: sectionPage ++ ++ required property string title ++ property string subtitle: "" ++ default property alias contentItems: contentLayout.data ++ ++ flickableDirection: Flickable.VerticalFlick ++ contentHeight: contentLayout.height ++ ++ StyledScrollBar.vertical: StyledScrollBar { ++ flickable: sectionPage ++ } ++ ++ ColumnLayout { ++ id: contentLayout ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.top: parent.top ++ spacing: Tokens.spacing.normal ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: sectionPage.title ++ font.pointSize: Tokens.font.size.extraLarge ++ font.weight: 600 ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ Layout.bottomMargin: Tokens.spacing.small ++ text: sectionPage.subtitle ++ color: Colours.palette.m3outline ++ visible: text.length > 0 ++ wrapMode: Text.WordWrap ++ } ++ } ++ } ++ ++ component SectionNavButton: StyledRect { ++ id: navButton ++ ++ required property var section ++ property bool active: false ++ ++ signal clicked ++ ++ implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 ++ color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" ++ radius: Tokens.rounding.normal ++ ++ Behavior on color { ++ CAnim {} ++ } ++ ++ StateLayer { ++ onClicked: navButton.clicked() ++ } ++ ++ RowLayout { ++ id: navRow ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.normal ++ ++ MaterialIcon { ++ Layout.alignment: Qt.AlignVCenter ++ text: navButton.section.icon ++ fill: navButton.active ? 1 : 0 ++ color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant ++ font.pointSize: Tokens.font.size.large ++ } ++ ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: 0 ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: navButton.section.title ++ font.weight: navButton.active ? 500 : 400 ++ elide: Text.ElideRight ++ maximumLineCount: 1 ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: navButton.section.description ++ color: Colours.palette.m3outline ++ font.pointSize: Tokens.font.size.small ++ elide: Text.ElideRight ++ maximumLineCount: 1 ++ } ++ } ++ ++ MaterialIcon { ++ Layout.alignment: Qt.AlignVCenter ++ text: "chevron_right" ++ opacity: navButton.active ? 1 : 0 ++ color: Colours.palette.m3primary ++ } ++ } ++ } ++} +diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml +index 8678672d..9b16bfe5 100644 +--- a/modules/controlcenter/state/BluetoothState.qml ++++ b/modules/controlcenter/state/BluetoothState.qml +@@ -1,5 +1,5 @@ +-import Quickshell.Bluetooth + import QtQuick ++import Quickshell.Bluetooth + + QtObject { + id: root +diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml +index d12d1744..c80769ac 100644 +--- a/modules/controlcenter/taskbar/TaskbarPane.qml ++++ b/modules/controlcenter/taskbar/TaskbarPane.qml +@@ -2,24 +2,29 @@ pragma ComponentBehavior: Bound + + import ".." + import "../components" ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components ++import qs.components.containers + import qs.components.controls + import qs.components.effects +-import qs.components.containers + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root + + required property Session session + ++ property string activeSection: "statusIcons" ++ property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false ++ property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false + property bool clockShowIcon: Config.bar.clock.showIcon ?? true ++ property bool clockBackground: Config.bar.clock.background ?? false ++ property bool clockShowDate: Config.bar.clock.showDate ?? false + property bool persistent: Config.bar.persistent ?? true + property bool showOnHover: Config.bar.showOnHover ?? true + property int dragThreshold: Config.bar.dragThreshold ?? 20 +@@ -38,56 +43,131 @@ Item { + property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true + property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false + property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false +- property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true ++ property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 ++ property bool workspacesPerMonitor: GlobalConfig.bar.workspaces.perMonitorWorkspaces ?? true + property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true + property bool scrollVolume: Config.bar.scrollActions.volume ?? true + property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true + property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true + property bool popoutTray: Config.bar.popouts.tray ?? true + property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true +- +- anchors.fill: parent +- +- Component.onCompleted: { +- if (Config.bar.entries) { +- entriesModel.clear(); +- for (let i = 0; i < Config.bar.entries.length; i++) { +- const entry = Config.bar.entries[i]; +- entriesModel.append({ +- id: entry.id, +- enabled: entry.enabled !== false +- }); +- } ++ property list monitorNames: Hypr.monitorNames() ++ property list excludedScreens: Config.bar.excludedScreens ?? [] ++ ++ readonly property var sections: [ ++ { ++ id: "statusIcons", ++ title: qsTr("Status Icons"), ++ description: qsTr("Bar indicator buttons"), ++ icon: "info" ++ }, ++ { ++ id: "workspaces", ++ title: qsTr("Workspaces"), ++ description: qsTr("Workspace button layout"), ++ icon: "workspaces" ++ }, ++ { ++ id: "scrollActions", ++ title: qsTr("Scroll Actions"), ++ description: qsTr("Wheel shortcuts"), ++ icon: "swap_vert" ++ }, ++ { ++ id: "clock", ++ title: qsTr("Clock"), ++ description: qsTr("Date and icon display"), ++ icon: "schedule" ++ }, ++ { ++ id: "behavior", ++ title: qsTr("Bar Behavior"), ++ description: qsTr("Visibility and dragging"), ++ icon: "dock_to_left" ++ }, ++ { ++ id: "activeWindow", ++ title: qsTr("Active Window"), ++ description: qsTr("Window title entry"), ++ icon: "select_window" ++ }, ++ { ++ id: "popouts", ++ title: qsTr("Popouts"), ++ description: qsTr("Hover panels"), ++ icon: "open_in_new" ++ }, ++ { ++ id: "tray", ++ title: qsTr("Tray Settings"), ++ description: qsTr("System tray style"), ++ icon: "apps" ++ }, ++ { ++ id: "monitors", ++ title: qsTr("Monitors"), ++ description: qsTr("Per-screen visibility"), ++ icon: "monitor" ++ } ++ ] ++ ++ function componentForSection(sectionId) { ++ switch (sectionId) { ++ case "workspaces": ++ return workspacesComponent; ++ case "scrollActions": ++ return scrollActionsComponent; ++ case "clock": ++ return clockComponent; ++ case "behavior": ++ return behaviorComponent; ++ case "activeWindow": ++ return activeWindowComponent; ++ case "popouts": ++ return popoutsComponent; ++ case "tray": ++ return trayComponent; ++ case "monitors": ++ return monitorsComponent; ++ case "statusIcons": ++ default: ++ return statusIconsComponent; + } + } + + function saveConfig(entryIndex, entryEnabled) { +- Config.bar.clock.showIcon = root.clockShowIcon; +- Config.bar.persistent = root.persistent; +- Config.bar.showOnHover = root.showOnHover; +- Config.bar.dragThreshold = root.dragThreshold; +- Config.bar.status.showAudio = root.showAudio; +- Config.bar.status.showMicrophone = root.showMicrophone; +- Config.bar.status.showKbLayout = root.showKbLayout; +- Config.bar.status.showNetwork = root.showNetwork; +- Config.bar.status.showWifi = root.showWifi; +- Config.bar.status.showBluetooth = root.showBluetooth; +- Config.bar.status.showBattery = root.showBattery; +- Config.bar.status.showLockStatus = root.showLockStatus; +- Config.bar.tray.background = root.trayBackground; +- Config.bar.tray.compact = root.trayCompact; +- Config.bar.tray.recolour = root.trayRecolour; +- Config.bar.workspaces.shown = root.workspacesShown; +- Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; +- Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; +- Config.bar.workspaces.showWindows = root.workspacesShowWindows; +- Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; +- Config.bar.scrollActions.workspaces = root.scrollWorkspaces; +- Config.bar.scrollActions.volume = root.scrollVolume; +- Config.bar.scrollActions.brightness = root.scrollBrightness; +- Config.bar.popouts.activeWindow = root.popoutActiveWindow; +- Config.bar.popouts.tray = root.popoutTray; +- Config.bar.popouts.statusIcons = root.popoutStatusIcons; ++ GlobalConfig.bar.activeWindow.compact = root.activeWindowCompact; ++ GlobalConfig.bar.activeWindow.inverted = root.activeWindowInverted; ++ GlobalConfig.bar.clock.background = root.clockBackground; ++ GlobalConfig.bar.clock.showDate = root.clockShowDate; ++ GlobalConfig.bar.clock.showIcon = root.clockShowIcon; ++ GlobalConfig.bar.persistent = root.persistent; ++ GlobalConfig.bar.showOnHover = root.showOnHover; ++ GlobalConfig.bar.dragThreshold = root.dragThreshold; ++ GlobalConfig.bar.status.showAudio = root.showAudio; ++ GlobalConfig.bar.status.showMicrophone = root.showMicrophone; ++ GlobalConfig.bar.status.showKbLayout = root.showKbLayout; ++ GlobalConfig.bar.status.showNetwork = root.showNetwork; ++ GlobalConfig.bar.status.showWifi = root.showWifi; ++ GlobalConfig.bar.status.showBluetooth = root.showBluetooth; ++ GlobalConfig.bar.status.showBattery = root.showBattery; ++ GlobalConfig.bar.status.showLockStatus = root.showLockStatus; ++ GlobalConfig.bar.tray.background = root.trayBackground; ++ GlobalConfig.bar.tray.compact = root.trayCompact; ++ GlobalConfig.bar.tray.recolour = root.trayRecolour; ++ GlobalConfig.bar.workspaces.shown = root.workspacesShown; ++ GlobalConfig.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; ++ GlobalConfig.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; ++ GlobalConfig.bar.workspaces.showWindows = root.workspacesShowWindows; ++ GlobalConfig.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; ++ GlobalConfig.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; ++ GlobalConfig.bar.scrollActions.workspaces = root.scrollWorkspaces; ++ GlobalConfig.bar.scrollActions.volume = root.scrollVolume; ++ GlobalConfig.bar.scrollActions.brightness = root.scrollBrightness; ++ GlobalConfig.bar.popouts.activeWindow = root.popoutActiveWindow; ++ GlobalConfig.bar.popouts.tray = root.popoutTray; ++ GlobalConfig.bar.popouts.statusIcons = root.popoutStatusIcons; ++ GlobalConfig.bar.excludedScreens = root.excludedScreens; + + const entries = []; + for (let i = 0; i < entriesModel.count; i++) { +@@ -101,547 +181,718 @@ Item { + enabled: enabled + }); + } +- Config.bar.entries = entries; +- Config.save(); ++ GlobalConfig.bar.entries = entries; ++ } ++ ++ anchors.fill: parent ++ ++ Component.onCompleted: { ++ if (Config.bar.entries) { ++ entriesModel.clear(); ++ for (let i = 0; i < Config.bar.entries.length; i++) { ++ const entry = Config.bar.entries[i]; ++ entriesModel.append({ ++ id: entry.id, ++ enabled: entry.enabled !== false ++ }); ++ } ++ } + } + + ListModel { + id: entriesModel + } + +- ClippingRectangle { +- id: taskbarClippingRect ++ SplitPaneLayout { + anchors.fill: parent +- anchors.margins: Appearance.padding.normal +- anchors.leftMargin: 0 +- anchors.rightMargin: Appearance.padding.normal +- +- radius: taskbarBorder.innerRadius +- color: "transparent" ++ leftWidthRatio: 0.32 ++ leftMinimumWidth: 300 + +- Loader { +- id: taskbarLoader ++ leftContent: Component { ++ StyledFlickable { ++ id: leftFlickable + +- anchors.fill: parent +- anchors.margins: Appearance.padding.large + Appearance.padding.normal +- anchors.leftMargin: Appearance.padding.large +- anchors.rightMargin: Appearance.padding.large ++ flickableDirection: Flickable.VerticalFlick ++ contentHeight: leftContentLayout.height + +- sourceComponent: taskbarContentComponent +- } +- } ++ StyledScrollBar.vertical: StyledScrollBar { ++ flickable: leftFlickable ++ } + +- InnerBorder { +- id: taskbarBorder +- leftThickness: 0 +- rightThickness: Appearance.padding.normal +- } ++ ColumnLayout { ++ id: leftContentLayout + +- Component { +- id: taskbarContentComponent ++ anchors.left: parent.left ++ anchors.right: parent.right ++ spacing: Tokens.spacing.normal + +- StyledFlickable { +- id: sidebarFlickable +- flickableDirection: Flickable.VerticalFlick +- contentHeight: sidebarLayout.height ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.smaller + +- StyledScrollBar.vertical: StyledScrollBar { +- flickable: sidebarFlickable +- } ++ StyledText { ++ text: qsTr("Taskbar") ++ font.pointSize: Tokens.font.size.large ++ font.weight: 500 ++ } + +- ColumnLayout { +- id: sidebarLayout +- anchors.left: parent.left +- anchors.right: parent.right +- anchors.top: parent.top ++ Item { ++ Layout.fillWidth: true ++ } ++ } + +- spacing: Appearance.spacing.normal ++ Repeater { ++ model: root.sections + +- RowLayout { +- spacing: Appearance.spacing.smaller ++ delegate: SectionNavButton { ++ required property var modelData + +- StyledText { +- text: qsTr("Taskbar") +- font.pointSize: Appearance.font.size.large +- font.weight: 500 ++ Layout.fillWidth: true ++ section: modelData ++ active: root.activeSection === modelData.id ++ onClicked: root.activeSection = modelData.id ++ } + } + } ++ } ++ } + +- SectionContainer { +- Layout.fillWidth: true +- alignTop: true ++ rightContent: Component { ++ Item { ++ id: rightPaneItem + +- StyledText { +- text: qsTr("Status Icons") +- font.pointSize: Appearance.font.size.normal +- } ++ property string paneId: root.activeSection ++ property Component targetComponent: root.componentForSection(root.activeSection) ++ property Component nextComponent: root.componentForSection(root.activeSection) + +- ConnectedButtonGroup { +- rootItem: root ++ onPaneIdChanged: { ++ nextComponent = root.componentForSection(root.activeSection); ++ } + +- options: [ +- { +- label: qsTr("Speakers"), +- propertyName: "showAudio", +- onToggled: function (checked) { +- root.showAudio = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Microphone"), +- propertyName: "showMicrophone", +- onToggled: function (checked) { +- root.showMicrophone = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Keyboard"), +- propertyName: "showKbLayout", +- onToggled: function (checked) { +- root.showKbLayout = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Network"), +- propertyName: "showNetwork", +- onToggled: function (checked) { +- root.showNetwork = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Wifi"), +- propertyName: "showWifi", +- onToggled: function (checked) { +- root.showWifi = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Bluetooth"), +- propertyName: "showBluetooth", +- onToggled: function (checked) { +- root.showBluetooth = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Battery"), +- propertyName: "showBattery", +- onToggled: function (checked) { +- root.showBattery = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Capslock"), +- propertyName: "showLockStatus", +- onToggled: function (checked) { +- root.showLockStatus = checked; +- root.saveConfig(); +- } ++ Loader { ++ id: rightLoader ++ ++ anchors.fill: parent ++ asynchronous: true ++ opacity: 1 ++ scale: 1 ++ transformOrigin: Item.Center ++ sourceComponent: rightPaneItem.targetComponent ++ } ++ ++ Behavior on paneId { ++ PaneTransition { ++ target: rightLoader ++ propertyActions: [ ++ PropertyAction { ++ target: rightPaneItem ++ property: "targetComponent" ++ value: rightPaneItem.nextComponent + } + ] + } + } ++ } ++ } ++ } + +- RowLayout { +- id: mainRowLayout +- Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ Component { ++ id: statusIconsComponent ++ ++ SectionPage { ++ title: qsTr("Status Icons") ++ subtitle: qsTr("Choose which status controls appear in the taskbar.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ ConnectedButtonGroup { ++ rootItem: root ++ options: [ ++ { ++ label: qsTr("Speakers"), ++ propertyName: "showAudio", ++ onToggled: function (checked) { ++ root.showAudio = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Microphone"), ++ propertyName: "showMicrophone", ++ onToggled: function (checked) { ++ root.showMicrophone = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Keyboard"), ++ propertyName: "showKbLayout", ++ onToggled: function (checked) { ++ root.showKbLayout = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Network"), ++ propertyName: "showNetwork", ++ onToggled: function (checked) { ++ root.showNetwork = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Wifi"), ++ propertyName: "showWifi", ++ onToggled: function (checked) { ++ root.showWifi = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Bluetooth"), ++ propertyName: "showBluetooth", ++ onToggled: function (checked) { ++ root.showBluetooth = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Battery"), ++ propertyName: "showBattery", ++ onToggled: function (checked) { ++ root.showBattery = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Capslock"), ++ propertyName: "showLockStatus", ++ onToggled: function (checked) { ++ root.showLockStatus = checked; ++ root.saveConfig(); ++ } ++ } ++ ] ++ } ++ } ++ } ++ } + +- ColumnLayout { +- id: leftColumnLayout +- Layout.fillWidth: true +- Layout.alignment: Qt.AlignTop +- spacing: Appearance.spacing.normal ++ Component { ++ id: workspacesComponent ++ ++ SectionPage { ++ title: qsTr("Workspaces") ++ subtitle: qsTr("Tune workspace buttons and window indicators.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SpinSettingRow { ++ label: qsTr("Shown") ++ min: 1 ++ max: 20 ++ value: root.workspacesShown ++ onModified: value => { ++ root.workspacesShown = value; ++ root.saveConfig(); ++ } ++ } + +- SectionContainer { +- Layout.fillWidth: true +- alignTop: true ++ SwitchRow { ++ label: qsTr("Active indicator") ++ checked: root.workspacesActiveIndicator ++ onToggled: checked => { ++ root.workspacesActiveIndicator = checked; ++ root.saveConfig(); ++ } ++ } + +- StyledText { +- text: qsTr("Workspaces") +- font.pointSize: Appearance.font.size.normal +- } ++ SwitchRow { ++ label: qsTr("Occupied background") ++ checked: root.workspacesOccupiedBg ++ onToggled: checked => { ++ root.workspacesOccupiedBg = checked; ++ root.saveConfig(); ++ } ++ } + +- StyledRect { +- Layout.fillWidth: true +- implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal +- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- +- Behavior on implicitHeight { +- Anim {} +- } +- +- RowLayout { +- id: workspacesShownRow +- anchors.left: parent.left +- anchors.right: parent.right +- anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal +- +- StyledText { +- Layout.fillWidth: true +- text: qsTr("Shown") +- } ++ SwitchRow { ++ label: qsTr("Show windows") ++ checked: root.workspacesShowWindows ++ onToggled: checked => { ++ root.workspacesShowWindows = checked; ++ root.saveConfig(); ++ } ++ } + +- CustomSpinBox { +- min: 1 +- max: 20 +- value: root.workspacesShown +- onValueModified: value => { +- root.workspacesShown = value; +- root.saveConfig(); +- } +- } +- } +- } ++ SpinSettingRow { ++ label: qsTr("Max window icons") ++ min: 0 ++ max: 20 ++ value: root.workspacesMaxWindowIcons ++ onModified: value => { ++ root.workspacesMaxWindowIcons = value; ++ root.saveConfig(); ++ } ++ } + +- StyledRect { +- Layout.fillWidth: true +- implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal +- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- +- Behavior on implicitHeight { +- Anim {} +- } +- +- RowLayout { +- id: workspacesActiveIndicatorRow +- anchors.left: parent.left +- anchors.right: parent.right +- anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal +- +- StyledText { +- Layout.fillWidth: true +- text: qsTr("Active indicator") +- } ++ SwitchRow { ++ label: qsTr("Per monitor workspaces") ++ checked: root.workspacesPerMonitor ++ onToggled: checked => { ++ root.workspacesPerMonitor = checked; ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } + +- StyledSwitch { +- checked: root.workspacesActiveIndicator +- onToggled: { +- root.workspacesActiveIndicator = checked; +- root.saveConfig(); +- } +- } +- } ++ Component { ++ id: scrollActionsComponent ++ ++ SectionPage { ++ title: qsTr("Scroll Actions") ++ subtitle: qsTr("Choose what responds to wheel input on the bar.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ ConnectedButtonGroup { ++ rootItem: root ++ options: [ ++ { ++ label: qsTr("Workspaces"), ++ propertyName: "scrollWorkspaces", ++ onToggled: function (checked) { ++ root.scrollWorkspaces = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Volume"), ++ propertyName: "scrollVolume", ++ onToggled: function (checked) { ++ root.scrollVolume = checked; ++ root.saveConfig(); + } ++ }, ++ { ++ label: qsTr("Brightness"), ++ propertyName: "scrollBrightness", ++ onToggled: function (checked) { ++ root.scrollBrightness = checked; ++ root.saveConfig(); ++ } ++ } ++ ] ++ } ++ } ++ } ++ } + +- StyledRect { +- Layout.fillWidth: true +- implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal +- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- +- Behavior on implicitHeight { +- Anim {} +- } +- +- RowLayout { +- id: workspacesOccupiedBgRow +- anchors.left: parent.left +- anchors.right: parent.right +- anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal +- +- StyledText { +- Layout.fillWidth: true +- text: qsTr("Occupied background") +- } ++ Component { ++ id: clockComponent ++ ++ SectionPage { ++ title: qsTr("Clock") ++ subtitle: qsTr("Adjust the clock entry shown on the bar.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SwitchRow { ++ label: qsTr("Background") ++ checked: root.clockBackground ++ onToggled: checked => { ++ root.clockBackground = checked; ++ root.saveConfig(); ++ } ++ } + +- StyledSwitch { +- checked: root.workspacesOccupiedBg +- onToggled: { +- root.workspacesOccupiedBg = checked; +- root.saveConfig(); +- } +- } +- } +- } ++ SwitchRow { ++ label: qsTr("Show date") ++ checked: root.clockShowDate ++ onToggled: checked => { ++ root.clockShowDate = checked; ++ root.saveConfig(); ++ } ++ } + +- StyledRect { +- Layout.fillWidth: true +- implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal +- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- +- Behavior on implicitHeight { +- Anim {} +- } +- +- RowLayout { +- id: workspacesShowWindowsRow +- anchors.left: parent.left +- anchors.right: parent.right +- anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal +- +- StyledText { +- Layout.fillWidth: true +- text: qsTr("Show windows") +- } ++ SwitchRow { ++ label: qsTr("Show clock icon") ++ checked: root.clockShowIcon ++ onToggled: checked => { ++ root.clockShowIcon = checked; ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } + +- StyledSwitch { +- checked: root.workspacesShowWindows +- onToggled: { +- root.workspacesShowWindows = checked; +- root.saveConfig(); +- } +- } +- } +- } ++ Component { ++ id: behaviorComponent ++ ++ SectionPage { ++ title: qsTr("Bar Behavior") ++ subtitle: qsTr("Control when the bar appears and how drag reveal feels.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SwitchRow { ++ label: qsTr("Persistent") ++ checked: root.persistent ++ onToggled: checked => { ++ root.persistent = checked; ++ root.saveConfig(); ++ } ++ } + +- StyledRect { +- Layout.fillWidth: true +- implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 +- radius: Appearance.rounding.normal +- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- +- Behavior on implicitHeight { +- Anim {} +- } +- +- RowLayout { +- id: workspacesPerMonitorRow +- anchors.left: parent.left +- anchors.right: parent.right +- anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal +- +- StyledText { +- Layout.fillWidth: true +- text: qsTr("Per monitor workspaces") +- } ++ SwitchRow { ++ label: qsTr("Show on hover") ++ checked: root.showOnHover ++ onToggled: checked => { ++ root.showOnHover = checked; ++ root.saveConfig(); ++ } ++ } ++ } + +- StyledSwitch { +- checked: root.workspacesPerMonitor +- onToggled: { +- root.workspacesPerMonitor = checked; +- root.saveConfig(); +- } +- } +- } +- } +- } ++ SectionContainer { ++ Layout.fillWidth: true ++ contentSpacing: Tokens.spacing.normal + +- SectionContainer { +- Layout.fillWidth: true +- alignTop: true ++ SliderInput { ++ Layout.fillWidth: true ++ label: qsTr("Drag threshold") ++ value: root.dragThreshold ++ from: 0 ++ to: 100 ++ suffix: "px" ++ validator: IntValidator { ++ bottom: 0 ++ top: 100 ++ } ++ formatValueFunction: val => Math.round(val).toString() ++ parseValueFunction: text => parseInt(text) ++ onValueModified: newValue => { ++ root.dragThreshold = Math.round(newValue); ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } + +- StyledText { +- text: qsTr("Scroll Actions") +- font.pointSize: Appearance.font.size.normal +- } ++ Component { ++ id: activeWindowComponent ++ ++ SectionPage { ++ title: qsTr("Active Window") ++ subtitle: qsTr("Configure the active window entry in the taskbar.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SwitchRow { ++ label: qsTr("Compact") ++ checked: root.activeWindowCompact ++ onToggled: checked => { ++ root.activeWindowCompact = checked; ++ root.saveConfig(); ++ } ++ } + +- ConnectedButtonGroup { +- rootItem: root +- +- options: [ +- { +- label: qsTr("Workspaces"), +- propertyName: "scrollWorkspaces", +- onToggled: function (checked) { +- root.scrollWorkspaces = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Volume"), +- propertyName: "scrollVolume", +- onToggled: function (checked) { +- root.scrollVolume = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Brightness"), +- propertyName: "scrollBrightness", +- onToggled: function (checked) { +- root.scrollBrightness = checked; +- root.saveConfig(); +- } +- } +- ] +- } +- } ++ SwitchRow { ++ label: qsTr("Inverted") ++ checked: root.activeWindowInverted ++ onToggled: checked => { ++ root.activeWindowInverted = checked; ++ root.saveConfig(); + } ++ } ++ } ++ } ++ } + +- ColumnLayout { +- id: middleColumnLayout +- Layout.fillWidth: true +- Layout.alignment: Qt.AlignTop +- spacing: Appearance.spacing.normal ++ Component { ++ id: popoutsComponent ++ ++ SectionPage { ++ title: qsTr("Popouts") ++ subtitle: qsTr("Select which taskbar entries open hover popouts.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ SwitchRow { ++ label: qsTr("Active window") ++ checked: root.popoutActiveWindow ++ onToggled: checked => { ++ root.popoutActiveWindow = checked; ++ root.saveConfig(); ++ } ++ } + +- SectionContainer { +- Layout.fillWidth: true +- alignTop: true ++ SwitchRow { ++ label: qsTr("Tray") ++ checked: root.popoutTray ++ onToggled: checked => { ++ root.popoutTray = checked; ++ root.saveConfig(); ++ } ++ } + +- StyledText { +- text: qsTr("Clock") +- font.pointSize: Appearance.font.size.normal +- } ++ SwitchRow { ++ label: qsTr("Status icons") ++ checked: root.popoutStatusIcons ++ onToggled: checked => { ++ root.popoutStatusIcons = checked; ++ root.saveConfig(); ++ } ++ } ++ } ++ } ++ } + +- SwitchRow { +- label: qsTr("Show clock icon") +- checked: root.clockShowIcon +- onToggled: checked => { +- root.clockShowIcon = checked; +- root.saveConfig(); +- } ++ Component { ++ id: trayComponent ++ ++ SectionPage { ++ title: qsTr("Tray Settings") ++ subtitle: qsTr("Change the system tray presentation.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ ConnectedButtonGroup { ++ rootItem: root ++ options: [ ++ { ++ label: qsTr("Background"), ++ propertyName: "trayBackground", ++ onToggled: function (checked) { ++ root.trayBackground = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Compact"), ++ propertyName: "trayCompact", ++ onToggled: function (checked) { ++ root.trayCompact = checked; ++ root.saveConfig(); ++ } ++ }, ++ { ++ label: qsTr("Recolour"), ++ propertyName: "trayRecolour", ++ onToggled: function (checked) { ++ root.trayRecolour = checked; ++ root.saveConfig(); + } + } ++ ] ++ } ++ } ++ } ++ } + +- SectionContainer { +- Layout.fillWidth: true +- alignTop: true +- +- StyledText { +- text: qsTr("Bar Behavior") +- font.pointSize: Appearance.font.size.normal +- } ++ Component { ++ id: monitorsComponent ++ ++ SectionPage { ++ title: qsTr("Monitors") ++ subtitle: qsTr("Choose which monitors show the taskbar.") ++ ++ SectionContainer { ++ Layout.fillWidth: true ++ alignTop: true ++ ++ ConnectedButtonGroup { ++ rootItem: root ++ rows: Math.max(1, Math.ceil(root.monitorNames.length / 3)) ++ options: root.monitorNames.map(e => ({ ++ label: qsTr(e), ++ propertyName: `monitor${e}`, ++ onToggled: function (_) { ++ const screens = []; ++ for (const screen of root.excludedScreens) ++ screens.push(screen); ++ ++ const addedBack = screens.includes(e); ++ if (addedBack) { ++ const index = screens.indexOf(e); ++ if (index !== -1) ++ screens.splice(index, 1); ++ } else if (!screens.includes(e)) { ++ screens.push(e); ++ } + +- SwitchRow { +- label: qsTr("Persistent") +- checked: root.persistent +- onToggled: checked => { +- root.persistent = checked; ++ root.excludedScreens = screens; + root.saveConfig(); +- } +- } ++ }, ++ state: !Strings.testRegexList(root.excludedScreens, e) ++ })) ++ } ++ } ++ } ++ } + +- SwitchRow { +- label: qsTr("Show on hover") +- checked: root.showOnHover +- onToggled: checked => { +- root.showOnHover = checked; +- root.saveConfig(); +- } +- } ++ component SectionPage: StyledFlickable { ++ id: sectionPage + +- SectionContainer { +- contentSpacing: Appearance.spacing.normal ++ required property string title ++ property string subtitle: "" ++ default property alias contentItems: contentLayout.data + +- SliderInput { +- Layout.fillWidth: true ++ flickableDirection: Flickable.VerticalFlick ++ contentHeight: contentLayout.height + +- label: qsTr("Drag threshold") +- value: root.dragThreshold +- from: 0 +- to: 100 +- suffix: "px" +- validator: IntValidator { +- bottom: 0 +- top: 100 +- } +- formatValueFunction: val => Math.round(val).toString() +- parseValueFunction: text => parseInt(text) ++ StyledScrollBar.vertical: StyledScrollBar { ++ flickable: sectionPage ++ } + +- onValueModified: newValue => { +- root.dragThreshold = Math.round(newValue); +- root.saveConfig(); +- } +- } +- } +- } +- } ++ ColumnLayout { ++ id: contentLayout + +- ColumnLayout { +- id: rightColumnLayout +- Layout.fillWidth: true +- Layout.alignment: Qt.AlignTop +- spacing: Appearance.spacing.normal ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.top: parent.top ++ spacing: Tokens.spacing.normal + +- SectionContainer { +- Layout.fillWidth: true +- alignTop: true ++ StyledText { ++ Layout.fillWidth: true ++ text: sectionPage.title ++ font.pointSize: Tokens.font.size.extraLarge ++ font.weight: 600 ++ } + +- StyledText { +- text: qsTr("Popouts") +- font.pointSize: Appearance.font.size.normal +- } ++ StyledText { ++ Layout.fillWidth: true ++ Layout.bottomMargin: Tokens.spacing.small ++ text: sectionPage.subtitle ++ color: Colours.palette.m3outline ++ visible: text.length > 0 ++ wrapMode: Text.WordWrap ++ } ++ } ++ } + +- SwitchRow { +- label: qsTr("Active window") +- checked: root.popoutActiveWindow +- onToggled: checked => { +- root.popoutActiveWindow = checked; +- root.saveConfig(); +- } +- } ++ component SectionNavButton: StyledRect { ++ id: navButton + +- SwitchRow { +- label: qsTr("Tray") +- checked: root.popoutTray +- onToggled: checked => { +- root.popoutTray = checked; +- root.saveConfig(); +- } +- } ++ required property var section ++ property bool active: false + +- SwitchRow { +- label: qsTr("Status icons") +- checked: root.popoutStatusIcons +- onToggled: checked => { +- root.popoutStatusIcons = checked; +- root.saveConfig(); +- } +- } +- } ++ signal clicked + +- SectionContainer { +- Layout.fillWidth: true +- alignTop: true ++ implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 ++ color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" ++ radius: Tokens.rounding.normal + +- StyledText { +- text: qsTr("Tray Settings") +- font.pointSize: Appearance.font.size.normal +- } ++ Behavior on color { ++ CAnim {} ++ } + +- ConnectedButtonGroup { +- rootItem: root +- +- options: [ +- { +- label: qsTr("Background"), +- propertyName: "trayBackground", +- onToggled: function (checked) { +- root.trayBackground = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Compact"), +- propertyName: "trayCompact", +- onToggled: function (checked) { +- root.trayCompact = checked; +- root.saveConfig(); +- } +- }, +- { +- label: qsTr("Recolour"), +- propertyName: "trayRecolour", +- onToggled: function (checked) { +- root.trayRecolour = checked; +- root.saveConfig(); +- } +- } +- ] +- } +- } +- } ++ StateLayer { ++ onClicked: navButton.clicked() ++ } ++ ++ RowLayout { ++ id: navRow ++ ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.normal ++ ++ MaterialIcon { ++ Layout.alignment: Qt.AlignVCenter ++ text: navButton.section.icon ++ fill: navButton.active ? 1 : 0 ++ color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant ++ font.pointSize: Tokens.font.size.large ++ } ++ ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: 0 ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: navButton.section.title ++ font.weight: navButton.active ? 500 : 400 ++ elide: Text.ElideRight ++ maximumLineCount: 1 + } ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: navButton.section.description ++ color: Colours.palette.m3outline ++ font.pointSize: Tokens.font.size.small ++ elide: Text.ElideRight ++ maximumLineCount: 1 ++ } ++ } ++ ++ MaterialIcon { ++ Layout.alignment: Qt.AlignVCenter ++ text: "chevron_right" ++ opacity: navButton.active ? 1 : 0 ++ color: Colours.palette.m3primary ++ } ++ } ++ } ++ ++ component SpinSettingRow: StyledRect { ++ id: spinRow ++ ++ required property string label ++ property int min: 0 ++ property int max: 100 ++ property int value: 0 ++ ++ signal modified(int value) ++ ++ Layout.fillWidth: true ++ implicitHeight: rowLayout.implicitHeight + Tokens.padding.large * 2 ++ radius: Tokens.rounding.normal ++ color: Colours.layer(Colours.palette.m3surfaceContainer, 2) ++ ++ RowLayout { ++ id: rowLayout ++ ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: spinRow.label ++ } ++ ++ CustomSpinBox { ++ min: spinRow.min ++ max: spinRow.max ++ value: spinRow.value ++ onValueModified: value => spinRow.modified(value) + } + } + } +diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml +deleted file mode 100644 +index e2a91f74..00000000 +--- a/modules/dashboard/Background.qml ++++ /dev/null +@@ -1,66 +0,0 @@ +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Shapes +- +-ShapePath { +- id: root +- +- required property Wrapper wrapper +- readonly property real rounding: Config.border.rounding +- readonly property bool flatten: wrapper.height < rounding * 2 +- readonly property real roundingY: flatten ? wrapper.height / 2 : rounding +- +- strokeWidth: -1 +- fillColor: Colours.palette.m3surface +- +- PathArc { +- relativeX: root.rounding +- relativeY: root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- } +- +- PathLine { +- relativeX: 0 +- relativeY: root.wrapper.height - root.roundingY * 2 +- } +- +- PathArc { +- relativeX: root.rounding +- relativeY: root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- direction: PathArc.Counterclockwise +- } +- +- PathLine { +- relativeX: root.wrapper.width - root.rounding * 2 +- relativeY: 0 +- } +- +- PathArc { +- relativeX: root.rounding +- relativeY: -root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- direction: PathArc.Counterclockwise +- } +- +- PathLine { +- relativeX: 0 +- relativeY: -(root.wrapper.height - root.roundingY * 2) +- } +- +- PathArc { +- relativeX: root.rounding +- relativeY: -root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- } +- +- Behavior on fillColor { +- CAnim {} +- } +-} +diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml +index f95b7d78..95d2a08a 100644 +--- a/modules/dashboard/Content.qml ++++ b/modules/dashboard/Content.qml +@@ -1,19 +1,59 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.components.filedialog +-import qs.config +-import Quickshell +-import Quickshell.Widgets + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components ++import qs.components.filedialog + + Item { + id: root + +- required property PersistentProperties visibilities +- required property PersistentProperties state ++ required property DrawerVisibilities visibilities ++ readonly property bool needsKeyboard: { ++ const count = repeater.count; ++ for (let i = 0; i < count; i++) { ++ const item = repeater.itemAt(i) as Loader; ++ if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) ++ return true; ++ } ++ return false; ++ } ++ required property DashboardState dashState + required property FileDialog facePicker ++ ++ readonly property var dashboardTabs: { ++ const allTabs = [ ++ { ++ component: dashComponent, ++ iconName: "dashboard", ++ text: qsTr("Dashboard"), ++ enabled: Config.dashboard.showDashboard ++ }, ++ { ++ component: mediaComponent, ++ iconName: "queue_music", ++ text: qsTr("Media"), ++ enabled: Config.dashboard.showMedia ++ }, ++ { ++ component: performanceComponent, ++ iconName: "speed", ++ text: qsTr("Performance"), ++ enabled: Config.dashboard.showPerformance && (Config.dashboard.performance.showCpu || Config.dashboard.performance.showGpu || Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork || Config.dashboard.performance.showBattery) ++ }, ++ { ++ component: weatherComponent, ++ iconName: "cloud", ++ text: qsTr("Weather"), ++ enabled: Config.dashboard.showWeather ++ } ++ ]; ++ return allTabs.filter(tab => tab.enabled); ++ } ++ + readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 + readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2 + +@@ -26,11 +66,12 @@ Item { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right +- anchors.topMargin: Appearance.padding.normal +- anchors.margins: Appearance.padding.large ++ anchors.topMargin: Tokens.padding.normal ++ anchors.margins: Tokens.padding.large + + nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 +- state: root.state ++ dashState: root.dashState ++ tabs: root.dashboardTabs + } + + ClippingRectangle { +@@ -40,84 +81,116 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: "transparent" + + Flickable { + id: view + +- readonly property int currentIndex: root.state.currentTab +- readonly property Item currentItem: row.children[currentIndex] ++ readonly property int currentIndex: root.dashState.currentTab ++ readonly property Item currentItem: { ++ repeater.count; // Trigger update on count change ++ return repeater.itemAt(currentIndex); ++ } + + anchors.fill: parent + + flickableDirection: Flickable.HorizontalFlick + +- implicitWidth: currentItem.implicitWidth +- implicitHeight: currentItem.implicitHeight ++ implicitWidth: currentItem?.implicitWidth ?? 0 ++ implicitHeight: currentItem?.implicitHeight ?? 0 + +- contentX: currentItem.x ++ contentX: currentItem?.x ?? 0 + contentWidth: row.implicitWidth + contentHeight: row.implicitHeight + + onContentXChanged: { +- if (!moving) ++ if (!moving || !currentItem) + return; + + const x = contentX - currentItem.x; + if (x > currentItem.implicitWidth / 2) +- root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); ++ root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); + else if (x < -currentItem.implicitWidth / 2) +- root.state.currentTab = Math.max(root.state.currentTab - 1, 0); ++ root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); + } + + onDragEnded: { ++ if (!currentItem) ++ return; ++ + const x = contentX - currentItem.x; + if (x > currentItem.implicitWidth / 10) +- root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); ++ root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); + else if (x < -currentItem.implicitWidth / 10) +- root.state.currentTab = Math.max(root.state.currentTab - 1, 0); ++ root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); + else +- contentX = Qt.binding(() => currentItem.x); ++ contentX = Qt.binding(() => currentItem?.x ?? 0); + } + + RowLayout { + id: row + +- Pane { +- index: 0 +- sourceComponent: Dash { +- visibilities: root.visibilities +- state: root.state +- facePicker: root.facePicker ++ Repeater { ++ id: repeater ++ ++ model: ScriptModel { ++ values: root.dashboardTabs + } +- } + +- Pane { +- index: 1 +- sourceComponent: Media { +- visibilities: root.visibilities ++ delegate: Loader { ++ id: paneLoader ++ ++ required property int index ++ required property var modelData ++ ++ Layout.alignment: Qt.AlignTop ++ ++ sourceComponent: modelData.component ++ ++ Component.onCompleted: active = Qt.binding(() => { ++ if (index === view.currentIndex) ++ return true; ++ const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); ++ const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); ++ return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); ++ }) + } + } ++ } + +- Pane { +- index: 2 +- sourceComponent: Performance {} +- } ++ Component { ++ id: dashComponent + +- Pane { +- index: 3 +- sourceComponent: Weather {} ++ Dash { ++ visibilities: root.visibilities ++ dashState: root.dashState ++ facePicker: root.facePicker + } ++ } ++ ++ Component { ++ id: mediaComponent + +- Pane { +- index: 4 +- sourceComponent: Monitors {} ++ MediaWrapper { ++ visibilities: root.visibilities + } + } + ++ Component { ++ id: performanceComponent ++ ++ Performance {} ++ } ++ ++ Component { ++ id: weatherComponent ++ ++ WeatherTab {} ++ } ++ + Behavior on contentX { + Anim {} + } +@@ -126,32 +199,13 @@ Item { + + Behavior on implicitWidth { + Anim { +- duration: Appearance.anim.durations.large +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.EmphasizedLarge + } + } + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.large +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.EmphasizedLarge + } + } +- +- component Pane: Loader { +- id: pane +- +- required property int index +- +- Layout.alignment: Qt.AlignTop +- +- Component.onCompleted: active = Qt.binding(() => { +- // Always keep current tab loaded +- if (pane.index === view.currentIndex) +- return true; +- const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); +- const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); +- return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); +- }) +- } + } +diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml +index 71e224fb..6785ede8 100644 +--- a/modules/dashboard/Dash.qml ++++ b/modules/dashboard/Dash.qml +@@ -1,20 +1,19 @@ ++import "dash" ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.filedialog + import qs.services +-import qs.config +-import "dash" +-import Quickshell +-import QtQuick.Layouts + + GridLayout { + id: root + +- required property PersistentProperties visibilities +- required property PersistentProperties state ++ required property DrawerVisibilities visibilities ++ required property DashboardState dashState + required property FileDialog facePicker + +- rowSpacing: Appearance.spacing.normal +- columnSpacing: Appearance.spacing.normal ++ rowSpacing: Tokens.spacing.normal ++ columnSpacing: Tokens.spacing.normal + + Rect { + Layout.column: 2 +@@ -22,13 +21,12 @@ GridLayout { + Layout.preferredWidth: user.implicitWidth + Layout.preferredHeight: user.implicitHeight + +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + + User { + id: user + + visibilities: root.visibilities +- state: root.state + facePicker: root.facePicker + } + } +@@ -36,12 +34,12 @@ GridLayout { + Rect { + Layout.row: 0 + Layout.columnSpan: 2 +- Layout.preferredWidth: Config.dashboard.sizes.weatherWidth ++ Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth + Layout.fillHeight: true + +- radius: Appearance.rounding.large * 1.5 ++ radius: Tokens.rounding.large * 1.5 + +- Weather {} ++ SmallWeather {} + } + + Rect { +@@ -49,7 +47,7 @@ GridLayout { + Layout.preferredWidth: dateTime.implicitWidth + Layout.fillHeight: true + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + DateTime { + id: dateTime +@@ -63,12 +61,12 @@ GridLayout { + Layout.fillWidth: true + Layout.preferredHeight: calendar.implicitHeight + +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + + Calendar { + id: calendar + +- state: root.state ++ dashState: root.dashState + } + } + +@@ -78,7 +76,7 @@ GridLayout { + Layout.preferredWidth: resources.implicitWidth + Layout.fillHeight: true + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + Resources { + id: resources +@@ -92,7 +90,7 @@ GridLayout { + Layout.preferredWidth: media.implicitWidth + Layout.fillHeight: true + +- radius: Appearance.rounding.large * 2 ++ radius: Tokens.rounding.large * 2 + + Media { + id: media +diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml +new file mode 100644 +index 00000000..1e5461f5 +--- /dev/null ++++ b/modules/dashboard/LyricMenu.qml +@@ -0,0 +1,412 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.components.controls ++import qs.services ++ ++StyledRect { ++ id: root ++ ++ required property real contentHeight ++ ++ function searchCandidates(title, artist) { ++ LyricsService.currentRequestId++; ++ LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); ++ } ++ ++ implicitHeight: contentHeight ++ ++ radius: Tokens.rounding.large ++ color: Colours.tPalette.m3surfaceContainer ++ ++ Loader { ++ asynchronous: true ++ anchors.fill: parent ++ active: root.height > 0 ++ ++ sourceComponent: ColumnLayout { ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal ++ ++ // Header: icon, backend selector, refresh, toggle ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.padding.small ++ ++ MaterialIcon { ++ text: "lyrics" ++ fill: 1 ++ color: Colours.palette.m3primary ++ font.pointSize: Tokens.spacing.large ++ } ++ ++ Rectangle { ++ Layout.preferredHeight: 24 ++ Layout.preferredWidth: 80 ++ radius: Tokens.rounding.small ++ color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) ++ ++ StyledText { ++ anchors.centerIn: parent ++ text: LyricsService.preferredBackend ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3primary ++ } ++ ++ MouseArea { ++ anchors.fill: parent ++ cursorShape: Qt.PointingHandCursor ++ onClicked: { ++ const backends = ["Auto", "Local", "NetEase"]; ++ const currentIndex = backends.indexOf(LyricsService.preferredBackend); ++ const nextIndex = (currentIndex + 1) % backends.length; ++ LyricsService.preferredBackend = backends[nextIndex]; ++ LyricsService.loadLyrics(); ++ } ++ } ++ } ++ ++ Rectangle { ++ Layout.preferredHeight: 24 ++ Layout.preferredWidth: 60 ++ radius: Tokens.rounding.small ++ visible: LyricsService.preferredBackend === "Auto" ++ color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) ++ ++ StyledText { ++ anchors.centerIn: parent ++ text: LyricsService.backend ++ font.pointSize: Tokens.font.size.small ++ color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary ++ } ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ ++ IconButton { ++ icon: "refresh" ++ type: IconButton.Text ++ onClicked: LyricsService.loadLyrics() ++ } ++ ++ StyledSwitch { ++ checked: LyricsService.lyricsVisible ++ onToggled: LyricsService.toggleVisibility() ++ } ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" ++ color: Colours.palette.m3outline ++ font.pointSize: Tokens.font.size.small ++ elide: Text.ElideRight ++ visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 ++ } ++ ++ // Local file info (shown in Local mode) ++ Rectangle { ++ Layout.fillWidth: true ++ Layout.preferredHeight: 48 ++ visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 ++ radius: Tokens.rounding.small ++ color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) ++ ++ ColumnLayout { ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.small ++ spacing: 0 ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: { ++ const path = LyricsService.loadedLocalFile; ++ const parts = path.split('/'); ++ return parts[parts.length - 1]; ++ } ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3tertiary ++ elide: Text.ElideMiddle ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: { ++ const path = LyricsService.loadedLocalFile; ++ const parts = path.split('/'); ++ if (parts.length > 2) { ++ return parts.slice(-3, -1).join('/'); ++ } ++ return ""; ++ } ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3outline ++ elide: Text.ElideMiddle ++ } ++ } ++ } ++ ++ // Candidates list ++ Loader { ++ Layout.fillWidth: true ++ Layout.fillHeight: true ++ ++ active: LyricsService.preferredBackend !== "Local" ++ ++ sourceComponent: ListView { ++ id: candidatesView ++ ++ model: LyricsService.candidatesModel ++ clip: true ++ spacing: Tokens.spacing.small ++ visible: LyricsService.candidatesModel.count > 0 ++ opacity: visible ? 1 : 0 ++ ++ delegate: Item { ++ id: delegateRoot ++ ++ required property real id ++ required property string title ++ required property string artist ++ ++ property bool hovered: false ++ property bool pressed: false ++ ++ width: ListView.view.width * 0.98 ++ height: 70 ++ ++ anchors.horizontalCenter: parent?.horizontalCenter ++ scale: hovered ? 1.02 : 1.0 ++ ++ Behavior on scale { ++ NumberAnimation { ++ duration: Tokens.anim.durations.small ++ easing.type: Easing.OutCubic ++ } ++ } ++ ++ Rectangle { ++ id: background ++ ++ anchors.fill: parent ++ radius: Tokens.rounding.small ++ ++ color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) ++ ++ border.width: delegateRoot.hovered ? 1 : 0 ++ border.color: Colours.palette.m3primary ++ ++ Behavior on color { ++ ColorAnimation { ++ duration: Tokens.anim.durations.small ++ } ++ } ++ Behavior on border.width { ++ NumberAnimation { ++ duration: Tokens.anim.durations.small ++ } ++ } ++ } ++ ++ MouseArea { ++ anchors.fill: parent ++ hoverEnabled: true ++ cursorShape: Qt.PointingHandCursor ++ ++ onEntered: delegateRoot.hovered = true ++ onExited: delegateRoot.hovered = false ++ onPressed: delegateRoot.pressed = true ++ onReleased: delegateRoot.pressed = false ++ onClicked: LyricsService.selectCandidate(delegateRoot.id) ++ } ++ ++ Row { ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.normal ++ spacing: Tokens.spacing.small ++ ++ // Active indicator bar ++ Rectangle { ++ width: 4 ++ height: parent.height * 0.6 ++ radius: 2 ++ anchors.verticalCenter: parent.verticalCenter ++ color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" ++ ++ Behavior on color { ++ ColorAnimation { ++ duration: Tokens.anim.durations.small ++ } ++ } ++ } ++ ++ Column { ++ anchors.verticalCenter: parent.verticalCenter ++ width: parent.width - 30 ++ spacing: 4 ++ ++ Text { ++ text: delegateRoot.title ++ font.pointSize: Tokens.font.size.normal ++ font.bold: true ++ color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface ++ width: parent.width ++ elide: Text.ElideRight ++ ++ Behavior on color { ++ ColorAnimation { ++ duration: Tokens.anim.durations.small ++ } ++ } ++ } ++ ++ Text { ++ text: delegateRoot.artist ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ elide: Text.ElideRight ++ } ++ } ++ } ++ } ++ } ++ } ++ ++ Item { ++ Layout.fillHeight: true ++ visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local" ++ } ++ ++ // Manual search ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.padding.small ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: "Manual Search" ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ elide: Text.ElideRight ++ } ++ ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.padding.small ++ ++ StyledInputField { ++ id: searchTitle ++ ++ Layout.fillWidth: true ++ horizontalAlignment: TextInput.AlignLeft ++ ++ Binding { ++ target: searchTitle ++ property: "text" ++ value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") ++ } ++ } ++ ++ StyledInputField { ++ id: searchArtist ++ ++ Layout.fillWidth: true ++ horizontalAlignment: TextInput.AlignLeft ++ ++ Binding { ++ target: searchArtist ++ property: "text" ++ value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") ++ } ++ } ++ ++ IconButton { ++ icon: "search" ++ onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) ++ } ++ } ++ } ++ ++ // Offset controls ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.padding.small ++ ++ MaterialIcon { ++ text: "contrast_square" ++ font.pointSize: Tokens.font.size.large ++ color: Colours.palette.m3secondary ++ } ++ ++ StyledText { ++ text: "Offset" ++ color: Colours.palette.m3outline ++ font.pointSize: Tokens.font.size.normal ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ ++ IconButton { ++ icon: "remove" ++ type: IconButton.Text ++ onClicked: { ++ LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); ++ LyricsService.savePrefs(); ++ } ++ } ++ ++ TextInput { ++ id: offsetInput ++ ++ horizontalAlignment: TextInput.AlignHCenter ++ color: Colours.palette.m3secondary ++ font.pointSize: Tokens.font.size.normal ++ selectByMouse: true ++ text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" ++ onEditingFinished: { ++ let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); ++ let val = parseFloat(cleaned); ++ if (!isNaN(val)) { ++ LyricsService.offset = parseFloat(val.toFixed(1)); ++ LyricsService.savePrefs(); ++ } else { ++ offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; ++ } ++ } ++ ++ Binding { ++ target: offsetInput ++ property: "text" ++ value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" ++ when: !offsetInput.activeFocus ++ } ++ ++ Connections { ++ function onCurrentRequestIdChanged() { ++ offsetInput.focus = false; ++ } ++ ++ target: LyricsService ++ } ++ } ++ ++ IconButton { ++ icon: "add" ++ type: IconButton.Text ++ onClicked: { ++ LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); ++ LyricsService.savePrefs(); ++ } ++ } ++ } ++ } ++ } ++} +diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml +new file mode 100644 +index 00000000..c0188304 +--- /dev/null ++++ b/modules/dashboard/LyricsView.qml +@@ -0,0 +1,112 @@ ++import QtQuick ++import QtQuick.Effects ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.components.containers ++import qs.services ++ ++StyledListView { ++ id: root ++ ++ readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 ++ ++ clip: true ++ model: LyricsService.model ++ currentIndex: LyricsService.currentIndex ++ visible: lyricsActuallyVisible || hideTimer.running ++ preferredHighlightBegin: height / 2 - 30 ++ preferredHighlightEnd: height / 2 + 30 ++ highlightRangeMode: ListView.ApplyRange ++ highlightFollowsCurrentItem: true ++ highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Tokens.anim.durations.normal ++ layer.enabled: true ++ layer.effect: ShaderEffect { ++ required property Item source ++ property real fadeMargin: 0.5 ++ ++ fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") ++ } ++ onLyricsActuallyVisibleChanged: { ++ if (!lyricsActuallyVisible) ++ hideTimer.restart(); ++ } ++ onModelChanged: { ++ if (model && model.count > 0) { ++ Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); ++ } ++ } ++ delegate: Item { ++ id: delegateRoot ++ ++ required property string lyricLine ++ required property real time ++ required property int index ++ readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 ++ property bool isCurrent: ListView.isCurrentItem ++ ++ width: ListView.view.width ++ height: hasContent ? (lyricText.contentHeight + Tokens.spacing.large) : 0 ++ ++ MultiEffect { ++ id: effect ++ ++ anchors.fill: lyricText ++ source: lyricText ++ scale: lyricText.scale ++ enabled: delegateRoot.isCurrent ++ visible: delegateRoot.isCurrent ++ ++ blurEnabled: true ++ blur: 0.4 ++ ++ shadowEnabled: true ++ shadowColor: Colours.palette.m3primary ++ shadowOpacity: 0.5 ++ shadowBlur: 0.6 ++ shadowHorizontalOffset: 0 ++ shadowVerticalOffset: 0 ++ ++ autoPaddingEnabled: true ++ } ++ ++ MouseArea { ++ anchors.fill: parent ++ cursorShape: Qt.PointingHandCursor ++ onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) ++ } ++ ++ Text { ++ id: lyricText ++ ++ text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" ++ width: parent.width * 0.85 ++ anchors.centerIn: parent ++ horizontalAlignment: Text.AlignHCenter ++ wrapMode: Text.WordWrap ++ font.pointSize: Tokens.font.size.normal ++ color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant ++ font.bold: delegateRoot.isCurrent ++ scale: delegateRoot.isCurrent ? 1.15 : 1.0 ++ ++ Behavior on color { ++ CAnim { ++ duration: Tokens.anim.durations.small ++ } ++ } ++ Behavior on scale { ++ Anim { ++ type: Anim.StandardSmall ++ } ++ } ++ } ++ } ++ ++ Timer { ++ id: hideTimer ++ ++ interval: 300 // long enough to bridge the track switch gap ++ running: false ++ repeat: false ++ } ++} +diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml +index 37d12263..367d9e98 100644 +--- a/modules/dashboard/Media.qml ++++ b/modules/dashboard/Media.qml +@@ -1,27 +1,33 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import QtQuick.Shapes ++import Quickshell ++import Quickshell.Services.Mpris ++import Caelestia.Config ++import Caelestia.Services + import qs.components +-import qs.components.effects + import qs.components.controls + import qs.services + import qs.utils +-import qs.config +-import Caelestia.Services +-import Quickshell +-import Quickshell.Widgets +-import Quickshell.Services.Mpris +-import QtQuick +-import QtQuick.Layouts +-import QtQuick.Shapes + + Item { + id: root + +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities ++ readonly property bool needsKeyboard: lyricMenuOpen ++ ++ readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Tokens.sizes.dashboard.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 ++ readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight ++ ++ property bool lyricMenuOpen: false ++ property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 ++ property bool lyricsShowingDebounced: false + + property real playerProgress: { + const active = Players.active; +- return active?.length ? active.position / active.length : 0; ++ return active?.length ? (active.position % active.length) / active.length : 0; + } + + function lengthStr(length: int): string { +@@ -37,21 +43,54 @@ Item { + return `${mins}:${secs}`; + } + +- implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 +- implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 ++ onLyricsShowingChanged: { ++ if (lyricsShowing) { ++ lyricsHideDelay.stop(); ++ lyricsShowingDebounced = true; ++ } else { ++ lyricsHideDelay.restart(); ++ } ++ } ++ ++ implicitWidth: cover.implicitWidth + Tokens.sizes.dashboard.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 ++ implicitHeight: nonAnimHeight ++ ++ Behavior on implicitHeight { ++ Anim {} ++ } + + Behavior on playerProgress { + Anim { +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + } + + Timer { + running: Players.active?.isPlaying ?? false +- interval: Config.dashboard.mediaUpdateInterval ++ interval: GlobalConfig.dashboard.mediaUpdateInterval + triggeredOnStart: true + repeat: true +- onTriggered: Players.active?.positionChanged() ++ onTriggered: { ++ if (!Players.active) ++ return; ++ LyricsService.updatePosition(); ++ Players.active?.positionChanged(); ++ } ++ } ++ ++ Timer { ++ id: lyricsHideDelay ++ ++ interval: 300 ++ repeat: false ++ } ++ ++ Connections { ++ function onTriggered() { ++ root.lyricsShowingDebounced = false; ++ } ++ ++ target: lyricsHideDelay + } + + ServiceRef { +@@ -67,12 +106,12 @@ Item { + + readonly property real centerX: width / 2 + readonly property real centerY: height / 2 +- readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small +- readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small ++ readonly property real innerX: cover.implicitWidth / 2 + Tokens.spacing.small ++ readonly property real innerY: cover.implicitHeight / 2 + Tokens.spacing.small + property color colour: Colours.palette.m3primary + + anchors.fill: cover +- anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize ++ anchors.margins: -Tokens.sizes.dashboard.mediaVisualiserSize + + asynchronous: true + preferredRendererType: Shape.CurveRenderer +@@ -83,7 +122,7 @@ Item { + id: visualiserBars + + model: Array.from({ +- length: Config.services.visualiserBars ++ length: GlobalConfig.services.visualiserBars + }, (_, i) => i) + + ShapePath { +@@ -92,13 +131,13 @@ Item { + required property int modelData + readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) + +- readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars +- readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize ++ readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars ++ readonly property real magnitude: value * root.Tokens.sizes.dashboard.mediaVisualiserSize + readonly property real cos: Math.cos(angle) + readonly property real sin: Math.sin(angle) + +- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap +- strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4 ++ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap ++ strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 + strokeColor: Colours.palette.m3primary + + startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos +@@ -120,10 +159,10 @@ Item { + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left +- anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize ++ anchors.leftMargin: Tokens.padding.large + Tokens.sizes.dashboard.mediaVisualiserSize + +- implicitWidth: Config.dashboard.sizes.mediaCoverArtSize +- implicitHeight: Config.dashboard.sizes.mediaCoverArtSize ++ implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize ++ implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize + + color: Colours.tPalette.m3surfaceContainerHigh + radius: Infinity +@@ -142,11 +181,20 @@ Item { + + anchors.fill: parent + +- source: Players.active?.trackArtUrl ?? "" ++ source: Players.getArtUrl(Players.active) + asynchronous: true + fillMode: Image.PreserveAspectCrop +- sourceSize.width: width +- sourceSize.height: height ++ sourceSize: { ++ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; ++ return Qt.size(width * dpr, height * dpr); ++ } ++ ++ MouseArea { ++ anchors.fill: parent ++ onClicked: { ++ LyricsService.toggleVisibility(); ++ } ++ } + } + } + +@@ -155,9 +203,9 @@ Item { + + anchors.verticalCenter: parent.verticalCenter + anchors.left: visualiser.right +- anchors.leftMargin: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.spacing.normal + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + id: title +@@ -169,7 +217,7 @@ Item { + horizontalAlignment: Text.AlignHCenter + text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") + color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + elide: Text.ElideRight + } + +@@ -184,7 +232,7 @@ Item { + visible: !!Players.active + text: Players.active?.trackAlbum || qsTr("Unknown album") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + } + +@@ -202,19 +250,34 @@ Item { + wrapMode: Players.active ? Text.NoWrap : Text.WordWrap + } + ++ LyricsView { ++ id: lyricsViewInDetails ++ ++ Layout.fillWidth: true ++ Layout.preferredHeight: 200 ++ } ++ + RowLayout { + id: controls + + Layout.alignment: Qt.AlignHCenter +- Layout.topMargin: Appearance.spacing.small +- Layout.bottomMargin: Appearance.spacing.smaller ++ Layout.topMargin: Tokens.spacing.small ++ Layout.bottomMargin: Tokens.spacing.smaller + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small ++ ++ PlayerControl { ++ type: IconButton.Text ++ icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" ++ font.pointSize: Math.round(Tokens.font.size.large) ++ disabled: !Players.active?.shuffleSupported ++ onClicked: Players.active.shuffle = !Players.active?.shuffle ++ } + + PlayerControl { + type: IconButton.Text + icon: "skip_previous" +- font.pointSize: Math.round(Appearance.font.size.large * 1.5) ++ font.pointSize: Math.round(Tokens.font.size.large * 1.5) + disabled: !Players.active?.canGoPrevious + onClicked: Players.active?.previous() + } +@@ -223,9 +286,9 @@ Item { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + label.animate: true + toggle: true +- padding: Appearance.padding.small / 2 ++ padding: Tokens.padding.small / 2 + checked: Players.active?.isPlaying ?? false +- font.pointSize: Math.round(Appearance.font.size.large * 1.5) ++ font.pointSize: Math.round(Tokens.font.size.large * 1.5) + disabled: !Players.active?.canTogglePlaying + onClicked: Players.active?.togglePlaying() + } +@@ -233,10 +296,17 @@ Item { + PlayerControl { + type: IconButton.Text + icon: "skip_next" +- font.pointSize: Math.round(Appearance.font.size.large * 1.5) ++ font.pointSize: Math.round(Tokens.font.size.large * 1.5) + disabled: !Players.active?.canGoNext + onClicked: Players.active?.next() + } ++ ++ PlayerControl { ++ type: IconButton.Text ++ icon: "lyrics" ++ font.pointSize: Math.round(Tokens.font.size.large) ++ onClicked: root.lyricMenuOpen = !root.lyricMenuOpen ++ } + } + + StyledSlider { +@@ -244,7 +314,7 @@ Item { + + enabled: !!Players.active + implicitWidth: 280 +- implicitHeight: Appearance.padding.normal * 3 ++ implicitHeight: Tokens.padding.normal * 3 + + onMoved: { + const active = Players.active; +@@ -260,9 +330,6 @@ Item { + } + + CustomMouseArea { +- anchors.fill: parent +- acceptedButtons: Qt.NoButton +- + function onWheel(event: WheelEvent) { + const active = Players.active; + if (!active?.canSeek || !active?.positionSupported) +@@ -274,6 +341,9 @@ Item { + active.position = Math.max(0, Math.min(active.length, active.position + delta)); + }); + } ++ ++ anchors.fill: parent ++ acceptedButtons: Qt.NoButton + } + } + +@@ -286,9 +356,9 @@ Item { + + anchors.left: parent.left + +- text: root.lengthStr(Players.active?.position ?? -1) ++ text: root.lengthStr(Players.active ? Players.active.position % Players.active.length : -1) + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledText { +@@ -298,105 +368,146 @@ Item { + + text: root.lengthStr(Players.active?.length ?? -1) + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } ++ } + +- RowLayout { +- Layout.alignment: Qt.AlignHCenter +- spacing: Appearance.spacing.small ++ ColumnLayout { ++ id: leftSection + +- PlayerControl { +- type: IconButton.Text +- icon: "move_up" +- inactiveOnColour: Colours.palette.m3secondary +- padding: Appearance.padding.small +- font.pointSize: Appearance.font.size.large +- disabled: !Players.active?.canRaise +- onClicked: { +- Players.active?.raise(); +- root.visibilities.dashboard = false; +- } ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 ++ anchors.left: details.right ++ anchors.leftMargin: Tokens.spacing.normal ++ ++ visible: lyricMenu.height === 0 || opacity > 0 ++ opacity: lyricMenu.height === 0 ? 1 : 0 ++ ++ Behavior on opacity { ++ NumberAnimation { ++ duration: Tokens.anim.durations.normal ++ easing.type: Easing.OutCubic + } ++ } ++ ++ Item { ++ id: bongocat + +- SplitButton { +- id: playerSelector ++ implicitWidth: visualiser.width ++ implicitHeight: visualiser.height + +- disabled: !Players.list.length +- active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null +- menu.onItemSelected: item => Players.manualActive = item.modelData ++ AnimatedImage { ++ anchors.centerIn: parent + +- menuItems: playerList.instances +- fallbackIcon: "music_off" +- fallbackText: qsTr("No players") ++ width: visualiser.width * 0.75 ++ height: visualiser.height * 0.75 + +- label.Layout.maximumWidth: slider.implicitWidth * 0.28 +- label.elide: Text.ElideRight ++ playing: Players.active?.isPlaying ?? false ++ speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type ++ source: Paths.absolutePath(Config.paths.mediaGif) ++ asynchronous: true ++ fillMode: AnimatedImage.PreserveAspectFit ++ } ++ } ++ } + +- stateLayer.disabled: true +- menuOnTop: true ++ LyricMenu { ++ id: lyricMenu + +- Variants { +- id: playerList ++ anchors.top: parent.top ++ anchors.left: details.right ++ anchors.right: parent.right ++ anchors.leftMargin: Tokens.spacing.normal + +- model: Players.list ++ contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Tokens.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight + +- MenuItem { +- required property MprisPlayer modelData ++ visible: root.lyricMenuOpen || height > 0 ++ height: root.lyricMenuOpen ? implicitHeight : 0 ++ clip: true + +- icon: modelData === Players.active ? "check" : "" +- text: Players.getIdentity(modelData) +- activeIcon: "animated_images" +- } +- } ++ Behavior on height { ++ NumberAnimation { ++ duration: Tokens.anim.durations.normal ++ easing.type: Easing.OutCubic + } ++ } ++ } + +- PlayerControl { +- type: IconButton.Text +- icon: "delete" +- inactiveOnColour: Colours.palette.m3error +- padding: Appearance.padding.small +- font.pointSize: Appearance.font.size.large +- disabled: !Players.active?.canQuit +- onClicked: Players.active?.quit() ++ RowLayout { ++ id: playerChanger ++ ++ parent: !root.lyricsShowingDebounced ? details : leftSection ++ Layout.alignment: Qt.AlignHCenter ++ spacing: Tokens.spacing.small ++ ++ PlayerControl { ++ type: IconButton.Text ++ icon: "move_up" ++ inactiveOnColour: Colours.palette.m3secondary ++ padding: Tokens.padding.small ++ font.pointSize: Tokens.font.size.large ++ disabled: !Players.active?.canRaise ++ onClicked: { ++ Players.active?.raise(); ++ root.visibilities.dashboard = false; + } + } +- } + +- Item { +- id: bongocat ++ SplitButton { ++ id: playerSelector + +- anchors.verticalCenter: parent.verticalCenter +- anchors.left: details.right +- anchors.leftMargin: Appearance.spacing.normal ++ disabled: !Players.list.length ++ active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null ++ menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData + +- implicitWidth: visualiser.width +- implicitHeight: visualiser.height ++ menuItems: playerList.instances ++ fallbackIcon: "music_off" ++ fallbackText: qsTr("No players") + +- AnimatedImage { +- anchors.centerIn: parent ++ label.Layout.maximumWidth: slider.implicitWidth * 0.28 ++ label.elide: Text.ElideRight + +- width: visualiser.width * 0.75 +- height: visualiser.height * 0.75 ++ stateLayer.disabled: true ++ menuOnTop: true + +- playing: Players.active?.isPlaying ?? false +- speed: Audio.beatTracker.bpm / 300 +- source: Paths.absolutePath(Config.paths.mediaGif) +- asynchronous: true +- fillMode: AnimatedImage.PreserveAspectFit ++ Variants { ++ id: playerList ++ ++ model: Players.list ++ ++ PlayerItem {} ++ } + } ++ ++ PlayerControl { ++ type: IconButton.Text ++ icon: "delete" ++ inactiveOnColour: Colours.palette.m3error ++ padding: Tokens.padding.small ++ font.pointSize: Tokens.font.size.large ++ disabled: !Players.active?.canQuit ++ onClicked: Players.active?.quit() ++ } ++ } ++ ++ component PlayerItem: MenuItem { ++ required property MprisPlayer modelData ++ ++ icon: modelData === Players.active ? "check" : "" ++ text: Players.getIdentity(modelData) ++ activeIcon: "animated_images" + } + + component PlayerControl: IconButton { +- Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) +- radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2 +- radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial +- radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) ++ radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : implicitHeight / 2 ++ radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial ++ radiusAnim.easing: Tokens.anim.expressiveFastSpatial + + Behavior on Layout.preferredWidth { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +diff --git a/modules/dashboard/MediaWrapper.qml b/modules/dashboard/MediaWrapper.qml +new file mode 100644 +index 00000000..b03a11ee +--- /dev/null ++++ b/modules/dashboard/MediaWrapper.qml +@@ -0,0 +1,13 @@ ++import QtQuick ++ ++Item { ++ property alias visibilities: media.visibilities ++ readonly property alias needsKeyboard: media.needsKeyboard ++ ++ implicitWidth: media.implicitWidth ++ implicitHeight: media.nonAnimHeight ++ ++ Media { ++ id: media ++ } ++} +diff --git a/modules/dashboard/Monitors.qml b/modules/dashboard/Monitors.qml +index e1bfd4a0..8af11306 100644 +--- a/modules/dashboard/Monitors.qml ++++ b/modules/dashboard/Monitors.qml +@@ -1,7 +1,7 @@ + import qs.components + import qs.components.controls + import qs.services +-import qs.config ++import Caelestia.Config + import Quickshell + import QtQuick + import QtQuick.Layouts +@@ -9,15 +9,15 @@ import QtQuick.Layouts + ColumnLayout { + id: root + +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + RowLayout { + Layout.fillWidth: true +- Layout.margins: Appearance.padding.normal ++ Layout.margins: Tokens.padding.normal + + StyledText { + text: qsTr("Monitors") +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + Layout.fillWidth: true + } + +@@ -40,7 +40,7 @@ ColumnLayout { + id: monitorsLayout + anchors.left: parent.left + anchors.right: parent.right +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Repeater { + model: Hyprctl.monitors +@@ -48,9 +48,9 @@ ColumnLayout { + delegate: StyledRect { + id: monitorDelegate + Layout.fillWidth: true +- implicitHeight: monitorContent.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: monitorContent.implicitHeight + Tokens.padding.large * 2 + color: Colours.tPalette.m3surfaceContainerHigh +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + + readonly property var mon: modelData + readonly property var brightnessMon: Brightness.getMonitor(mon.name) +@@ -58,8 +58,8 @@ ColumnLayout { + ColumnLayout { + id: monitorContent + anchors.fill: parent +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.medium ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.medium + + RowLayout { + Layout.fillWidth: true +@@ -72,13 +72,13 @@ ColumnLayout { + spacing: 0 + StyledText { + text: `${mon.name} - ${mon.make} ${mon.model}` +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + Layout.fillWidth: true + } + StyledText { + text: `${mon.width}x${mon.height}@${(mon.refreshRate ?? 0).toFixed(2)}Hz` + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + StyledText { +@@ -94,7 +94,7 @@ ColumnLayout { + + MaterialIcon { + text: "brightness_medium" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledSlider { +@@ -115,7 +115,7 @@ ColumnLayout { + + MaterialIcon { + text: "zoom_in" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledSlider { +@@ -135,7 +135,7 @@ ColumnLayout { + // Refresh Rate + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Refresh Rate") +@@ -155,7 +155,7 @@ ColumnLayout { + // Rotation + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Rotation") +@@ -182,7 +182,7 @@ ColumnLayout { + // Arrangement + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + text: qsTr("Position relative to:") +diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml +index 5e00d892..c93499ca 100644 +--- a/modules/dashboard/Performance.qml ++++ b/modules/dashboard/Performance.qml +@@ -1,227 +1,826 @@ ++import QtQuick ++import QtQuick.Controls ++import QtQuick.Layouts ++import Quickshell.Services.UPower ++import Caelestia.Config ++import Caelestia.Internal + import qs.components + import qs.components.misc + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + +-RowLayout { ++Item { + id: root + +- readonly property int padding: Appearance.padding.large ++ readonly property int minWidth: 400 + 400 + Tokens.spacing.normal + 120 + Tokens.padding.large * 2 + + function displayTemp(temp: real): string { +- return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`; ++ return `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}`; + } + +- spacing: Appearance.spacing.large * 3 ++ implicitWidth: Math.max(minWidth, content.implicitWidth) ++ implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight + +- Ref { +- service: SystemUsage +- } ++ StyledRect { ++ id: placeholder ++ ++ anchors.centerIn: parent ++ width: 400 ++ height: 350 ++ radius: Tokens.rounding.large ++ color: Colours.tPalette.m3surfaceContainer ++ visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) + +- Resource { +- Layout.alignment: Qt.AlignVCenter +- Layout.topMargin: root.padding +- Layout.bottomMargin: root.padding +- Layout.leftMargin: root.padding * 2 ++ ColumnLayout { ++ anchors.centerIn: parent ++ spacing: Tokens.spacing.normal + +- value1: Math.min(1, SystemUsage.gpuTemp / 90) +- value2: SystemUsage.gpuPerc ++ MaterialIcon { ++ Layout.alignment: Qt.AlignHCenter ++ text: "tune" ++ font.pointSize: Tokens.font.size.extraLarge * 2 ++ color: Colours.palette.m3onSurfaceVariant ++ } + +- label1: root.displayTemp(SystemUsage.gpuTemp) +- label2: `${Math.round(SystemUsage.gpuPerc * 100)}%` ++ StyledText { ++ Layout.alignment: Qt.AlignHCenter ++ text: qsTr("No widgets enabled") ++ font.pointSize: Tokens.font.size.large ++ color: Colours.palette.m3onSurface ++ } + +- sublabel1: qsTr("GPU temp") +- sublabel2: qsTr("Usage") ++ StyledText { ++ Layout.alignment: Qt.AlignHCenter ++ text: qsTr("Enable widgets in dashboard settings") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ } + } + +- Resource { +- Layout.alignment: Qt.AlignVCenter +- Layout.topMargin: root.padding +- Layout.bottomMargin: root.padding ++ RowLayout { ++ id: content + +- primary: true ++ anchors.left: parent.left ++ anchors.right: parent.right ++ spacing: Tokens.spacing.normal ++ visible: !placeholder.visible + +- value1: Math.min(1, SystemUsage.cpuTemp / 90) +- value2: SystemUsage.cpuPerc ++ Ref { ++ service: SystemUsage ++ } ++ ++ ColumnLayout { ++ id: mainColumn ++ ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") ++ ++ HeroCard { ++ Layout.fillWidth: true ++ Layout.minimumWidth: 400 ++ Layout.preferredHeight: 150 ++ visible: Config.dashboard.performance.showCpu ++ icon: "memory" ++ title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU") ++ mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%` ++ mainLabel: qsTr("Usage") ++ secondaryValue: root.displayTemp(SystemUsage.cpuTemp) ++ secondaryLabel: qsTr("Temp") ++ usage: SystemUsage.cpuPerc ++ temperature: SystemUsage.cpuTemp ++ accentColor: Colours.palette.m3primary ++ } ++ ++ HeroCard { ++ Layout.fillWidth: true ++ Layout.minimumWidth: 400 ++ Layout.preferredHeight: 150 ++ visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE" ++ icon: "desktop_windows" ++ title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU") ++ mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%` ++ mainLabel: qsTr("Usage") ++ secondaryValue: root.displayTemp(SystemUsage.gpuTemp) ++ secondaryLabel: qsTr("Temp") ++ usage: SystemUsage.gpuPerc ++ temperature: SystemUsage.gpuTemp ++ accentColor: Colours.palette.m3secondary ++ } ++ } + +- label1: root.displayTemp(SystemUsage.cpuTemp) +- label2: `${Math.round(SystemUsage.cpuPerc * 100)}%` ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork ++ ++ GaugeCard { ++ Layout.minimumWidth: 250 ++ Layout.preferredHeight: 220 ++ Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork ++ icon: "memory_alt" ++ title: qsTr("Memory") ++ percentage: SystemUsage.memPerc ++ subtitle: { ++ const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed); ++ const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal); ++ return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; ++ } ++ accentColor: Colours.palette.m3tertiary ++ visible: Config.dashboard.performance.showMemory ++ } ++ ++ StorageGaugeCard { ++ Layout.minimumWidth: 250 ++ Layout.preferredHeight: 220 ++ Layout.fillWidth: !Config.dashboard.performance.showNetwork ++ visible: Config.dashboard.performance.showStorage ++ } ++ ++ NetworkCard { ++ Layout.fillWidth: true ++ Layout.minimumWidth: 200 ++ Layout.preferredHeight: 220 ++ visible: Config.dashboard.performance.showNetwork ++ } ++ } ++ } + +- sublabel1: qsTr("CPU temp") +- sublabel2: qsTr("Usage") ++ BatteryTank { ++ Layout.preferredWidth: 120 ++ Layout.preferredHeight: mainColumn.implicitHeight ++ visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery ++ } + } + +- Resource { +- Layout.alignment: Qt.AlignVCenter +- Layout.topMargin: root.padding +- Layout.bottomMargin: root.padding +- Layout.rightMargin: root.padding * 3 ++ component BatteryTank: StyledClippingRect { ++ id: batteryTank ++ ++ property real percentage: UPower.displayDevice.percentage ++ property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging ++ property color accentColor: Colours.palette.m3primary ++ property real animatedPercentage: 0 ++ ++ color: Colours.tPalette.m3surfaceContainer ++ radius: Tokens.rounding.large ++ Component.onCompleted: animatedPercentage = percentage ++ onPercentageChanged: animatedPercentage = percentage ++ ++ // Background Fill ++ StyledRect { ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.bottom: parent.bottom ++ height: parent.height * batteryTank.animatedPercentage ++ color: Qt.alpha(batteryTank.accentColor, 0.15) ++ } + +- value1: SystemUsage.memPerc +- value2: SystemUsage.storagePerc ++ ColumnLayout { ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.small ++ ++ // Header Section ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.small ++ ++ MaterialIcon { ++ text: { ++ if (!UPower.displayDevice.isLaptopBattery) { ++ if (PowerProfiles.profile === PowerProfile.PowerSaver) ++ return "energy_savings_leaf"; ++ ++ if (PowerProfiles.profile === PowerProfile.Performance) ++ return "rocket_launch"; ++ ++ return "balance"; ++ } ++ if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) ++ return "battery_full"; ++ ++ const perc = UPower.displayDevice.percentage; ++ const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); ++ if (perc >= 0.99) ++ return "battery_full"; ++ ++ let level = Math.floor(perc * 7); ++ if (charging && (level === 4 || level === 1)) ++ level--; ++ ++ return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; ++ } ++ font.pointSize: Tokens.font.size.large ++ color: batteryTank.accentColor ++ } ++ ++ StyledText { ++ Layout.fillWidth: true ++ text: qsTr("Battery") ++ font.pointSize: Tokens.font.size.normal ++ color: Colours.palette.m3onSurface ++ } ++ } + +- label1: { +- const fmt = SystemUsage.formatKib(SystemUsage.memUsed); +- return `${+fmt.value.toFixed(1)}${fmt.unit}`; ++ Item { ++ Layout.fillHeight: true ++ } ++ ++ // Bottom Info Section ++ ColumnLayout { ++ Layout.fillWidth: true ++ spacing: -4 ++ ++ StyledText { ++ Layout.alignment: Qt.AlignRight ++ text: `${Math.round(batteryTank.percentage * 100)}%` ++ font.pointSize: Tokens.font.size.extraLarge ++ font.weight: Font.Medium ++ color: batteryTank.accentColor ++ } ++ ++ StyledText { ++ Layout.alignment: Qt.AlignRight ++ text: { ++ if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) ++ return qsTr("Full"); ++ ++ if (batteryTank.isCharging) ++ return qsTr("Charging"); ++ ++ const s = UPower.displayDevice.timeToEmpty; ++ if (s === 0) ++ return qsTr("..."); ++ ++ const hr = Math.floor(s / 3600); ++ const min = Math.floor((s % 3600) / 60); ++ if (hr > 0) ++ return `${hr}h ${min}m`; ++ ++ return `${min}m`; ++ } ++ font.pointSize: Tokens.font.size.smaller ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ } ++ } ++ ++ Behavior on animatedPercentage { ++ Anim { ++ type: Anim.StandardLarge ++ } + } +- label2: { +- const fmt = SystemUsage.formatKib(SystemUsage.storageUsed); +- return `${Math.floor(fmt.value)}${fmt.unit}`; ++ } ++ ++ component CardHeader: RowLayout { ++ property string icon ++ property string title ++ property color accentColor: Colours.palette.m3primary ++ ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.small ++ ++ MaterialIcon { ++ text: parent.icon ++ fill: 1 ++ color: parent.accentColor ++ font.pointSize: Tokens.spacing.large + } + +- sublabel1: qsTr("Memory") +- sublabel2: qsTr("Storage") ++ StyledText { ++ Layout.fillWidth: true ++ text: parent.title ++ font.pointSize: Tokens.font.size.normal ++ elide: Text.ElideRight ++ } + } + +- component Resource: Item { +- id: res ++ component ProgressBar: StyledRect { ++ id: progressBar ++ ++ property real value: 0 ++ property color fgColor: Colours.palette.m3primary ++ property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) ++ property real animatedValue: 0 ++ ++ color: bgColor ++ radius: Tokens.rounding.full ++ Component.onCompleted: animatedValue = value ++ onValueChanged: animatedValue = value ++ ++ StyledRect { ++ anchors.left: parent.left ++ anchors.top: parent.top ++ anchors.bottom: parent.bottom ++ width: parent.width * progressBar.animatedValue ++ color: progressBar.fgColor ++ radius: Tokens.rounding.full ++ } + +- required property real value1 +- required property real value2 +- required property string sublabel1 +- required property string sublabel2 +- required property string label1 +- required property string label2 ++ Behavior on animatedValue { ++ Anim { ++ type: Anim.StandardLarge ++ } ++ } ++ } + +- property bool primary +- readonly property real primaryMult: primary ? 1.2 : 1 ++ component HeroCard: StyledClippingRect { ++ id: heroCard ++ ++ property string icon ++ property string title ++ property string mainValue ++ property string mainLabel ++ property string secondaryValue ++ property string secondaryLabel ++ property real usage: 0 ++ property real temperature: 0 ++ property color accentColor: Colours.palette.m3primary ++ readonly property real maxTemp: 100 ++ readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp)) ++ property real animatedUsage: 0 ++ property real animatedTemp: 0 ++ ++ color: Colours.tPalette.m3surfaceContainer ++ radius: Tokens.rounding.large ++ Component.onCompleted: { ++ animatedUsage = usage; ++ animatedTemp = tempProgress; ++ } ++ onUsageChanged: animatedUsage = usage ++ onTempProgressChanged: animatedTemp = tempProgress ++ ++ StyledRect { ++ anchors.left: parent.left ++ anchors.top: parent.top ++ anchors.bottom: parent.bottom ++ implicitWidth: parent.width * heroCard.animatedUsage ++ color: Qt.alpha(heroCard.accentColor, 0.15) ++ } + +- readonly property real thickness: Config.dashboard.sizes.resourceProgessThickness * primaryMult ++ CardHeader { ++ anchors.left: parent.left ++ anchors.top: parent.top ++ anchors.leftMargin: Tokens.padding.large ++ anchors.topMargin: Math.round(Tokens.padding.large * 1.2) + +- property color fg1: Colours.palette.m3primary +- property color fg2: Colours.palette.m3secondary +- property color bg1: Colours.palette.m3primaryContainer +- property color bg2: Colours.palette.m3secondaryContainer ++ width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Tokens.spacing.normal ++ icon: heroCard.icon ++ title: heroCard.title ++ accentColor: heroCard.accentColor ++ } + +- implicitWidth: Config.dashboard.sizes.resourceSize * primaryMult +- implicitHeight: Config.dashboard.sizes.resourceSize * primaryMult ++ Column { ++ anchors.left: parent.left ++ anchors.right: parent.right ++ anchors.bottom: parent.bottom ++ anchors.margins: Math.round(Tokens.padding.large * 1.2) ++ anchors.bottomMargin: Math.round(Tokens.padding.large * 1.3) ++ ++ spacing: Tokens.spacing.small ++ ++ Row { ++ spacing: Tokens.spacing.small ++ ++ StyledText { ++ text: heroCard.secondaryValue ++ font.pointSize: Tokens.font.size.normal ++ font.weight: Font.Medium ++ } ++ ++ StyledText { ++ text: heroCard.secondaryLabel ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ anchors.baseline: parent.children[0].baseline ++ } ++ } + +- onValue1Changed: canvas.requestPaint() +- onValue2Changed: canvas.requestPaint() +- onFg1Changed: canvas.requestPaint() +- onFg2Changed: canvas.requestPaint() +- onBg1Changed: canvas.requestPaint() +- onBg2Changed: canvas.requestPaint() ++ ProgressBar { ++ implicitWidth: parent.width * 0.5 ++ implicitHeight: 6 ++ value: heroCard.tempProgress ++ fgColor: heroCard.accentColor ++ bgColor: Qt.alpha(heroCard.accentColor, 0.2) ++ } ++ } + + Column { +- anchors.centerIn: parent ++ id: usageColumn ++ ++ anchors.right: parent.right ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.margins: Tokens.padding.large ++ anchors.rightMargin: 32 ++ spacing: 0 + + StyledText { +- anchors.horizontalCenter: parent.horizontalCenter ++ id: usageLabel + +- text: res.label1 +- font.pointSize: Appearance.font.size.extraLarge * res.primaryMult ++ anchors.right: parent.right ++ text: heroCard.mainLabel ++ font.pointSize: Tokens.font.size.normal ++ color: Colours.palette.m3onSurfaceVariant + } + + StyledText { +- anchors.horizontalCenter: parent.horizontalCenter ++ anchors.right: parent.right ++ text: heroCard.mainValue ++ font.pointSize: Tokens.font.size.extraLarge ++ font.weight: Font.Medium ++ color: heroCard.accentColor ++ } ++ } + +- text: res.sublabel1 +- color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.smaller * res.primaryMult ++ Behavior on animatedUsage { ++ Anim { ++ type: Anim.StandardLarge + } + } + +- Column { +- anchors.horizontalCenter: parent.right +- anchors.top: parent.verticalCenter +- anchors.horizontalCenterOffset: -res.thickness / 2 +- anchors.topMargin: res.thickness / 2 + Appearance.spacing.small ++ Behavior on animatedTemp { ++ Anim { ++ type: Anim.StandardLarge ++ } ++ } ++ } + +- StyledText { +- anchors.horizontalCenter: parent.horizontalCenter ++ component GaugeCard: StyledRect { ++ id: gaugeCard ++ ++ property string icon ++ property string title ++ property real percentage: 0 ++ property string subtitle ++ property color accentColor: Colours.palette.m3primary ++ readonly property real arcStartAngle: 0.75 * Math.PI ++ readonly property real arcSweep: 1.5 * Math.PI ++ property real animatedPercentage: 0 ++ ++ color: Colours.tPalette.m3surfaceContainer ++ radius: Tokens.rounding.large ++ clip: true ++ Component.onCompleted: animatedPercentage = percentage ++ onPercentageChanged: animatedPercentage = percentage ++ ++ ColumnLayout { ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.smaller + +- text: res.label2 +- font.pointSize: Appearance.font.size.smaller * res.primaryMult ++ CardHeader { ++ icon: gaugeCard.icon ++ title: gaugeCard.title ++ accentColor: gaugeCard.accentColor + } + +- StyledText { +- anchors.horizontalCenter: parent.horizontalCenter ++ Item { ++ Layout.fillWidth: true ++ Layout.fillHeight: true ++ ++ ArcGauge { ++ anchors.centerIn: parent ++ width: Math.min(parent.width, parent.height) ++ height: width ++ percentage: gaugeCard.animatedPercentage ++ accentColor: gaugeCard.accentColor ++ trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) ++ startAngle: gaugeCard.arcStartAngle ++ sweepAngle: gaugeCard.arcSweep ++ } ++ ++ StyledText { ++ anchors.centerIn: parent ++ text: `${Math.round(gaugeCard.percentage * 100)}%` ++ font.pointSize: Tokens.font.size.extraLarge ++ font.weight: Font.Medium ++ color: gaugeCard.accentColor ++ } ++ } + +- text: res.sublabel2 ++ StyledText { ++ Layout.alignment: Qt.AlignHCenter ++ text: gaugeCard.subtitle ++ font.pointSize: Tokens.font.size.smaller + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small * res.primaryMult + } + } + +- Canvas { +- id: canvas ++ Behavior on animatedPercentage { ++ Anim { ++ type: Anim.StandardLarge ++ } ++ } ++ } + +- readonly property real centerX: width / 2 +- readonly property real centerY: height / 2 ++ component StorageGaugeCard: StyledRect { ++ id: storageGaugeCard ++ ++ property int currentDiskIndex: 0 ++ readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null ++ property int diskCount: 0 ++ readonly property real arcStartAngle: 0.75 * Math.PI ++ readonly property real arcSweep: 1.5 * Math.PI ++ property real animatedPercentage: 0 ++ property color accentColor: Colours.palette.m3secondary ++ ++ color: Colours.tPalette.m3surfaceContainer ++ radius: Tokens.rounding.large ++ clip: true ++ Component.onCompleted: { ++ diskCount = SystemUsage.disks.length; ++ if (currentDisk) ++ animatedPercentage = currentDisk.perc; ++ } ++ onCurrentDiskChanged: { ++ if (currentDisk) ++ animatedPercentage = currentDisk.perc; ++ } ++ ++ // Update diskCount and animatedPercentage when disks data changes ++ Connections { ++ function onDisksChanged() { ++ if (SystemUsage.disks.length !== storageGaugeCard.diskCount) ++ storageGaugeCard.diskCount = SystemUsage.disks.length; + +- readonly property real arc1Start: degToRad(45) +- readonly property real arc1End: degToRad(220) +- readonly property real arc2Start: degToRad(230) +- readonly property real arc2End: degToRad(360) ++ // Update animated percentage when disk data refreshes ++ if (storageGaugeCard.currentDisk) ++ storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; ++ } ++ ++ target: SystemUsage ++ } + +- function degToRad(deg: int): real { +- return deg * Math.PI / 180; ++ MouseArea { ++ anchors.fill: parent ++ onWheel: wheel => { ++ if (wheel.angleDelta.y > 0) ++ storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; ++ else if (wheel.angleDelta.y < 0) ++ storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; + } ++ } + ++ ColumnLayout { + anchors.fill: parent ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.smaller ++ ++ CardHeader { ++ icon: "hard_disk" ++ title: { ++ const base = qsTr("Storage"); ++ if (!storageGaugeCard.currentDisk) ++ return base; ++ ++ return `${base} - ${storageGaugeCard.currentDisk.mount}`; ++ } ++ accentColor: storageGaugeCard.accentColor ++ ++ // Scroll hint icon ++ MaterialIcon { ++ text: "unfold_more" ++ color: Colours.palette.m3onSurfaceVariant ++ font.pointSize: Tokens.font.size.normal ++ visible: storageGaugeCard.diskCount > 1 ++ opacity: 0.7 ++ ToolTip.visible: hintHover.hovered ++ ToolTip.text: qsTr("Scroll to switch disks") ++ ToolTip.delay: 500 ++ ++ HoverHandler { ++ id: hintHover ++ } ++ } ++ } + +- onPaint: { +- const ctx = getContext("2d"); +- ctx.reset(); ++ Item { ++ Layout.fillWidth: true ++ Layout.fillHeight: true ++ ++ ArcGauge { ++ anchors.centerIn: parent ++ width: Math.min(parent.width, parent.height) ++ height: width ++ percentage: storageGaugeCard.animatedPercentage ++ accentColor: storageGaugeCard.accentColor ++ trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) ++ startAngle: storageGaugeCard.arcStartAngle ++ sweepAngle: storageGaugeCard.arcSweep ++ } ++ ++ StyledText { ++ anchors.centerIn: parent ++ text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" ++ font.pointSize: Tokens.font.size.extraLarge ++ font.weight: Font.Medium ++ color: storageGaugeCard.accentColor ++ } ++ } + +- ctx.lineWidth = res.thickness; +- ctx.lineCap = Appearance.rounding.scale === 0 ? "square" : "round"; ++ StyledText { ++ Layout.alignment: Qt.AlignHCenter ++ text: { ++ if (!storageGaugeCard.currentDisk) ++ return "—"; ++ ++ const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); ++ const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); ++ return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; ++ } ++ font.pointSize: Tokens.font.size.smaller ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ } + +- const radius = (Math.min(width, height) - ctx.lineWidth) / 2; +- const cx = centerX; +- const cy = centerY; +- const a1s = arc1Start; +- const a1e = arc1End; +- const a2s = arc2Start; +- const a2e = arc2End; ++ Behavior on animatedPercentage { ++ Anim { ++ type: Anim.StandardLarge ++ } ++ } ++ } + +- ctx.beginPath(); +- ctx.arc(cx, cy, radius, a1s, a1e, false); +- ctx.strokeStyle = res.bg1; +- ctx.stroke(); ++ component NetworkCard: StyledRect { ++ id: networkCard + +- ctx.beginPath(); +- ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false); +- ctx.strokeStyle = res.fg1; +- ctx.stroke(); ++ property color accentColor: Colours.palette.m3primary + +- ctx.beginPath(); +- ctx.arc(cx, cy, radius, a2s, a2e, false); +- ctx.strokeStyle = res.bg2; +- ctx.stroke(); ++ color: Colours.tPalette.m3surfaceContainer ++ radius: Tokens.rounding.large ++ clip: true + +- ctx.beginPath(); +- ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false); +- ctx.strokeStyle = res.fg2; +- ctx.stroke(); +- } ++ Ref { ++ service: NetworkUsage + } + +- Behavior on value1 { +- Anim {} +- } ++ ColumnLayout { ++ anchors.fill: parent ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.small + +- Behavior on value2 { +- Anim {} +- } ++ CardHeader { ++ icon: "swap_vert" ++ title: qsTr("Network") ++ accentColor: networkCard.accentColor ++ } + +- Behavior on fg1 { +- CAnim {} +- } ++ // Sparkline graph ++ Item { ++ Layout.fillWidth: true ++ Layout.fillHeight: true ++ ++ SparklineItem { ++ id: sparkline ++ ++ property real targetMax: 1024 ++ property real smoothMax: targetMax ++ ++ anchors.fill: parent ++ line1: NetworkUsage.uploadBuffer // qmllint disable missing-type ++ line1Color: Colours.palette.m3secondary ++ line1FillAlpha: 0.15 ++ line2: NetworkUsage.downloadBuffer // qmllint disable missing-type ++ line2Color: Colours.palette.m3tertiary ++ line2FillAlpha: 0.2 ++ maxValue: smoothMax ++ historyLength: NetworkUsage.historyLength ++ ++ Connections { ++ function onValuesChanged(): void { ++ sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); ++ slideAnim.restart(); ++ } ++ ++ target: NetworkUsage.downloadBuffer ++ } ++ ++ NumberAnimation { ++ id: slideAnim ++ ++ target: sparkline ++ property: "slideProgress" ++ from: 0 ++ to: 1 ++ duration: GlobalConfig.dashboard.resourceUpdateInterval ++ } ++ ++ Behavior on smoothMax { ++ Anim { ++ type: Anim.StandardLarge ++ } ++ } ++ } ++ ++ // "No data" placeholder ++ StyledText { ++ anchors.centerIn: parent ++ text: qsTr("Collecting data...") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ visible: NetworkUsage.downloadBuffer.count < 2 ++ opacity: 0.6 ++ } ++ } + +- Behavior on fg2 { +- CAnim {} +- } ++ // Download row ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ ++ MaterialIcon { ++ text: "download" ++ color: Colours.palette.m3tertiary ++ font.pointSize: Tokens.font.size.normal ++ } ++ ++ StyledText { ++ text: qsTr("Download") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ ++ StyledText { ++ text: { ++ const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); ++ return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; ++ } ++ font.pointSize: Tokens.font.size.normal ++ font.weight: Font.Medium ++ color: Colours.palette.m3tertiary ++ } ++ } + +- Behavior on bg1 { +- CAnim {} +- } ++ // Upload row ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ ++ MaterialIcon { ++ text: "upload" ++ color: Colours.palette.m3secondary ++ font.pointSize: Tokens.font.size.normal ++ } ++ ++ StyledText { ++ text: qsTr("Upload") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ ++ StyledText { ++ text: { ++ const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); ++ return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; ++ } ++ font.pointSize: Tokens.font.size.normal ++ font.weight: Font.Medium ++ color: Colours.palette.m3secondary ++ } ++ } + +- Behavior on bg2 { +- CAnim {} ++ // Session totals ++ RowLayout { ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.normal ++ ++ MaterialIcon { ++ text: "history" ++ color: Colours.palette.m3onSurfaceVariant ++ font.pointSize: Tokens.font.size.normal ++ } ++ ++ StyledText { ++ text: qsTr("Total") ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ ++ Item { ++ Layout.fillWidth: true ++ } ++ ++ StyledText { ++ text: { ++ const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); ++ const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); ++ return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; ++ } ++ font.pointSize: Tokens.font.size.small ++ color: Colours.palette.m3onSurfaceVariant ++ } ++ } + } + } + } +diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml +index eef01be7..5bf99ca3 100644 +--- a/modules/dashboard/Tabs.qml ++++ b/modules/dashboard/Tabs.qml +@@ -1,19 +1,21 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Controls ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Controls + + Item { + id: root + + required property real nonAnimWidth +- required property PersistentProperties state ++ required property DashboardState dashState ++ required property var tabs ++ + readonly property alias count: bar.count + + implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight +@@ -25,55 +27,45 @@ Item { + anchors.right: parent.right + anchors.top: parent.top + +- currentIndex: root.state.currentTab ++ currentIndex: root.dashState.currentTab + background: null + +- onCurrentIndexChanged: root.state.currentTab = currentIndex +- +- Tab { +- iconName: "dashboard" +- text: qsTr("Dashboard") +- } +- +- Tab { +- iconName: "queue_music" +- text: qsTr("Media") +- } ++ onCurrentIndexChanged: root.dashState.currentTab = currentIndex + +- Tab { +- iconName: "speed" +- text: qsTr("Performance") +- } ++ Repeater { ++ model: ScriptModel { ++ values: root.tabs ++ } + +- Tab { +- iconName: "cloud" +- text: qsTr("Weather") +- } ++ delegate: Tab { ++ required property var modelData + +- Tab { +- iconName: "monitor" +- text: qsTr("Monitors") ++ iconName: modelData.iconName ++ text: modelData.text ++ } + } +- +- // Tab { +- // iconName: "workspaces" +- // text: qsTr("Workspaces") +- // } + } + + Item { + id: indicator + + anchors.top: bar.bottom +- anchors.topMargin: Config.dashboard.sizes.tabIndicatorSpacing ++ anchors.topMargin: 5 + +- implicitWidth: bar.currentItem.implicitWidth +- implicitHeight: Config.dashboard.sizes.tabIndicatorHeight ++ implicitWidth: { ++ const tab = bar.currentItem; ++ if (tab) ++ return tab.implicitWidth; ++ const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; ++ return width; ++ } ++ implicitHeight: 3 + + x: { + const tab = bar.currentItem; + const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; +- return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; ++ const tabWidth = tab?.implicitWidth ?? width; ++ return width * bar.currentIndex + (width - tabWidth) / 2; + } + + clip: true +@@ -85,7 +77,7 @@ Item { + implicitHeight: parent.implicitHeight * 2 + + color: Colours.palette.m3primary +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + } + + Behavior on x { +@@ -119,13 +111,21 @@ Item { + contentItem: CustomMouseArea { + id: mouse + ++ function onWheel(event: WheelEvent): void { ++ if (event.angleDelta.y < 0) ++ root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, bar.count - 1); ++ else if (event.angleDelta.y > 0) ++ root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); ++ } ++ + implicitWidth: Math.max(icon.width, label.width) + implicitHeight: icon.height + label.height + ++ hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onPressed: event => { +- root.state.currentTab = tab.TabBar.index; ++ root.dashState.currentTab = tab.TabBar.index; + + const stateY = stateWrapper.y; + rippleAnim.x = event.x; +@@ -137,13 +137,6 @@ Item { + rippleAnim.restart(); + } + +- function onWheel(event: WheelEvent): void { +- if (event.angleDelta.y < 0) +- root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); +- else if (event.angleDelta.y > 0) +- root.state.currentTab = Math.max(root.state.currentTab - 1, 0); +- } +- + SequentialAnimation { + id: rippleAnim + +@@ -171,16 +164,13 @@ Item { + properties: "implicitWidth,implicitHeight" + from: 0 + to: rippleAnim.radius * 2 +- duration: Appearance.anim.durations.normal +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ duration: Tokens.anim.durations.normal ++ easing: Tokens.anim.standardDecel + } + Anim { + target: ripple + property: "opacity" + to: 0 +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard + } + } + +@@ -190,10 +180,10 @@ Item { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2 ++ implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2 + + color: "transparent" +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + + StyledRect { + id: stateLayer +@@ -201,7 +191,7 @@ Item { + anchors.fill: parent + + color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface +- opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 ++ opacity: mouse.pressed ? 0.1 : mouse.containsMouse ? 0.08 : 0 + + Behavior on opacity { + Anim {} +@@ -211,7 +201,7 @@ Item { + StyledRect { + id: ripple + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface + opacity: 0 + +@@ -231,7 +221,7 @@ Item { + text: tab.iconName + color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + fill: tab.current ? 1 : 0 +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + + Behavior on fill { + Anim {} +diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/WeatherTab.qml +similarity index 74% +rename from modules/dashboard/Weather.qml +rename to modules/dashboard/WeatherTab.qml +index 3981633a..3cf8d3b7 100644 +--- a/modules/dashboard/Weather.qml ++++ b/modules/dashboard/WeatherTab.qml +@@ -1,43 +1,42 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root + +- implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 +- implicitHeight: layout.implicitHeight +- + readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null + ++ implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 ++ implicitHeight: layout.implicitHeight + Component.onCompleted: Weather.reload() + + ColumnLayout { + id: layout + + anchors.fill: parent +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + RowLayout { +- Layout.leftMargin: Appearance.padding.large +- Layout.rightMargin: Appearance.padding.large ++ Layout.leftMargin: Tokens.padding.large ++ Layout.rightMargin: Tokens.padding.large + Layout.fillWidth: true + + Column { +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { + text: Weather.city || qsTr("Loading...") +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + font.weight: 600 + color: Colours.palette.m3onSurface + } + + StyledText { + text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } +@@ -47,7 +46,7 @@ Item { + } + + Row { +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + WeatherStat { + icon: "wb_twilight" +@@ -67,40 +66,40 @@ Item { + + StyledRect { + Layout.fillWidth: true +- implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small * 2 + +- radius: Appearance.rounding.large * 2 ++ radius: Tokens.rounding.large * 2 + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: bigInfoRow + + anchors.centerIn: parent +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: Weather.icon +- font.pointSize: Appearance.font.size.extraLarge * 3 ++ font.pointSize: Tokens.font.size.extraLarge * 3 + color: Colours.palette.m3secondary + animate: true + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter +- spacing: -Appearance.spacing.small ++ spacing: -Tokens.spacing.small + + StyledText { + text: Weather.temp +- font.pointSize: Appearance.font.size.extraLarge * 2 ++ font.pointSize: Tokens.font.size.extraLarge * 2 + font.weight: 500 + color: Colours.palette.m3primary + } + + StyledText { +- Layout.leftMargin: Appearance.padding.small ++ Layout.leftMargin: Tokens.padding.small + text: Weather.description +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + } +@@ -109,7 +108,7 @@ Item { + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + DetailCard { + icon: "water_drop" +@@ -132,18 +131,18 @@ Item { + } + + StyledText { +- Layout.topMargin: Appearance.spacing.normal +- Layout.leftMargin: Appearance.padding.normal ++ Layout.topMargin: Tokens.spacing.normal ++ Layout.leftMargin: Tokens.padding.normal + visible: forecastRepeater.count > 0 + text: qsTr("7-Day Forecast") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 600 + color: Colours.palette.m3onSurface + } + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + Repeater { + id: forecastRepeater +@@ -157,30 +156,30 @@ Item { + required property var modelData + + Layout.fillWidth: true +- implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.normal * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: forecastItemColumn + + anchors.centerIn: parent +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 600 + color: Colours.palette.m3primary + } + + StyledText { +- Layout.topMargin: -Appearance.spacing.small / 2 ++ Layout.topMargin: -Tokens.spacing.small / 2 + Layout.alignment: Qt.AlignHCenter + text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + opacity: 0.7 + color: Colours.palette.m3onSurfaceVariant + } +@@ -188,13 +187,13 @@ Item { + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.modelData.icon +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + color: Colours.palette.m3secondary + } + + StyledText { + Layout.alignment: Qt.AlignHCenter +- text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" ++ text: GlobalConfig.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" + font.weight: 600 + color: Colours.palette.m3tertiary + } +@@ -214,17 +213,17 @@ Item { + + Layout.fillWidth: true + Layout.preferredHeight: 60 +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainer + + Row { + anchors.centerIn: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: detailRoot.icon + color: detailRoot.colour +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + anchors.verticalCenter: parent.verticalCenter + } + +@@ -234,7 +233,7 @@ Item { + + StyledText { + text: detailRoot.label +- font.pointSize: Appearance.font.size.smaller ++ font.pointSize: Tokens.font.size.smaller + opacity: 0.7 + horizontalAlignment: Text.AlignLeft + } +@@ -255,23 +254,23 @@ Item { + property string value + property color colour + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + MaterialIcon { + text: weatherStat.icon +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + color: weatherStat.colour + } + + Column { + StyledText { + text: weatherStat.label +- font.pointSize: Appearance.font.size.smaller ++ font.pointSize: Tokens.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + StyledText { + text: weatherStat.value +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + font.weight: 600 + color: Colours.palette.m3onSurface + } +diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml +index 0e37909e..f7f03742 100644 +--- a/modules/dashboard/Wrapper.qml ++++ b/modules/dashboard/Wrapper.qml +@@ -1,21 +1,19 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Caelestia ++import Caelestia.Config + import qs.components + import qs.components.filedialog +-import qs.config + import qs.utils +-import Caelestia +-import Quickshell +-import QtQuick + + Item { + id: root + +- required property PersistentProperties visibilities +- readonly property PersistentProperties dashState: PersistentProperties { +- property int currentTab +- property date currentDate: new Date() +- ++ required property DrawerVisibilities visibilities ++ readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false ++ readonly property DashboardState dashState: DashboardState { + reloadableId: "dashboardState" + } + readonly property FileDialog facePicker: FileDialog { +@@ -30,60 +28,19 @@ Item { + } + } + +- readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 +- +- visible: height > 0 +- implicitHeight: 0 +- implicitWidth: content.implicitWidth +- +- onStateChanged: { +- if (state === "visible" && timer.running) { +- timer.triggered(); +- timer.stop(); +- } +- } +- +- states: State { +- name: "visible" +- when: root.visibilities.dashboard && Config.dashboard.enabled +- +- PropertyChanges { +- root.implicitHeight: content.implicitHeight +- } +- } +- +- transitions: [ +- Transition { +- from: "" +- to: "visible" +- +- Anim { +- target: root +- property: "implicitHeight" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- }, +- Transition { +- from: "visible" +- to: "" +- +- Anim { +- target: root +- property: "implicitHeight" +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- } +- ] ++ readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 ++ readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled ++ property real offsetScale: shouldBeActive ? 0 : 1 + +- Timer { +- id: timer ++ visible: offsetScale < 1 ++ anchors.topMargin: (-implicitHeight - 5) * offsetScale ++ implicitHeight: content.implicitHeight ++ implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open ++ opacity: 1 - offsetScale + +- running: true +- interval: Appearance.anim.durations.extraLarge +- onTriggered: { +- content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); +- content.visible = true; ++ Behavior on offsetScale { ++ Anim { ++ type: Anim.DefaultSpatial + } + } + +@@ -93,12 +50,11 @@ Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom + +- visible: false +- active: true ++ active: root.shouldBeActive || root.visible + + sourceComponent: Content { + visibilities: root.visibilities +- state: root.dashState ++ dashState: root.dashState + facePicker: root.facePicker + } + } +diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml +index 56c04938..e2af3c01 100644 +--- a/modules/dashboard/dash/Calendar.qml ++++ b/modules/dashboard/dash/Calendar.qml +@@ -1,61 +1,58 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.components.effects +-import qs.components.controls +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Controls + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.components.controls ++import qs.components.effects ++import qs.services + + CustomMouseArea { + id: root + +- required property var state ++ required property DashboardState dashState ++ ++ readonly property int currMonth: dashState.currentDate.getMonth() ++ readonly property int currYear: dashState.currentDate.getFullYear() + +- readonly property int currMonth: state.currentDate.getMonth() +- readonly property int currYear: state.currentDate.getFullYear() ++ function onWheel(event: WheelEvent): void { ++ if (event.angleDelta.y > 0) ++ root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1); ++ else if (event.angleDelta.y < 0) ++ root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); ++ } + + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 + + acceptedButtons: Qt.MiddleButton +- onClicked: root.state.currentDate = new Date() +- +- function onWheel(event: WheelEvent): void { +- if (event.angleDelta.y > 0) +- root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); +- else if (event.angleDelta.y < 0) +- root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); +- } ++ onClicked: root.dashState.currentDate = new Date() + + ColumnLayout { + id: inner + + anchors.fill: parent +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.small ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.small + + RowLayout { + id: monthNavigationRow + + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Item { + implicitWidth: implicitHeight +- implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: prevMonthText.implicitHeight + Tokens.padding.small * 2 + + StateLayer { + id: prevMonthStateLayer + +- radius: Appearance.rounding.full +- +- function onClicked(): void { +- root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); +- } ++ radius: Tokens.rounding.full ++ onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) + } + + MaterialIcon { +@@ -64,7 +61,7 @@ CustomMouseArea { + anchors.centerIn: parent + text: "chevron_left" + color: Colours.palette.m3tertiary +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 700 + } + } +@@ -72,24 +69,24 @@ CustomMouseArea { + Item { + Layout.fillWidth: true + +- implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 +- implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.small * 2 ++ implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.small * 2 + + StateLayer { ++ onClicked: { ++ root.dashState.currentDate = new Date(); ++ } ++ + anchors.fill: monthYearDisplay +- anchors.margins: -Appearance.padding.small +- anchors.leftMargin: -Appearance.padding.normal +- anchors.rightMargin: -Appearance.padding.normal ++ anchors.margins: -Tokens.padding.small ++ anchors.leftMargin: -Tokens.padding.normal ++ anchors.rightMargin: -Tokens.padding.normal + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + disabled: { + const now = new Date(); + return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); + } +- +- function onClicked(): void { +- root.state.currentDate = new Date(); +- } + } + + StyledText { +@@ -98,7 +95,7 @@ CustomMouseArea { + anchors.centerIn: parent + text: grid.title + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + font.capitalization: Font.Capitalize + } +@@ -106,16 +103,16 @@ CustomMouseArea { + + Item { + implicitWidth: implicitHeight +- implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: nextMonthText.implicitHeight + Tokens.padding.small * 2 + + StateLayer { + id: nextMonthStateLayer + +- radius: Appearance.rounding.full +- +- function onClicked(): void { +- root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); ++ onClicked: { ++ root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + } ++ ++ radius: Tokens.rounding.full + } + + MaterialIcon { +@@ -124,7 +121,7 @@ CustomMouseArea { + anchors.centerIn: parent + text: "chevron_right" + color: Colours.palette.m3tertiary +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 700 + } + } +@@ -167,7 +164,7 @@ CustomMouseArea { + required property var model + + implicitWidth: implicitHeight +- implicitHeight: text.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: text.implicitHeight + Tokens.padding.small * 2 + + StyledText { + id: text +@@ -184,7 +181,7 @@ CustomMouseArea { + return Colours.palette.m3onSurfaceVariant; + } + opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + font.weight: 500 + } + } +@@ -208,7 +205,7 @@ CustomMouseArea { + implicitHeight: today?.implicitHeight ?? 0 + + clip: true +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3primary + + opacity: todayItem ? 1 : 0 +@@ -236,15 +233,13 @@ CustomMouseArea { + + Behavior on x { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + Behavior on y { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml +index e7404488..82bee151 100644 +--- a/modules/dashboard/dash/DateTime.qml ++++ b/modules/dashboard/dash/DateTime.qml +@@ -1,17 +1,17 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root + + anchors.top: parent.top + anchors.bottom: parent.bottom +- implicitWidth: Config.dashboard.sizes.dateTimeWidth ++ implicitWidth: Tokens.sizes.dashboard.dateTimeWidth + + ColumnLayout { + anchors.left: parent.left +@@ -24,8 +24,8 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: Time.hourStr + color: Colours.palette.m3secondary +- font.pointSize: Appearance.font.size.extraLarge +- font.family: Appearance.font.family.clock ++ font.pointSize: Tokens.font.size.extraLarge ++ font.family: Tokens.font.family.clock + font.weight: 600 + } + +@@ -33,8 +33,8 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: "•••" + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.extraLarge * 0.9 +- font.family: Appearance.font.family.clock ++ font.pointSize: Tokens.font.size.extraLarge * 0.9 ++ font.family: Tokens.font.family.clock + } + + StyledText { +@@ -42,22 +42,23 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: Time.minuteStr + color: Colours.palette.m3secondary +- font.pointSize: Appearance.font.size.extraLarge +- font.family: Appearance.font.family.clock ++ font.pointSize: Tokens.font.size.extraLarge ++ font.family: Tokens.font.family.clock + font.weight: 600 + } + + Loader { ++ asynchronous: true + Layout.alignment: Qt.AlignHCenter + +- active: Config.services.useTwelveHourClock ++ active: GlobalConfig.services.useTwelveHourClock + visible: active + + sourceComponent: StyledText { + text: Time.amPmStr + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.large +- font.family: Appearance.font.family.clock ++ font.pointSize: Tokens.font.size.large ++ font.family: Tokens.font.family.clock + font.weight: 600 + } + } +diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml +index 3a2b685e..a14e5e38 100644 +--- a/modules/dashboard/dash/Media.qml ++++ b/modules/dashboard/dash/Media.qml +@@ -1,32 +1,33 @@ ++import QtQuick ++import QtQuick.Shapes ++import Quickshell ++import Caelestia.Config ++import Caelestia.Services + import qs.components + import qs.services +-import qs.config + import qs.utils +-import Caelestia.Services +-import QtQuick +-import QtQuick.Shapes + + Item { + id: root + + property real playerProgress: { + const active = Players.active; +- return active?.length ? active.position / active.length : 0; ++ return active?.length ? (active.position % active.length) / active.length : 0; + } + + anchors.top: parent.top + anchors.bottom: parent.bottom +- implicitWidth: Config.dashboard.sizes.mediaWidth ++ implicitWidth: Tokens.sizes.dashboard.mediaWidth + + Behavior on playerProgress { + Anim { +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + } + + Timer { + running: Players.active?.isPlaying ?? false +- interval: Config.dashboard.mediaUpdateInterval ++ interval: GlobalConfig.dashboard.mediaUpdateInterval + triggeredOnStart: true + repeat: true + onTriggered: Players.active?.positionChanged() +@@ -42,16 +43,16 @@ Item { + ShapePath { + fillColor: "transparent" + strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) +- strokeWidth: Config.dashboard.sizes.mediaProgressThickness +- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap ++ strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness ++ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + centerX: cover.x + cover.width / 2 + centerY: cover.y + cover.height / 2 +- radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small +- radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small +- startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 +- sweepAngle: Config.dashboard.sizes.mediaProgressSweep ++ radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small ++ radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small ++ startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 ++ sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep + } + + Behavior on strokeColor { +@@ -62,16 +63,16 @@ Item { + ShapePath { + fillColor: "transparent" + strokeColor: Colours.palette.m3primary +- strokeWidth: Config.dashboard.sizes.mediaProgressThickness +- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap ++ strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness ++ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + + PathAngleArc { + centerX: cover.x + cover.width / 2 + centerY: cover.y + cover.height / 2 +- radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small +- radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small +- startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 +- sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress ++ radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small ++ radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small ++ startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 ++ sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress + } + + Behavior on strokeColor { +@@ -86,7 +87,7 @@ Item { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right +- anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small ++ anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + Tokens.spacing.small + + implicitHeight: width + color: Colours.tPalette.m3surfaceContainerHigh +@@ -106,11 +107,13 @@ Item { + + anchors.fill: parent + +- source: Players.active?.trackArtUrl ?? "" ++ source: Players.getArtUrl(Players.active) + asynchronous: true + fillMode: Image.PreserveAspectCrop +- sourceSize.width: width +- sourceSize.height: height ++ sourceSize: { ++ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; ++ return Qt.size(width * dpr, height * dpr); ++ } + } + } + +@@ -119,15 +122,15 @@ Item { + + anchors.top: cover.bottom + anchors.horizontalCenter: parent.horizontalCenter +- anchors.topMargin: Appearance.spacing.normal ++ anchors.topMargin: Tokens.spacing.normal + + animate: true + horizontalAlignment: Text.AlignHCenter + text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + +- width: parent.implicitWidth - Appearance.padding.large * 2 ++ width: parent.implicitWidth - Tokens.padding.large * 2 + elide: Text.ElideRight + } + +@@ -136,15 +139,15 @@ Item { + + anchors.top: title.bottom + anchors.horizontalCenter: parent.horizontalCenter +- anchors.topMargin: Appearance.spacing.small ++ anchors.topMargin: Tokens.spacing.small + + animate: true + horizontalAlignment: Text.AlignHCenter + text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + +- width: parent.implicitWidth - Appearance.padding.large * 2 ++ width: parent.implicitWidth - Tokens.padding.large * 2 + elide: Text.ElideRight + } + +@@ -153,14 +156,14 @@ Item { + + anchors.top: album.bottom + anchors.horizontalCenter: parent.horizontalCenter +- anchors.topMargin: Appearance.spacing.small ++ anchors.topMargin: Tokens.spacing.small + + animate: true + horizontalAlignment: Text.AlignHCenter + text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") + color: Colours.palette.m3secondary + +- width: parent.implicitWidth - Appearance.padding.large * 2 ++ width: parent.implicitWidth - Tokens.padding.large * 2 + elide: Text.ElideRight + } + +@@ -169,35 +172,26 @@ Item { + + anchors.top: artist.bottom + anchors.horizontalCenter: parent.horizontalCenter +- anchors.topMargin: Appearance.spacing.smaller ++ anchors.topMargin: Tokens.spacing.smaller + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + +- Control { ++ PlayerControl { + icon: "skip_previous" + canUse: Players.active?.canGoPrevious ?? false +- +- function onClicked(): void { +- Players.active?.previous(); +- } ++ onClicked: Players.active?.previous() + } + +- Control { ++ PlayerControl { + icon: Players.active?.isPlaying ? "pause" : "play_arrow" + canUse: Players.active?.canTogglePlaying ?? false +- +- function onClicked(): void { +- Players.active?.togglePlaying(); +- } ++ onClicked: Players.active?.togglePlaying() + } + +- Control { ++ PlayerControl { + icon: "skip_next" + canUse: Players.active?.canGoNext ?? false +- +- function onClicked(): void { +- Players.active?.next(); +- } ++ onClicked: Players.active?.next() + } + } + +@@ -208,35 +202,32 @@ Item { + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.right: parent.right +- anchors.topMargin: Appearance.spacing.small +- anchors.bottomMargin: Appearance.padding.large +- anchors.margins: Appearance.padding.large * 2 ++ anchors.topMargin: Tokens.spacing.small ++ anchors.bottomMargin: Tokens.padding.large ++ anchors.margins: Tokens.padding.large * 2 + + playing: Players.active?.isPlaying ?? false +- speed: Audio.beatTracker.bpm / 300 ++ speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type + source: Paths.absolutePath(Config.paths.mediaGif) + asynchronous: true + fillMode: AnimatedImage.PreserveAspectFit + } + +- component Control: StyledRect { ++ component PlayerControl: StyledRect { + id: control + + required property string icon + required property bool canUse +- function onClicked(): void { +- } + +- implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small ++ signal clicked ++ ++ implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small + implicitHeight: implicitWidth + + StateLayer { + disabled: !control.canUse +- radius: Appearance.rounding.full +- +- function onClicked(): void { +- control.onClicked(); +- } ++ radius: Tokens.rounding.full ++ onClicked: control.clicked() + } + + MaterialIcon { +@@ -248,7 +239,7 @@ Item { + animate: true + text: control.icon + color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml +index 7f44a9d0..2e0f085b 100644 +--- a/modules/dashboard/dash/Resources.qml ++++ b/modules/dashboard/dash/Resources.qml +@@ -1,8 +1,8 @@ ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.components.misc + import qs.services +-import qs.config +-import QtQuick + + Row { + id: root +@@ -10,8 +10,8 @@ Row { + anchors.top: parent.top + anchors.bottom: parent.bottom + +- padding: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ padding: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + Ref { + service: SystemUsage +@@ -44,19 +44,19 @@ Row { + + anchors.top: parent.top + anchors.bottom: parent.bottom +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + implicitWidth: icon.implicitWidth + + StyledRect { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: icon.top +- anchors.bottomMargin: Appearance.spacing.small ++ anchors.bottomMargin: Tokens.spacing.small + +- implicitWidth: Config.dashboard.sizes.resourceProgessThickness ++ implicitWidth: Tokens.sizes.dashboard.resourceProgressThickness + + color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + StyledRect { + anchors.left: parent.left +@@ -65,7 +65,7 @@ Row { + implicitHeight: res.value * parent.height + + color: res.colour +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + } + } + +@@ -80,7 +80,7 @@ Row { + + Behavior on value { + Anim { +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + } + } +diff --git a/modules/dashboard/dash/Weather.qml b/modules/dashboard/dash/SmallWeather.qml +similarity index 76% +rename from modules/dashboard/dash/Weather.qml +rename to modules/dashboard/dash/SmallWeather.qml +index c90ccf0a..998c7125 100644 +--- a/modules/dashboard/dash/Weather.qml ++++ b/modules/dashboard/dash/SmallWeather.qml +@@ -1,8 +1,7 @@ ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import qs.utils +-import QtQuick + + Item { + id: root +@@ -22,7 +21,7 @@ Item { + animate: true + text: Weather.icon + color: Colours.palette.m3secondary +- font.pointSize: Appearance.font.size.extraLarge * 2 ++ font.pointSize: Tokens.font.size.extraLarge * 2 + } + + Column { +@@ -30,9 +29,9 @@ Item { + + anchors.verticalCenter: parent.verticalCenter + anchors.left: icon.right +- anchors.leftMargin: Appearance.spacing.large ++ anchors.leftMargin: Tokens.spacing.large + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter +@@ -40,7 +39,7 @@ Item { + animate: true + text: Weather.temp + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + font.weight: 500 + } + +@@ -51,7 +50,7 @@ Item { + text: Weather.description + + elide: Text.ElideRight +- width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2) ++ width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.large * 2) + } + } + } +diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml +index b66b1f9a..79787de5 100644 +--- a/modules/dashboard/dash/User.qml ++++ b/modules/dashboard/dash/User.qml +@@ -1,28 +1,26 @@ ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.components.effects +-import qs.components.images + import qs.components.filedialog ++import qs.components.images + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick + + Row { + id: root + +- required property PersistentProperties visibilities +- required property PersistentProperties state ++ required property DrawerVisibilities visibilities + required property FileDialog facePicker + +- padding: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ padding: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + StyledClippingRect { + implicitWidth: info.implicitHeight + implicitHeight: info.implicitHeight + +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + + MaterialIcon { +@@ -32,6 +30,7 @@ Row { + fill: 1 + grade: 200 + font.pointSize: Math.floor(info.implicitHeight / 2) || 1 ++ visible: pfp.status !== Image.Ready + } + + CachingImage { +@@ -53,7 +52,7 @@ Row { + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial ++ duration: Tokens.anim.durations.expressiveFastSpatial + } + } + } +@@ -61,18 +60,17 @@ Row { + StyledRect { + anchors.centerIn: parent + +- implicitWidth: selectIcon.implicitHeight + Appearance.padding.small * 2 +- implicitHeight: selectIcon.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: selectIcon.implicitHeight + Tokens.padding.small * 2 ++ implicitHeight: selectIcon.implicitHeight + Tokens.padding.small * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.palette.m3primary + scale: parent.containsMouse ? 1 : 0.5 + opacity: parent.containsMouse ? 1 : 0 + + StateLayer { + color: Colours.palette.m3onPrimary +- +- function onClicked(): void { ++ onClicked: { + root.visibilities.launcher = false; + root.facePicker.open(); + } +@@ -86,19 +84,18 @@ Row { + + text: "frame_person" + color: Colours.palette.m3onPrimary +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + } + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial ++ duration: Tokens.anim.durations.expressiveFastSpatial + } + } + } +@@ -109,7 +106,7 @@ Row { + id: info + + anchors.verticalCenter: parent.verticalCenter +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Item { + id: line +@@ -121,10 +118,10 @@ Row { + id: icon + + anchors.left: parent.left +- anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 ++ anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 + + source: SysInfo.osLogo +- implicitSize: Math.floor(Appearance.font.size.normal * 1.34) ++ implicitSize: Math.floor(Tokens.font.size.normal * 1.34) + colour: Colours.palette.m3primary + } + +@@ -135,9 +132,9 @@ Row { + anchors.left: icon.right + anchors.leftMargin: icon.anchors.leftMargin + text: `: ${SysInfo.osPrettyName || SysInfo.osName}` +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + +- width: Config.dashboard.sizes.infoWidth ++ width: Tokens.sizes.dashboard.infoWidth + elide: Text.ElideRight + } + } +@@ -171,12 +168,12 @@ Row { + id: icon + + anchors.left: parent.left +- anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 ++ anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 + + fill: 1 + text: line.icon + color: line.colour +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledText { +@@ -186,9 +183,9 @@ Row { + anchors.left: icon.right + anchors.leftMargin: icon.anchors.leftMargin + text: `: ${line.text}` +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + +- width: Config.dashboard.sizes.infoWidth ++ width: Tokens.sizes.dashboard.infoWidth + elide: Text.ElideRight + } + } +diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml +deleted file mode 100644 +index 7fa2ca17..00000000 +--- a/modules/drawers/Backgrounds.qml ++++ /dev/null +@@ -1,86 +0,0 @@ +-import qs.services +-import qs.config +-import qs.modules.osd as Osd +-import qs.modules.notifications as Notifications +-import qs.modules.session as Session +-import qs.modules.launcher as Launcher +-import qs.modules.dashboard as Dashboard +-import qs.modules.bar.popouts as BarPopouts +-import qs.modules.utilities as Utilities +-import qs.modules.sidebar as Sidebar +-import QtQuick +-import QtQuick.Shapes +- +-Shape { +- id: root +- +- required property Panels panels +- required property Item bar +- +- anchors.fill: parent +- anchors.margins: Config.border.thickness +- anchors.leftMargin: bar.implicitWidth +- preferredRendererType: Shape.CurveRenderer +- +- Osd.Background { +- wrapper: root.panels.osd +- +- startX: root.width - root.panels.session.width - root.panels.sidebar.width +- startY: (root.height - wrapper.height) / 2 - rounding +- } +- +- Notifications.Background { +- wrapper: root.panels.notifications +- sidebar: sidebar +- +- startX: root.width +- startY: 0 +- } +- +- Session.Background { +- wrapper: root.panels.session +- +- startX: root.width - root.panels.sidebar.width +- startY: (root.height - wrapper.height) / 2 - rounding +- } +- +- Launcher.Background { +- wrapper: root.panels.launcher +- +- startX: (root.width - wrapper.width) / 2 - rounding +- startY: root.height +- } +- +- Dashboard.Background { +- wrapper: root.panels.dashboard +- +- startX: (root.width - wrapper.width) / 2 - rounding +- startY: 0 +- } +- +- BarPopouts.Background { +- wrapper: root.panels.popouts +- invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height +- +- startX: wrapper.x +- startY: wrapper.y - rounding * sideRounding +- } +- +- Utilities.Background { +- wrapper: root.panels.utilities +- sidebar: sidebar +- +- startX: root.width +- startY: root.height +- } +- +- Sidebar.Background { +- id: sidebar +- +- wrapper: root.panels.sidebar +- panels: root.panels +- +- startX: root.width +- startY: root.panels.notifications.height +- } +-} +diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml +deleted file mode 100644 +index 6fdd73bd..00000000 +--- a/modules/drawers/Border.qml ++++ /dev/null +@@ -1,44 +0,0 @@ +-pragma ComponentBehavior: Bound +- +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Effects +- +-Item { +- id: root +- +- required property Item bar +- +- anchors.fill: parent +- +- StyledRect { +- anchors.fill: parent +- color: Colours.palette.m3surface +- +- layer.enabled: true +- layer.effect: MultiEffect { +- maskSource: mask +- maskEnabled: true +- maskInverted: true +- maskThresholdMin: 0.5 +- maskSpreadAtMin: 1 +- } +- } +- +- Item { +- id: mask +- +- anchors.fill: parent +- layer.enabled: true +- visible: false +- +- Rectangle { +- anchors.fill: parent +- anchors.margins: Config.border.thickness +- anchors.leftMargin: root.bar.implicitWidth +- radius: Config.border.rounding +- } +- } +-} +diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml +new file mode 100644 +index 00000000..677c84a0 +--- /dev/null ++++ b/modules/drawers/ContentWindow.qml +@@ -0,0 +1,318 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import QtQuick.Controls ++import QtQuick.Effects ++import Quickshell ++import Quickshell.Hyprland ++import Quickshell.Wayland ++import Caelestia.Blobs ++import Caelestia.Config ++import qs.components ++import qs.components.containers ++import qs.services ++import qs.modules.bar ++ ++StyledWindow { ++ id: root ++ ++ readonly property alias bar: bar ++ readonly property alias interactionWrapper: interactions ++ ++ readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) ++ readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0 ++ readonly property bool hasFullscreen: { ++ if (hasSpecialWorkspace) { ++ const specialName = monitor?.lastIpcObject.specialWorkspace?.name; ++ if (!specialName) ++ return false; ++ const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); ++ return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; ++ } ++ return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; ++ } ++ ++ property real fsTransitionProg: hasFullscreen ? 1 : 0 ++ readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows ++ readonly property real borderThickness: contentItem.Config.border.thickness * (1 - fsTransitionProg) ++ readonly property real borderRounding: contentItem.Config.border.rounding * (1 - fsTransitionProg) ++ readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) ++ readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness ++ ++ readonly property int dragMaskPadding: { ++ if (focusGrab.active || panels.popouts.isDetached) ++ return 0; ++ ++ if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0) ++ return 0; ++ ++ const thresholds = []; ++ for (const panel of ["dashboard", "launcher", "session", "sidebar"]) ++ if (contentItem.Config[panel].enabled) ++ thresholds.push(contentItem.Config[panel].dragThreshold); ++ return Math.max(...thresholds); ++ } ++ ++ onHasFullscreenChanged: { ++ visibilities.launcher = false; ++ visibilities.session = false; ++ visibilities.dashboard = false; ++ panels.popouts.close(); ++ } ++ ++ name: "drawers" ++ WlrLayershell.exclusionMode: ExclusionMode.Ignore ++ WlrLayershell.layer: fsTransitionProg > 0 && contentItem.Config.general.showOverFullscreen ? WlrLayer.Overlay : WlrLayer.Top ++ WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None ++ ++ mask: hasFullscreen ? emptyRegion : regions ++ ++ anchors.top: true ++ anchors.bottom: true ++ anchors.left: true ++ anchors.right: true ++ ++ Behavior on fsTransitionProg { ++ Anim {} ++ } ++ ++ Region { ++ id: emptyRegion ++ ++ x: panels.notifications.x + bar.implicitWidth ++ y: panels.notifications.y + root.borderThickness ++ width: panels.notifications.width ++ height: panels.notifications.height ++ ++ Region { ++ x: root.width - width ++ y: panels.osdWrapper.y + root.borderThickness ++ width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness ++ height: panels.osd.height ++ } ++ } ++ ++ Regions { ++ id: regions ++ ++ bar: bar ++ panels: panels ++ win: root ++ } ++ ++ HyprlandFocusGrab { ++ id: focusGrab ++ ++ active: (visibilities.launcher && root.contentItem.Config.launcher.enabled) || (visibilities.session && root.contentItem.Config.session.enabled) || (visibilities.sidebar && root.contentItem.Config.sidebar.enabled) || (!root.contentItem.Config.dashboard.showOnHover && visibilities.dashboard && root.contentItem.Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) ++ windows: [root] ++ onCleared: { ++ visibilities.launcher = false; ++ visibilities.session = false; ++ visibilities.sidebar = false; ++ visibilities.dashboard = false; ++ panels.popouts.hasCurrent = false; ++ bar.closeTray(); ++ } ++ } ++ ++ StyledRect { ++ anchors.fill: parent ++ opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 ++ color: Colours.palette.m3scrim ++ ++ Behavior on opacity { ++ Anim {} ++ } ++ } ++ ++ Item { ++ anchors.fill: parent ++ opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 ++ layer.enabled: true ++ layer.effect: MultiEffect { ++ shadowEnabled: true ++ blurMax: 15 ++ shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity)) ++ } ++ ++ BlobGroup { ++ id: blobGroup ++ ++ color: Colours.palette.m3surface ++ smoothing: root.contentItem.Config.border.smoothing ++ ++ Behavior on color { ++ CAnim {} ++ } ++ } ++ ++ BlobInvertedRect { ++ anchors.fill: parent ++ anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers ++ group: blobGroup ++ radius: root.borderRounding ++ borderLeft: bar.implicitWidth - anchors.margins - root.sdfBorderOffset ++ borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset ++ borderTop: root.borderThickness - anchors.margins - root.sdfBorderOffset ++ borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset ++ } ++ ++ PanelBg { ++ id: dashBg ++ ++ panel: panels.dashboard ++ deformAmount: 0.1 ++ } ++ ++ PanelBg { ++ id: launcherBg ++ ++ panel: panels.launcher ++ deformAmount: 0.1 ++ } ++ ++ PanelBg { ++ id: sessionBg ++ ++ panel: panels.sessionWrapper ++ deformAmount: 0.2 ++ x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth ++ implicitWidth: panels.session.width ++ } ++ ++ PanelBg { ++ id: sidebarBg ++ ++ panel: panels.sidebar ++ deformAmount: 0.03 ++ implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 ++ exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] ++ bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius ++ } ++ ++ PanelBg { ++ id: osdBg ++ ++ panel: panels.osdWrapper ++ deformAmount: 0.25 ++ x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth ++ implicitWidth: panels.osd.width ++ } ++ ++ PanelBg { ++ id: notifsBg ++ ++ panel: panels.notifications ++ } ++ ++ PanelBg { ++ id: utilsBg ++ ++ panel: panels.utilities ++ deformAmount: panels.sidebar.visible ? 0.1 : 0.15 ++ exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] ++ topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius ++ } ++ ++ PanelBg { ++ id: popoutBg ++ ++ // Extra width to prevent vertical movement deformation partially detaching panel from bar ++ property real extraWidth: panels.popouts.isDetached ? 0 : 0.2 ++ ++ panel: panels.popoutsWrapper ++ deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 ++ x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth ++ implicitWidth: panels.popouts.width * (1 + extraWidth) ++ ++ Behavior on extraWidth { ++ Anim { ++ type: Anim.DefaultSpatial ++ } ++ } ++ } ++ } ++ ++ DrawerVisibilities { ++ id: visibilities ++ ++ Component.onCompleted: Visibilities.load(root.screen, this) ++ } ++ ++ Interactions { ++ id: interactions ++ ++ screen: root.screen ++ popouts: panels.popouts ++ visibilities: visibilities ++ panels: panels ++ bar: bar ++ borderThickness: root.borderLayoutThickness ++ fullscreen: root.hasFullscreen ++ ++ Panels { ++ id: panels ++ ++ screen: root.screen ++ visibilities: visibilities ++ bar: bar ++ borderThickness: root.borderThickness ++ ++ utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 ++ utilities.deformMatrix: utilsBg.rawDeformMatrix ++ ++ dashboard.transform: Matrix4x4 { ++ matrix: dashBg.deformMatrix ++ } ++ launcher.transform: Matrix4x4 { ++ matrix: launcherBg.deformMatrix ++ } ++ session.transform: Matrix4x4 { ++ matrix: sessionBg.deformMatrix ++ } ++ sidebar.transform: Matrix4x4 { ++ matrix: sidebarBg.deformMatrix ++ } ++ osd.transform: Matrix4x4 { ++ matrix: osdBg.deformMatrix ++ } ++ notifications.transform: Matrix4x4 { ++ matrix: notifsBg.deformMatrix ++ } ++ utilities.transform: Matrix4x4 { ++ matrix: utilsBg.deformMatrix ++ } ++ popouts.transform: Matrix4x4 { ++ matrix: popoutBg.deformMatrix ++ } ++ } ++ ++ BarWrapper { ++ id: bar ++ ++ anchors.top: parent.top ++ anchors.bottom: parent.bottom ++ ++ screen: root.screen ++ visibilities: visibilities ++ popouts: panels.popouts ++ ++ fullscreen: root.hasFullscreen ++ ++ Component.onCompleted: Visibilities.bars.set(root.screen, this) ++ } ++ } ++ ++ component PanelBg: BlobRect { ++ required property Item panel ++ property real deformAmount: 0.15 ++ ++ group: blobGroup ++ x: panel.x + bar.implicitWidth ++ y: panel.y + root.borderThickness ++ implicitWidth: panel.width ++ implicitHeight: panel.height ++ radius: Tokens.rounding.large ++ deformScale: (deformAmount * Config.appearance.deformScale) / 10000 ++ } ++} +diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml +index 00f9596a..c642692c 100644 +--- a/modules/drawers/Drawers.qml ++++ b/modules/drawers/Drawers.qml +@@ -1,193 +1,24 @@ +-pragma ComponentBehavior: Bound +- +-import qs.components +-import qs.components.containers +-import qs.services +-import qs.config +-import qs.modules.bar + import Quickshell +-import Quickshell.Wayland +-import Quickshell.Hyprland +-import QtQuick +-import QtQuick.Effects ++import qs.services + + Variants { +- model: Quickshell.screens ++ model: Screens.screens + + Scope { + id: scope + + required property ShellScreen modelData +- readonly property bool barDisabled: { +- const regexChecker = /^\^.*\$$/; +- for (const filter of Config.bar.excludedScreens) { +- // If filter is a regex +- if (regexChecker.test(filter)) { +- if ((new RegExp(filter)).test(modelData.name)) +- return true; +- } else { +- if (filter === modelData.name) +- return true; +- } +- } +- return false; +- } + + Exclusions { + screen: scope.modelData +- bar: bar ++ bar: drawerWindow.bar + } + +- StyledWindow { +- id: win +- +- readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false +- readonly property int dragMaskPadding: { +- if (focusGrab.active || panels.popouts.isDetached) +- return 0; +- +- const mon = Hypr.monitorFor(screen); +- if (mon?.lastIpcObject?.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject?.windows > 0) +- return 0; +- +- const thresholds = []; +- for (const panel of ["dashboard", "launcher", "session", "sidebar"]) +- if (Config[panel].enabled) +- thresholds.push(Config[panel].dragThreshold); +- return Math.max(...thresholds); +- } +- +- onHasFullscreenChanged: { +- visibilities.launcher = false; +- visibilities.session = false; +- visibilities.dashboard = false; +- } ++ ContentWindow { ++ id: drawerWindow + + screen: scope.modelData + name: "drawers" +- WlrLayershell.exclusionMode: ExclusionMode.Ignore +- WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None +- +- mask: Region { +- x: bar.implicitWidth + win.dragMaskPadding +- y: Config.border.thickness + win.dragMaskPadding +- width: win.width - bar.implicitWidth - Config.border.thickness - win.dragMaskPadding * 2 +- height: win.height - Config.border.thickness * 2 - win.dragMaskPadding * 2 +- intersection: Intersection.Xor +- +- regions: regions.instances +- } +- +- anchors.top: true +- anchors.bottom: true +- anchors.left: true +- anchors.right: true +- +- Variants { +- id: regions +- +- model: panels.children +- +- Region { +- required property Item modelData +- +- x: modelData.x + bar.implicitWidth +- y: modelData.y + Config.border.thickness +- width: modelData.width +- height: modelData.height +- intersection: Intersection.Subtract +- } +- } +- +- HyprlandFocusGrab { +- id: focusGrab +- +- active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && panels.popouts.current?.depth > 1) +- windows: [win] +- onCleared: { +- visibilities.launcher = false; +- visibilities.session = false; +- visibilities.sidebar = false; +- visibilities.dashboard = false; +- panels.popouts.hasCurrent = false; +- bar.closeTray(); +- } +- } +- +- StyledRect { +- anchors.fill: parent +- opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 +- color: Colours.palette.m3scrim +- +- Behavior on opacity { +- Anim {} +- } +- } +- +- Item { +- anchors.fill: parent +- opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 +- layer.enabled: true +- layer.effect: MultiEffect { +- shadowEnabled: true +- blurMax: 15 +- shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) +- } +- +- Border { +- bar: bar +- } +- +- Backgrounds { +- panels: panels +- bar: bar +- } +- } +- +- PersistentProperties { +- id: visibilities +- +- property bool bar +- property bool osd +- property bool session +- property bool launcher +- property bool dashboard +- property bool utilities +- property bool sidebar +- +- Component.onCompleted: Visibilities.load(scope.modelData, this) +- } +- +- Interactions { +- screen: scope.modelData +- popouts: panels.popouts +- visibilities: visibilities +- panels: panels +- bar: bar +- +- Panels { +- id: panels +- +- screen: scope.modelData +- visibilities: visibilities +- bar: bar +- } +- +- BarWrapper { +- id: bar +- +- anchors.top: parent.top +- anchors.bottom: parent.bottom +- +- screen: scope.modelData +- visibilities: visibilities +- popouts: panels.popouts +- +- disabled: scope.barDisabled +- +- Component.onCompleted: Visibilities.bars.set(scope.modelData, this) +- } +- } + } + } + } +diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml +index e4015c89..fe72730c 100644 +--- a/modules/drawers/Exclusions.qml ++++ b/modules/drawers/Exclusions.qml +@@ -1,15 +1,16 @@ + pragma ComponentBehavior: Bound + +-import qs.components.containers +-import qs.config +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia.Config ++import qs.components.containers ++import qs.modules.bar as Bar + + Scope { + id: root + + required property ShellScreen screen +- required property Item bar ++ required property Bar.BarWrapper bar + + ExclusionZone { + anchors.left: true +@@ -31,7 +32,7 @@ Scope { + component ExclusionZone: StyledWindow { + screen: root.screen + name: "border-exclusion" +- exclusiveZone: Config.border.thickness ++ exclusiveZone: contentItem.Config.border.thickness + mask: Region {} + implicitWidth: 1 + implicitHeight: 1 +diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml +index 9579b15a..b89cb007 100644 +--- a/modules/drawers/Interactions.qml ++++ b/modules/drawers/Interactions.qml +@@ -1,17 +1,22 @@ ++import QtQuick ++import QtQuick.Controls ++import Quickshell ++import Caelestia.Config ++import qs.components + import qs.components.controls +-import qs.config ++import qs.modules.bar as Bar + import qs.modules.bar.popouts as BarPopouts +-import Quickshell +-import QtQuick + + CustomMouseArea { + id: root + + required property ShellScreen screen + required property BarPopouts.Wrapper popouts +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + required property Panels panels +- required property Item bar ++ required property Bar.BarWrapper bar ++ required property real borderThickness ++ required property bool fullscreen + + property point dragStart + property bool dashboardShortcutActive +@@ -19,7 +24,7 @@ CustomMouseArea { + property bool utilitiesShortcutActive + + function withinPanelHeight(panel: Item, x: real, y: real): bool { +- const panelY = Config.border.thickness + panel.y; ++ const panelY = root.borderThickness + panel.y; + return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; + } + +@@ -33,24 +38,29 @@ CustomMouseArea { + } + + function inRightPanel(panel: Item, x: real, y: real): bool { +- return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y); ++ return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); + } + + function inTopPanel(panel: Item, x: real, y: real): bool { +- return y < Config.border.thickness + panel.y + panel.height && withinPanelWidth(panel, x, y); ++ const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property ++ return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y); + } + +- function inBottomPanel(panel: Item, x: real, y: real): bool { +- return y > root.height - Config.border.thickness - panel.height - Config.border.rounding && withinPanelWidth(panel, x, y); ++ function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { ++ const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property ++ return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); + } + + function onWheel(event: WheelEvent): void { ++ if (fullscreen) ++ return; + if (event.x < bar.implicitWidth) { + bar.handleWheel(event.y, event.angleDelta); + } + } + + anchors.fill: parent ++ acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons + hoverEnabled: true + + onPressed: event => dragStart = Qt.point(event.x, event.y) +@@ -68,7 +78,7 @@ CustomMouseArea { + if (!utilitiesShortcutActive) + visibilities.utilities = false; + +- if (!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) { ++ if (!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) { + popouts.hasCurrent = false; + bar.closeTray(); + } +@@ -87,21 +97,26 @@ CustomMouseArea { + const dragX = x - dragStart.x; + const dragY = y - dragStart.y; + ++ if (fullscreen) { ++ root.panels.osd.hovered = inRightPanel(panels.osdWrapper, x, y); ++ return; ++ } ++ + // Show bar in non-exclusive mode on hover +- if (!visibilities.bar && Config.bar.showOnHover && x < bar.implicitWidth) ++ if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) + bar.isHovered = true; + + // Show/hide bar on drag +- if (pressed && dragStart.x < bar.implicitWidth) { ++ if (pressed && dragStart.x < bar.clampedWidth) { + if (dragX > Config.bar.dragThreshold) + visibilities.bar = true; + else if (dragX < -Config.bar.dragThreshold) + visibilities.bar = false; + } + +- if (panels.sidebar.width === 0) { ++ if (panels.sidebar.offsetScale === 1) { + // Show osd on hover +- const showOsd = inRightPanel(panels.osd, x, y); ++ const showOsd = inRightPanel(panels.osdWrapper, x, y); + + // Always update visibility based on hover if not in shortcut mode + if (!osdShortcutActive) { +@@ -113,26 +128,26 @@ CustomMouseArea { + root.panels.osd.hovered = true; + } + +- const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; ++ const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); + + // Show/hide session on drag +- if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { ++ if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { + if (dragX < -Config.session.dragThreshold) + visibilities.session = true; + else if (dragX > Config.session.dragThreshold) + visibilities.session = false; + + // Show sidebar on drag if in session area and session is nearly fully visible +- if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) ++ if (showSidebar && panels.session.offsetScale <= 0 && dragX < -Config.sidebar.dragThreshold) + visibilities.sidebar = true; + } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { + // Show sidebar on drag if not in session area + visibilities.sidebar = true; + } + } else { +- const outOfSidebar = x < width - panels.sidebar.width; ++ const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale); + // Show osd on hover +- const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); ++ const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y); + + // Always update visibility based on hover if not in shortcut mode + if (!osdShortcutActive) { +@@ -145,7 +160,7 @@ CustomMouseArea { + } + + // Show/hide session on drag +- if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { ++ if (pressed && outOfSidebar && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { + if (dragX < -Config.session.dragThreshold) + visibilities.session = true; + else if (dragX > Config.session.dragThreshold) +@@ -188,7 +203,7 @@ CustomMouseArea { + } + + // Show utilities on hover +- const showUtilities = inBottomPanel(panels.utilities, x, y); ++ const showUtilities = inBottomPanel(panels.utilities, x, y, true); + + // Always update visibility based on hover if not in shortcut mode + if (!utilitiesShortcutActive) { +@@ -201,7 +216,7 @@ CustomMouseArea { + // Show popouts on hover + if (x < bar.implicitWidth) { + bar.checkPopout(y); +- } else if ((!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { ++ } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) { + popouts.hasCurrent = false; + bar.closeTray(); + } +@@ -209,8 +224,6 @@ CustomMouseArea { + + // Monitor individual visibility changes + Connections { +- target: root.visibilities +- + function onLauncherChanged() { + // If launcher is hidden, clear shortcut flags for dashboard and OSD + if (!root.visibilities.launcher) { +@@ -220,7 +233,7 @@ CustomMouseArea { + + // Also hide dashboard and OSD if they're not being hovered + const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); +- const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); ++ const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); + + if (!inDashboardArea) { + root.visibilities.dashboard = false; +@@ -248,7 +261,7 @@ CustomMouseArea { + function onOsdChanged() { + if (root.visibilities.osd) { + // OSD became visible, immediately check if this should be shortcut mode +- const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); ++ const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); + if (!inOsdArea) { + root.osdShortcutActive = true; + } +@@ -270,5 +283,7 @@ CustomMouseArea { + root.utilitiesShortcutActive = false; + } + } ++ ++ target: root.visibilities + } + } +diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml +index 7705732a..4ca420e3 100644 +--- a/modules/drawers/Panels.qml ++++ b/modules/drawers/Panels.qml +@@ -1,69 +1,106 @@ +-import qs.config +-import qs.modules.osd as Osd ++import QtQuick ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.modules.bar as Bar ++import qs.modules.dashboard as Dashboard ++import qs.modules.launcher as Launcher + import qs.modules.notifications as Notifications ++import qs.modules.osd as Osd + import qs.modules.session as Session +-import qs.modules.launcher as Launcher +-import qs.modules.dashboard as Dashboard +-import qs.modules.bar.popouts as BarPopouts ++import qs.modules.sidebar as Sidebar + import qs.modules.utilities as Utilities ++import qs.modules.bar.popouts as BarPopouts + import qs.modules.utilities.toasts as Toasts +-import qs.modules.sidebar as Sidebar +-import Quickshell +-import QtQuick + + Item { + id: root + + required property ShellScreen screen +- required property PersistentProperties visibilities +- required property Item bar ++ required property DrawerVisibilities visibilities ++ required property Bar.BarWrapper bar ++ required property real borderThickness + + readonly property alias osd: osd ++ readonly property alias osdWrapper: osdWrapper + readonly property alias notifications: notifications + readonly property alias session: session ++ readonly property alias sessionWrapper: sessionWrapper + readonly property alias launcher: launcher + readonly property alias dashboard: dashboard +- readonly property alias popouts: popouts ++ readonly property alias popouts: popoutsWrapper.content ++ readonly property alias popoutsWrapper: popoutsWrapper + readonly property alias utilities: utilities + readonly property alias toasts: toasts + readonly property alias sidebar: sidebar + + anchors.fill: parent +- anchors.margins: Config.border.thickness ++ anchors.margins: borderThickness + anchors.leftMargin: bar.implicitWidth + +- Osd.Wrapper { +- id: osd ++ Behavior on anchors.margins { ++ Anim {} ++ } + +- clip: session.width > 0 || sidebar.width > 0 +- screen: root.screen +- visibilities: root.visibilities ++ Behavior on anchors.leftMargin { ++ Anim {} ++ } ++ ++ Item { ++ id: osdWrapper + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right +- anchors.rightMargin: session.width + sidebar.width ++ anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale) ++ clip: sidebar.visible || session.visible ++ ++ implicitWidth: osd.implicitWidth * (1 - osd.offsetScale) ++ implicitHeight: osd.implicitHeight ++ ++ Osd.Wrapper { ++ id: osd ++ ++ screen: root.screen ++ visibilities: root.visibilities ++ sidebarOrSessionVisible: sidebar.visible || session.visible ++ ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.right: parent.right ++ } + } + + Notifications.Wrapper { + id: notifications + + visibilities: root.visibilities +- panels: root ++ sidebarPanel: sidebar ++ osdPanel: osdWrapper ++ sessionPanel: sessionWrapper + + anchors.top: parent.top + anchors.right: parent.right + } + +- Session.Wrapper { +- id: session +- +- clip: sidebar.width > 0 +- visibilities: root.visibilities +- panels: root ++ Item { ++ id: sessionWrapper + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right +- anchors.rightMargin: sidebar.width ++ anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale) ++ clip: sidebar.visible ++ ++ implicitWidth: session.implicitWidth * (1 - session.offsetScale) ++ implicitHeight: session.implicitHeight ++ ++ Session.Wrapper { ++ id: session ++ ++ visibilities: root.visibilities ++ sidebarVisible: sidebar.visible ++ ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.right: parent.right ++ } + } + + Launcher.Wrapper { +@@ -86,22 +123,11 @@ Item { + anchors.top: parent.top + } + +- BarPopouts.Wrapper { +- id: popouts ++ BarPopouts.ClipWrapper { ++ id: popoutsWrapper + + screen: root.screen +- +- x: isDetached ? (root.width - nonAnimWidth) / 2 : 0 +- y: { +- if (isDetached) +- return (root.height - nonAnimHeight) / 2; +- +- const off = currentCenter - Config.border.thickness - nonAnimHeight / 2; +- const diff = root.height - Math.floor(off + nonAnimHeight); +- if (diff < 0) +- return off + diff; +- return Math.max(off, 0); +- } ++ borderThickness: root.borderThickness + } + + Utilities.Wrapper { +@@ -109,7 +135,7 @@ Item { + + visibilities: root.visibilities + sidebar: sidebar +- popouts: popouts ++ popouts: popoutsWrapper.content + + anchors.bottom: parent.bottom + anchors.right: parent.right +@@ -120,14 +146,13 @@ Item { + + anchors.bottom: sidebar.visible ? parent.bottom : utilities.top + anchors.right: sidebar.left +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + } + + Sidebar.Wrapper { + id: sidebar + + visibilities: root.visibilities +- panels: root + + anchors.top: notifications.bottom + anchors.bottom: utilities.top +diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml +new file mode 100644 +index 00000000..47ad0680 +--- /dev/null ++++ b/modules/drawers/Regions.qml +@@ -0,0 +1,84 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import Quickshell ++import Caelestia.Config ++import qs.modules.bar as Bar ++ ++Region { ++ id: root ++ ++ required property Bar.BarWrapper bar ++ required property Panels panels ++ required property var win ++ ++ readonly property real borderThickness: win.contentItem.Config.border.thickness ++ readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness ++ ++ x: bar.clampedWidth + win.dragMaskPadding ++ y: clampedThickness + win.dragMaskPadding ++ width: win.width - bar.clampedWidth - clampedThickness - win.dragMaskPadding * 2 ++ height: win.height - clampedThickness * 2 - win.dragMaskPadding * 2 ++ intersection: Intersection.Xor ++ ++ R { ++ panel: root.panels.dashboard ++ y: 0 ++ height: panel.height * (1 - root.panels.dashboard.offsetScale) + root.borderThickness ++ } ++ ++ R { ++ panel: root.panels.launcher ++ y: root.win.height - height ++ height: panel.height * (1 - root.panels.launcher.offsetScale) + root.borderThickness ++ } ++ ++ R { ++ id: sessionRegion ++ ++ panel: root.panels.sessionWrapper ++ x: root.win.width - width ++ width: panel.width * (1 - root.panels.session.offsetScale) + root.borderThickness + sidebarRegion.width ++ } ++ ++ R { ++ id: sidebarRegion ++ ++ panel: root.panels.sidebar ++ x: root.win.width - width ++ width: panel.width * (1 - root.panels.sidebar.offsetScale) + root.borderThickness ++ } ++ ++ R { ++ panel: root.panels.osdWrapper ++ x: root.win.width - width ++ width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness + sessionRegion.width ++ } ++ ++ R { ++ panel: root.panels.notifications ++ y: 0 ++ height: panel.height + root.borderThickness ++ } ++ ++ R { ++ panel: root.panels.utilities ++ y: root.win.height - height ++ height: panel.height * (1 - root.panels.utilities.offsetScale) + root.borderThickness ++ } ++ ++ R { ++ panel: root.panels.popoutsWrapper ++ width: panel.width * (1 - root.panels.popoutsWrapper.offsetScale) ++ } ++ ++ component R: Region { ++ required property Item panel ++ ++ x: panel.x + root.bar.implicitWidth ++ y: panel.y + root.borderThickness ++ width: panel.width ++ height: panel.height ++ intersection: Intersection.Subtract ++ } ++} +diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml +index 7f7b843a..a2109c04 100644 +--- a/modules/launcher/AppList.qml ++++ b/modules/launcher/AppList.qml +@@ -1,20 +1,20 @@ + pragma ComponentBehavior: Bound + +-import "items" +-import "services" ++import QtQuick ++import Quickshell ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import QtQuick ++import qs.modules.launcher.items ++import qs.modules.launcher.services + + StyledListView { + id: root + + required property StyledTextField search +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + + model: ScriptModel { + id: model +@@ -22,9 +22,9 @@ StyledListView { + onValuesChanged: root.currentIndex = 0 + } + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + orientation: Qt.Vertical +- implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing ++ implicitHeight: (Tokens.sizes.launcher.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing + + preferredHighlightBegin: 0 + preferredHighlightEnd: height +@@ -32,7 +32,7 @@ StyledListView { + + highlightFollowsCurrentItem: false + highlight: StyledRect { +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.palette.m3onSurface + opacity: 0.08 + +@@ -42,15 +42,14 @@ StyledListView { + + Behavior on y { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } + + state: { + const text = search.text; +- const prefix = Config.launcher.actionPrefix; ++ const prefix = GlobalConfig.launcher.actionPrefix; + if (text.startsWith(prefix)) { + for (const action of ["calc", "scheme", "variant"]) + if (text.startsWith(`${prefix}${action} `)) +@@ -118,16 +117,16 @@ StyledListView { + property: "opacity" + from: 1 + to: 0 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ duration: Tokens.anim.durations.small ++ easing: Tokens.anim.standardAccel + } + Anim { + target: root + property: "scale" + from: 1 + to: 0.9 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ duration: Tokens.anim.durations.small ++ easing: Tokens.anim.standardAccel + } + } + PropertyAction { +@@ -140,16 +139,16 @@ StyledListView { + property: "opacity" + from: 0 + to: 1 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ duration: Tokens.anim.durations.small ++ easing: Tokens.anim.standardDecel + } + Anim { + target: root + property: "scale" + from: 0.9 + to: 1 +- duration: Appearance.anim.durations.small +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ duration: Tokens.anim.durations.small ++ easing: Tokens.anim.standardDecel + } + } + PropertyAction { +@@ -197,7 +196,7 @@ StyledListView { + addDisplaced: Transition { + Anim { + property: "y" +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + Anim { + properties: "opacity,scale" +diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml +deleted file mode 100644 +index 709c7d03..00000000 +--- a/modules/launcher/Background.qml ++++ /dev/null +@@ -1,60 +0,0 @@ +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Shapes +- +-ShapePath { +- id: root +- +- required property Wrapper wrapper +- readonly property real rounding: Config.border.rounding +- readonly property bool flatten: wrapper.height < rounding * 2 +- readonly property real roundingY: flatten ? wrapper.height / 2 : rounding +- +- strokeWidth: -1 +- fillColor: Colours.palette.m3surface +- +- PathArc { +- relativeX: root.rounding +- relativeY: -root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- direction: PathArc.Counterclockwise +- } +- PathLine { +- relativeX: 0 +- relativeY: -(root.wrapper.height - root.roundingY * 2) +- } +- PathArc { +- relativeX: root.rounding +- relativeY: -root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- } +- PathLine { +- relativeX: root.wrapper.width - root.rounding * 2 +- relativeY: 0 +- } +- PathArc { +- relativeX: root.rounding +- relativeY: root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- } +- PathLine { +- relativeX: 0 +- relativeY: root.wrapper.height - root.roundingY * 2 +- } +- PathArc { +- relativeX: root.rounding +- relativeY: root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- direction: PathArc.Counterclockwise +- } +- +- Behavior on fillColor { +- CAnim {} +- } +-} +diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml +index c0859769..69be0c3a 100644 +--- a/modules/launcher/Content.qml ++++ b/modules/launcher/Content.qml +@@ -1,22 +1,21 @@ + pragma ComponentBehavior: Bound + +-import "services" ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import QtQuick ++import qs.modules.launcher.services + + Item { + id: root + +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + required property var panels + required property real maxHeight + +- readonly property int padding: Appearance.padding.large +- readonly property int rounding: Appearance.rounding.large ++ readonly property int padding: Tokens.padding.large ++ readonly property int rounding: Tokens.rounding.large + + implicitWidth: listWrapper.width + padding * 2 + implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 +@@ -48,7 +47,7 @@ Item { + id: searchWrapper + + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + anchors.left: parent.left + anchors.right: parent.right +@@ -73,13 +72,13 @@ Item { + + anchors.left: searchIcon.right + anchors.right: clearIcon.left +- anchors.leftMargin: Appearance.spacing.small +- anchors.rightMargin: Appearance.spacing.small ++ anchors.leftMargin: Tokens.spacing.small ++ anchors.rightMargin: Tokens.spacing.small + +- topPadding: Appearance.padding.larger +- bottomPadding: Appearance.padding.larger ++ topPadding: Tokens.padding.larger ++ bottomPadding: Tokens.padding.larger + +- placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) ++ placeholderText: qsTr("Type \"%1\" for commands").arg(GlobalConfig.launcher.actionPrefix) + + onAccepted: { + const currentItem = list.currentList?.currentItem; +@@ -89,8 +88,8 @@ Item { + Wallpapers.previewColourLock = true; + Wallpapers.setWallpaper(currentItem.modelData.path); + root.visibilities.launcher = false; +- } else if (text.startsWith(Config.launcher.actionPrefix)) { +- if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) ++ } else if (text.startsWith(GlobalConfig.launcher.actionPrefix)) { ++ if (text.startsWith(`${GlobalConfig.launcher.actionPrefix}calc `)) + currentItem.onClicked(); + else + currentItem.modelData.onClicked(list.currentList); +@@ -107,14 +106,14 @@ Item { + Keys.onEscapePressed: root.visibilities.launcher = false + + Keys.onPressed: event => { +- if (!Config.launcher.vimKeybinds) ++ if (!GlobalConfig.launcher.vimKeybinds) + return; + + if (event.modifiers & Qt.ControlModifier) { +- if (event.key === Qt.Key_J) { ++ if (event.key === Qt.Key_J || event.key === Qt.Key_N) { + list.currentList?.incrementCurrentIndex(); + event.accepted = true; +- } else if (event.key === Qt.Key_K) { ++ } else if (event.key === Qt.Key_K || event.key === Qt.Key_P) { + list.currentList?.decrementCurrentIndex(); + event.accepted = true; + } +@@ -130,8 +129,6 @@ Item { + Component.onCompleted: forceActiveFocus() + + Connections { +- target: root.visibilities +- + function onLauncherChanged(): void { + if (!root.visibilities.launcher) + search.text = ""; +@@ -141,6 +138,8 @@ Item { + if (!root.visibilities.session) + search.forceActiveFocus(); + } ++ ++ target: root.visibilities + } + } + +@@ -177,13 +176,13 @@ Item { + + Behavior on width { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml +index b2a9c770..db7abdf9 100644 +--- a/modules/launcher/ContentList.qml ++++ b/modules/launcher/ContentList.qml +@@ -1,26 +1,25 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick + + Item { + id: root + + required property var content +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + required property var panels + required property real maxHeight + required property StyledTextField search + required property int padding + required property int rounding + +- readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) +- readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item ++ readonly property bool showWallpapers: search.text.startsWith(`${GlobalConfig.launcher.actionPrefix}wallpaper `) ++ readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom +@@ -33,7 +32,7 @@ Item { + name: "apps" + + PropertyChanges { +- root.implicitWidth: Config.launcher.sizes.itemWidth ++ root.implicitWidth: root.Tokens.sizes.launcher.itemWidth + root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) + appList.active: true + } +@@ -47,8 +46,8 @@ Item { + name: "wallpapers" + + PropertyChanges { +- root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) +- root.implicitHeight: Config.launcher.sizes.wallpaperHeight ++ root.implicitWidth: Math.max(root.Tokens.sizes.launcher.itemWidth * 1.2, wallpaperList.implicitWidth) ++ root.implicitHeight: root.Tokens.sizes.launcher.wallpaperHeight + wallpaperList.active: true + } + } +@@ -61,7 +60,7 @@ Item { + property: "opacity" + from: 1 + to: 0 +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + PropertyAction {} + Anim { +@@ -69,7 +68,7 @@ Item { + property: "opacity" + from: 0 + to: 1 +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + } +@@ -90,6 +89,7 @@ Item { + Loader { + id: wallpaperList + ++ asynchronous: true + active: false + + anchors.top: parent.top +@@ -110,8 +110,8 @@ Item { + opacity: root.currentList?.count === 0 ? 1 : 0 + scale: root.currentList?.count === 0 ? 1 : 0.5 + +- spacing: Appearance.spacing.normal +- padding: Appearance.padding.large ++ spacing: Tokens.spacing.normal ++ padding: Tokens.padding.large + + anchors.horizontalCenter: parent.horizontalCenter + anchors.verticalCenter: parent.verticalCenter +@@ -119,7 +119,7 @@ Item { + MaterialIcon { + text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + + anchors.verticalCenter: parent.verticalCenter + } +@@ -130,14 +130,14 @@ Item { + StyledText { + text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + font.weight: 500 + } + + StyledText { + text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + } + +@@ -154,8 +154,8 @@ Item { + enabled: root.visibilities.launcher + + Anim { +- duration: Appearance.anim.durations.large +- easing.bezierCurve: Appearance.anim.curves.emphasizedDecel ++ duration: Tokens.anim.durations.large ++ easing: Tokens.anim.emphasizedDecel + } + } + +@@ -163,8 +163,8 @@ Item { + enabled: root.visibilities.launcher + + Anim { +- duration: Appearance.anim.durations.large +- easing.bezierCurve: Appearance.anim.curves.emphasizedDecel ++ duration: Tokens.anim.durations.large ++ easing: Tokens.anim.emphasizedDecel + } + } + } +diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml +index 4aba4365..3663f4bc 100644 +--- a/modules/launcher/WallpaperList.qml ++++ b/modules/launcher/WallpaperList.qml +@@ -1,11 +1,11 @@ + pragma ComponentBehavior: Bound + + import "items" ++import QtQuick ++import Quickshell ++import Caelestia.Config + import qs.components.controls + import qs.services +-import qs.config +-import Quickshell +-import QtQuick + + PathView { + id: root +@@ -15,10 +15,10 @@ PathView { + required property var panels + required property var content + +- readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 ++ readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 + + readonly property int numItems: { +- const screen = QsWindow.window?.screen; ++ const screen = (QsWindow.window as QsWindow)?.screen; + if (!screen) + return 0; + +@@ -58,7 +58,7 @@ PathView { + + onCurrentItemChanged: { + if (currentItem) +- Wallpapers.preview(currentItem.modelData.path); ++ Wallpapers.preview((currentItem as WallpaperItem).modelData.path); + } + + implicitWidth: Math.min(numItems, count) * itemWidth +diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml +index d62d726a..d5630b23 100644 +--- a/modules/launcher/Wrapper.qml ++++ b/modules/launcher/Wrapper.qml +@@ -1,111 +1,47 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.modules.launcher.services + + Item { + id: root + + required property ShellScreen screen +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + required property var panels + + readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled +- property int contentHeight + + readonly property real maxHeight: { +- let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; ++ let max = screen.height - Config.border.thickness * 2 - Tokens.spacing.large; + if (visibilities.dashboard) + max -= panels.dashboard.nonAnimHeight; + return max; + } + +- onMaxHeightChanged: timer.start() +- +- visible: height > 0 +- implicitHeight: 0 +- implicitWidth: content.implicitWidth ++ property real offsetScale: shouldBeActive ? 0 : 1 + + onShouldBeActiveChanged: { +- if (shouldBeActive) { +- timer.stop(); +- hideAnim.stop(); +- showAnim.start(); +- } else { +- showAnim.stop(); +- hideAnim.start(); +- } ++ if (shouldBeActive) ++ implicitHeight = Qt.binding(() => content.implicitHeight); ++ else ++ implicitHeight = implicitHeight; // Break binding during close anim + } + +- SequentialAnimation { +- id: showAnim +- +- Anim { +- target: root +- property: "implicitHeight" +- to: root.contentHeight +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- ScriptAction { +- script: root.implicitHeight = Qt.binding(() => content.implicitHeight) +- } +- } ++ visible: offsetScale < 1 ++ anchors.bottomMargin: (-implicitHeight - 5) * offsetScale ++ implicitHeight: content.implicitHeight ++ implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open ++ opacity: 1 - offsetScale + +- SequentialAnimation { +- id: hideAnim ++ Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init + +- ScriptAction { +- script: root.implicitHeight = root.implicitHeight +- } ++ Behavior on offsetScale { + Anim { +- target: root +- property: "implicitHeight" +- to: 0 +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- } +- +- Connections { +- target: Config.launcher +- +- function onEnabledChanged(): void { +- timer.start(); +- } +- +- function onMaxShownChanged(): void { +- timer.start(); +- } +- } +- +- Connections { +- target: DesktopEntries.applications +- +- function onValuesChanged(): void { +- if (DesktopEntries.applications.values.length < Config.launcher.maxShown) +- timer.start(); +- } +- } +- +- Timer { +- id: timer +- +- interval: Appearance.anim.durations.extraLarge +- onRunningChanged: { +- if (running && !root.shouldBeActive) { +- content.visible = false; +- content.active = true; +- } else { +- root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); +- content.active = Qt.binding(() => root.shouldBeActive || root.visible); +- content.visible = true; +- if (showAnim.running) { +- showAnim.stop(); +- showAnim.start(); +- } +- } ++ type: Anim.DefaultSpatial + } + } + +@@ -115,16 +51,12 @@ Item { + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + +- visible: false +- active: false +- Component.onCompleted: timer.start() ++ active: root.shouldBeActive || root.visible + + sourceComponent: Content { + visibilities: root.visibilities + panels: root.panels + maxHeight: root.maxHeight +- +- Component.onCompleted: root.contentHeight = implicitHeight + } + } + } +diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml +index e1580290..0f9fb5dd 100644 +--- a/modules/launcher/items/ActionItem.qml ++++ b/modules/launcher/items/ActionItem.qml +@@ -1,8 +1,7 @@ +-import "../services" ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import QtQuick + + Item { + id: root +@@ -10,37 +9,34 @@ Item { + required property var modelData + required property var list + +- implicitHeight: Config.launcher.sizes.itemHeight ++ implicitHeight: Tokens.sizes.launcher.itemHeight + + anchors.left: parent?.left + anchors.right: parent?.right + + StateLayer { +- radius: Appearance.rounding.normal +- +- function onClicked(): void { +- root.modelData?.onClicked(root.list); +- } ++ radius: Tokens.rounding.normal ++ onClicked: root.modelData?.onClicked(root.list) + } + + Item { + anchors.fill: parent +- anchors.leftMargin: Appearance.padding.larger +- anchors.rightMargin: Appearance.padding.larger +- anchors.margins: Appearance.padding.smaller ++ anchors.leftMargin: Tokens.padding.larger ++ anchors.rightMargin: Tokens.padding.larger ++ anchors.margins: Tokens.padding.smaller + + MaterialIcon { + id: icon + + text: root.modelData?.icon ?? "" +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + + anchors.verticalCenter: parent.verticalCenter + } + + Item { + anchors.left: icon.right +- anchors.leftMargin: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.spacing.normal + anchors.verticalCenter: icon.verticalCenter + + implicitWidth: parent.width - icon.width +@@ -50,18 +46,18 @@ Item { + id: name + + text: root.modelData?.name ?? "" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledText { + id: desc + + text: root.modelData?.desc ?? "" +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + + elide: Text.ElideRight +- width: root.width - icon.width - Appearance.rounding.normal * 2 ++ width: root.width - icon.width - Tokens.rounding.normal * 2 + + anchors.top: name.bottom + } +diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml +index 48aace76..80739c8f 100644 +--- a/modules/launcher/items/AppItem.qml ++++ b/modules/launcher/items/AppItem.qml +@@ -1,26 +1,26 @@ +-import "../services" +-import qs.components +-import qs.services +-import qs.config ++import QtQuick + import Quickshell + import Quickshell.Widgets +-import QtQuick ++import Caelestia.Config ++import qs.components ++import qs.services ++import qs.utils ++import qs.modules.launcher.services + + Item { + id: root + + required property DesktopEntry modelData +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + +- implicitHeight: Config.launcher.sizes.itemHeight ++ implicitHeight: Tokens.sizes.launcher.itemHeight + + anchors.left: parent?.left + anchors.right: parent?.right + + StateLayer { +- radius: Appearance.rounding.normal +- +- function onClicked(): void { ++ radius: Tokens.rounding.normal ++ onClicked: { + Apps.launch(root.modelData); + root.visibilities.launcher = false; + } +@@ -28,13 +28,14 @@ Item { + + Item { + anchors.fill: parent +- anchors.leftMargin: Appearance.padding.larger +- anchors.rightMargin: Appearance.padding.larger +- anchors.margins: Appearance.padding.smaller ++ anchors.leftMargin: Tokens.padding.larger ++ anchors.rightMargin: Tokens.padding.larger ++ anchors.margins: Tokens.padding.smaller + + IconImage { + id: icon + ++ asynchronous: true + source: Quickshell.iconPath(root.modelData?.icon, "image-missing") + implicitSize: parent.height * 0.8 + +@@ -43,31 +44,46 @@ Item { + + Item { + anchors.left: icon.right +- anchors.leftMargin: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.spacing.normal + anchors.verticalCenter: icon.verticalCenter + +- implicitWidth: parent.width - icon.width ++ implicitWidth: parent.width - icon.width - favouriteIcon.width + implicitHeight: name.implicitHeight + comment.implicitHeight + + StyledText { + id: name + + text: root.modelData?.name ?? "" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledText { + id: comment + + text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + + elide: Text.ElideRight +- width: root.width - icon.width - Appearance.rounding.normal * 2 ++ width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.normal * 2 + + anchors.top: name.bottom + } + } ++ ++ Loader { ++ id: favouriteIcon ++ ++ asynchronous: true ++ anchors.verticalCenter: parent.verticalCenter ++ anchors.right: parent.right ++ active: root.modelData && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, root.modelData.id) ++ ++ sourceComponent: MaterialIcon { ++ text: "favorite" ++ fill: 1 ++ color: Colours.palette.m3primary ++ } ++ } + } + } +diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml +index 65489d9b..f746f96c 100644 +--- a/modules/launcher/items/CalcItem.qml ++++ b/modules/launcher/items/CalcItem.qml +@@ -1,46 +1,48 @@ +-import qs.components +-import qs.services +-import qs.config +-import Caelestia +-import Quickshell + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Caelestia ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root + + required property var list +- readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) ++ readonly property string math: list.search.text.slice(`${GlobalConfig.launcher.actionPrefix}calc `.length) + + function onClicked(): void { +- Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); ++ Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); + root.list.visibilities.launcher = false; + } + +- implicitHeight: Config.launcher.sizes.itemHeight ++ onMathChanged: { ++ if (math.length > 0) ++ Qalculator.evalAsync(math); ++ } ++ ++ implicitHeight: Tokens.sizes.launcher.itemHeight + + anchors.left: parent?.left + anchors.right: parent?.right + + StateLayer { +- radius: Appearance.rounding.normal +- +- function onClicked(): void { +- root.onClicked(); +- } ++ radius: Tokens.rounding.normal ++ onClicked: root.onClicked() + } + + RowLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter +- anchors.margins: Appearance.padding.larger ++ anchors.margins: Tokens.padding.larger + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + MaterialIcon { + text: "function" +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + Layout.alignment: Qt.AlignVCenter + } + +@@ -55,7 +57,7 @@ Item { + return Colours.palette.m3onSurface; + } + +- text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") ++ text: root.math.length > 0 ? (Qalculator.result || qsTr("Calculating...")) : qsTr("Type an expression to calculate") + elide: Text.ElideLeft + + Layout.fillWidth: true +@@ -64,23 +66,23 @@ Item { + + StyledRect { + color: Colours.palette.m3tertiary +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + clip: true + +- implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 ++ implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small * 2 + + Layout.alignment: Qt.AlignVCenter + + StateLayer { + id: stateLayer + +- color: Colours.palette.m3onTertiary +- +- function onClicked(): void { +- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); ++ onClicked: { ++ Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); + root.list.visibilities.launcher = false; + } ++ ++ color: Colours.palette.m3onTertiary + } + + StyledText { +@@ -88,11 +90,11 @@ Item { + + anchors.verticalCenter: parent.verticalCenter + anchors.right: icon.left +- anchors.rightMargin: Appearance.spacing.small ++ anchors.rightMargin: Tokens.spacing.small + + text: qsTr("Open in calculator") + color: Colours.palette.m3onTertiary +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + + opacity: stateLayer.containsMouse ? 1 : 0 + +@@ -106,16 +108,16 @@ Item { + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right +- anchors.rightMargin: Appearance.padding.normal ++ anchors.rightMargin: Tokens.padding.normal + + text: "open_in_new" + color: Colours.palette.m3onTertiary +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + + Behavior on implicitWidth { + Anim { +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + } +diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml +index 3ff18468..ab0784bd 100644 +--- a/modules/launcher/items/SchemeItem.qml ++++ b/modules/launcher/items/SchemeItem.qml +@@ -1,8 +1,8 @@ +-import "../services" ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import QtQuick ++import qs.modules.launcher.services + + Item { + id: root +@@ -10,24 +10,21 @@ Item { + required property Schemes.Scheme modelData + required property var list + +- implicitHeight: Config.launcher.sizes.itemHeight ++ implicitHeight: Tokens.sizes.launcher.itemHeight + + anchors.left: parent?.left + anchors.right: parent?.right + + StateLayer { +- radius: Appearance.rounding.normal +- +- function onClicked(): void { +- root.modelData?.onClicked(root.list); +- } ++ radius: Tokens.rounding.normal ++ onClicked: root.modelData?.onClicked(root.list) + } + + Item { + anchors.fill: parent +- anchors.leftMargin: Appearance.padding.larger +- anchors.rightMargin: Appearance.padding.larger +- anchors.margins: Appearance.padding.smaller ++ anchors.leftMargin: Tokens.padding.larger ++ anchors.rightMargin: Tokens.padding.larger ++ anchors.margins: Tokens.padding.smaller + + StyledRect { + id: preview +@@ -38,7 +35,7 @@ Item { + border.color: Qt.alpha(`#${root.modelData?.colours?.outline}`, 0.5) + + color: `#${root.modelData?.colours?.surface}` +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + implicitWidth: parent.height * 0.8 + implicitHeight: parent.height * 0.8 + +@@ -57,27 +54,27 @@ Item { + + implicitWidth: preview.implicitWidth + color: `#${root.modelData?.colours?.primary}` +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + } + } + } + + Column { + anchors.left: preview.right +- anchors.leftMargin: Appearance.spacing.normal ++ anchors.leftMargin: Tokens.spacing.normal + anchors.verticalCenter: parent.verticalCenter + +- width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) ++ width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) + spacing: 0 + + StyledText { + text: root.modelData?.flavour ?? "" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledText { + text: root.modelData?.name ?? "" +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + + elide: Text.ElideRight +@@ -89,6 +86,7 @@ Item { + Loader { + id: current + ++ asynchronous: true + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + +@@ -97,7 +95,7 @@ Item { + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml +index 5c34fa89..b13d5d8e 100644 +--- a/modules/launcher/items/VariantItem.qml ++++ b/modules/launcher/items/VariantItem.qml +@@ -1,8 +1,8 @@ +-import "../services" ++import QtQuick ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import QtQuick ++import qs.modules.launcher.services + + Item { + id: root +@@ -10,50 +10,47 @@ Item { + required property M3Variants.Variant modelData + required property var list + +- implicitHeight: Config.launcher.sizes.itemHeight ++ implicitHeight: Tokens.sizes.launcher.itemHeight + + anchors.left: parent?.left + anchors.right: parent?.right + + StateLayer { +- radius: Appearance.rounding.normal +- +- function onClicked(): void { +- root.modelData?.onClicked(root.list); +- } ++ radius: Tokens.rounding.normal ++ onClicked: root.modelData?.onClicked(root.list) + } + + Item { + anchors.fill: parent +- anchors.leftMargin: Appearance.padding.larger +- anchors.rightMargin: Appearance.padding.larger +- anchors.margins: Appearance.padding.smaller ++ anchors.leftMargin: Tokens.padding.larger ++ anchors.rightMargin: Tokens.padding.larger ++ anchors.margins: Tokens.padding.smaller + + MaterialIcon { + id: icon + + text: root.modelData?.icon ?? "" +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.left: icon.right +- anchors.leftMargin: Appearance.spacing.larger ++ anchors.leftMargin: Tokens.spacing.larger + anchors.verticalCenter: icon.verticalCenter + +- width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) ++ width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) + spacing: 0 + + StyledText { + text: root.modelData?.name ?? "" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + StyledText { + text: root.modelData?.description ?? "" +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + + elide: Text.ElideRight +@@ -65,6 +62,7 @@ Item { + Loader { + id: current + ++ asynchronous: true + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + +@@ -73,7 +71,7 @@ Item { + sourceComponent: MaterialIcon { + text: "check" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml +index 9fdac3f3..d09a894c 100644 +--- a/modules/launcher/items/WallpaperItem.qml ++++ b/modules/launcher/items/WallpaperItem.qml +@@ -1,34 +1,33 @@ ++import QtQuick ++import Quickshell ++import Caelestia.Config ++import Caelestia.Models + import qs.components + import qs.components.effects + import qs.components.images + import qs.services +-import qs.config +-import Caelestia.Models +-import Quickshell +-import QtQuick + + Item { + id: root + + required property FileSystemEntry modelData +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + + scale: 0.5 + opacity: 0 +- z: PathView.z ?? 0 ++ z: PathView.z ?? 0 // qmllint disable missing-property + + Component.onCompleted: { + scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); + opacity = Qt.binding(() => PathView.onPath ? 1 : 0); + } + +- implicitWidth: image.width + Appearance.padding.larger * 2 +- implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal ++ implicitWidth: image.width + Tokens.padding.larger * 2 ++ implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal + + StateLayer { +- radius: Appearance.rounding.normal +- +- function onClicked(): void { ++ radius: Tokens.rounding.normal ++ onClicked: { + Wallpapers.setWallpaper(root.modelData.path); + root.visibilities.launcher = false; + } +@@ -49,27 +48,29 @@ Item { + id: image + + anchors.horizontalCenter: parent.horizontalCenter +- y: Appearance.padding.large ++ y: Tokens.padding.large + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + +- implicitWidth: Config.launcher.sizes.wallpaperWidth ++ implicitWidth: Tokens.sizes.launcher.wallpaperWidth + implicitHeight: implicitWidth / 16 * 9 + + MaterialIcon { + anchors.centerIn: parent + text: "image" + color: Colours.tPalette.m3outline +- font.pointSize: Appearance.font.size.extraLarge * 2 ++ font.pointSize: Tokens.font.size.extraLarge * 2 + font.weight: 600 + } + + CachingImage { ++ anchors.fill: parent + path: root.modelData.path + smooth: !root.PathView.view.moving +- cache: true +- +- anchors.fill: parent ++ sourceSize: { ++ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; ++ return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr); ++ } + } + } + +@@ -77,15 +78,15 @@ Item { + id: label + + anchors.top: image.bottom +- anchors.topMargin: Appearance.spacing.small / 2 ++ anchors.topMargin: Tokens.spacing.small / 2 + anchors.horizontalCenter: parent.horizontalCenter + +- width: image.width - Appearance.padding.normal * 2 ++ width: image.width - Tokens.padding.normal * 2 + horizontalAlignment: Text.AlignHCenter + elide: Text.ElideRight + renderType: Text.QtRendering + text: root.modelData.relativePath +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + Behavior on scale { +diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml +index 5c1cb6bb..634b3e6a 100644 +--- a/modules/launcher/services/Actions.qml ++++ b/modules/launcher/services/Actions.qml +@@ -1,26 +1,26 @@ + pragma Singleton + + import ".." ++import QtQuick ++import Quickshell ++import Caelestia.Config + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick + + Searcher { + id: root + + function transformSearch(search: string): string { +- return search.slice(Config.launcher.actionPrefix.length); ++ return search.slice(GlobalConfig.launcher.actionPrefix.length); + } + + list: variants.instances +- useFuzzy: Config.launcher.useFuzzy.actions ++ useFuzzy: GlobalConfig.launcher.useFuzzy.actions + + Variants { + id: variants + +- model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false))) ++ model: GlobalConfig.launcher.actions.filter(a => (a.enabled ?? true) && (GlobalConfig.launcher.enableDangerousActions || !(a.dangerous ?? false))) + + Action {} + } +@@ -39,7 +39,7 @@ Searcher { + return; + + if (command[0] === "autocomplete" && command.length > 1) { +- list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; ++ list.search.text = `${GlobalConfig.launcher.actionPrefix}${command[1]} `; + } else if (command[0] === "setMode" && command.length > 1) { + list.visibilities.launcher = false; + Colours.setMode(command[1]); +diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml +index c409a7bb..168d9209 100644 +--- a/modules/launcher/services/Apps.qml ++++ b/modules/launcher/services/Apps.qml +@@ -1,9 +1,9 @@ + pragma Singleton + +-import qs.config +-import qs.utils +-import Caelestia + import Quickshell ++import Caelestia ++import Caelestia.Config ++import qs.utils + + Searcher { + id: root +@@ -13,7 +13,7 @@ Searcher { + + if (entry.runInTerminal) + Quickshell.execDetached({ +- command: ["app2unit", "--", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], ++ command: ["app2unit", "--", ...GlobalConfig.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], + workingDirectory: entry.workingDirectory + }); + else +@@ -24,7 +24,7 @@ Searcher { + } + + function search(search: string): list { +- const prefix = Config.launcher.specialPrefix; ++ const prefix = GlobalConfig.launcher.specialPrefix; + + if (search.startsWith(`${prefix}i `)) { + keys = ["id", "name"]; +@@ -66,12 +66,13 @@ Searcher { + } + + list: appDb.apps +- useFuzzy: Config.launcher.useFuzzy.apps ++ useFuzzy: GlobalConfig.launcher.useFuzzy.apps + + AppDb { + id: appDb + + path: `${Paths.state}/apps.sqlite` +- entries: DesktopEntries.applications.values.filter(a => !Config.launcher.hiddenApps.includes(a.id)) ++ favouriteApps: GlobalConfig.launcher.favouriteApps ++ entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(GlobalConfig.launcher.hiddenApps, a.id)) + } + } +diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml +index 963a4d43..4517d02c 100644 +--- a/modules/launcher/services/M3Variants.qml ++++ b/modules/launcher/services/M3Variants.qml +@@ -1,16 +1,16 @@ + pragma Singleton + + import ".." +-import qs.config +-import qs.utils +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia.Config ++import qs.utils + + Searcher { + id: root + + function transformSearch(search: string): string { +- return search.slice(`${Config.launcher.actionPrefix}variant `.length); ++ return search.slice(`${GlobalConfig.launcher.actionPrefix}variant `.length); + } + + list: [ +@@ -69,7 +69,7 @@ Searcher { + description: qsTr("All colours are grayscale, no chroma.") + } + ] +- useFuzzy: Config.launcher.useFuzzy.variants ++ useFuzzy: GlobalConfig.launcher.useFuzzy.variants + + component Variant: QtObject { + required property string variant +diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml +index dbb2dac0..cc555f29 100644 +--- a/modules/launcher/services/Schemes.qml ++++ b/modules/launcher/services/Schemes.qml +@@ -1,11 +1,11 @@ + pragma Singleton + + import ".." +-import qs.config +-import qs.utils ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick ++import Caelestia.Config ++import qs.utils + + Searcher { + id: root +@@ -14,7 +14,7 @@ Searcher { + property string currentVariant + + function transformSearch(search: string): string { +- return search.slice(`${Config.launcher.actionPrefix}scheme `.length); ++ return search.slice(`${GlobalConfig.launcher.actionPrefix}scheme `.length); + } + + function selector(item: var): string { +@@ -26,7 +26,7 @@ Searcher { + } + + list: schemes.instances +- useFuzzy: Config.launcher.useFuzzy.schemes ++ useFuzzy: GlobalConfig.launcher.useFuzzy.schemes + keys: ["name", "flavour"] + weights: [0.9, 0.1] + +@@ -55,7 +55,7 @@ Searcher { + for (const f of s) + flat.push(f); + +- schemes.model = flat.sort((a, b) => (a.name + a.flavour).localeCompare((b.name + b.flavour))); ++ schemes.model = flat.sort((a, b) => String(a.name + a.flavour).localeCompare((b.name + b.flavour))); + } + } + } +diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml +index 19cf9d29..a987d4eb 100644 +--- a/modules/lock/Center.qml ++++ b/modules/lock/Center.qml +@@ -1,37 +1,37 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.images + import qs.services +-import qs.config + import qs.utils +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property var lock + readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) +- readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale ++ readonly property int centerWidth: Tokens.sizes.lock.centerWidth * centerScale + + Layout.preferredWidth: centerWidth + Layout.fillWidth: false + Layout.fillHeight: true + +- spacing: Appearance.spacing.large * 2 ++ spacing: Tokens.spacing.large * 2 + + RowLayout { + Layout.alignment: Qt.AlignHCenter +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + Layout.alignment: Qt.AlignVCenter + text: Time.hourStr + color: Colours.palette.m3secondary +- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) +- font.family: Appearance.font.family.clock ++ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) ++ font.family: Tokens.font.family.clock + font.bold: true + } + +@@ -39,8 +39,8 @@ ColumnLayout { + Layout.alignment: Qt.AlignVCenter + text: ":" + color: Colours.palette.m3primary +- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) +- font.family: Appearance.font.family.clock ++ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) ++ font.family: Tokens.font.family.clock + font.bold: true + } + +@@ -48,23 +48,24 @@ ColumnLayout { + Layout.alignment: Qt.AlignVCenter + text: Time.minuteStr + color: Colours.palette.m3secondary +- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) +- font.family: Appearance.font.family.clock ++ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) ++ font.family: Tokens.font.family.clock + font.bold: true + } + + Loader { +- Layout.leftMargin: Appearance.spacing.small ++ asynchronous: true ++ Layout.leftMargin: Tokens.spacing.small + Layout.alignment: Qt.AlignVCenter + +- active: Config.services.useTwelveHourClock ++ active: GlobalConfig.services.useTwelveHourClock + visible: active + + sourceComponent: StyledText { + text: Time.amPmStr + color: Colours.palette.m3primary +- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 2 * root.centerScale) +- font.family: Appearance.font.family.clock ++ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 2 * root.centerScale) ++ font.family: Tokens.font.family.clock + font.bold: true + } + } +@@ -72,24 +73,24 @@ ColumnLayout { + + StyledText { + Layout.alignment: Qt.AlignHCenter +- Layout.topMargin: -Appearance.padding.large * 2 ++ Layout.topMargin: -Tokens.padding.large * 2 + + text: Time.format("dddd, d MMMM yyyy") + color: Colours.palette.m3tertiary +- font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) +- font.family: Appearance.font.family.mono ++ font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale) ++ font.family: Tokens.font.family.mono + font.bold: true + } + + StyledClippingRect { +- Layout.topMargin: Appearance.spacing.large * 2 ++ Layout.topMargin: Tokens.spacing.large * 2 + Layout.alignment: Qt.AlignHCenter + + implicitWidth: root.centerWidth / 2 + implicitHeight: root.centerWidth / 2 + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + MaterialIcon { + anchors.centerIn: parent +@@ -97,6 +98,7 @@ ColumnLayout { + text: "person" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Math.floor(root.centerWidth / 4) ++ visible: pfp.status !== Image.Ready + } + + CachingImage { +@@ -111,10 +113,10 @@ ColumnLayout { + Layout.alignment: Qt.AlignHCenter + + implicitWidth: root.centerWidth * 0.8 +- implicitHeight: input.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: input.implicitHeight + Tokens.padding.small * 2 + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + focus: true + onActiveFocusChanged: { +@@ -133,24 +135,24 @@ ColumnLayout { + } + + StateLayer { +- hoverEnabled: false +- cursorShape: Qt.IBeamCursor +- +- function onClicked(): void { ++ onClicked: { + parent.forceActiveFocus(); + } ++ ++ hoverEnabled: false ++ cursorShape: Qt.IBeamCursor + } + + RowLayout { + id: input + + anchors.fill: parent +- anchors.margins: Appearance.padding.small +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.small ++ spacing: Tokens.spacing.normal + + Item { + implicitWidth: implicitHeight +- implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small * 2 + + MaterialIcon { + id: fprintIcon +@@ -158,13 +160,13 @@ ColumnLayout { + anchors.centerIn: parent + animate: true + text: { +- if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) ++ if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) + return "fingerprint_off"; + if (root.lock.pam.fprint.active) + return "fingerprint"; + return "lock"; + } +- color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface ++ color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface + opacity: root.lock.pam.passwd.active ? 0 : 1 + + Behavior on opacity { +@@ -186,17 +188,14 @@ ColumnLayout { + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: enterIcon.implicitHeight + Tokens.padding.small * 2 + + color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + StateLayer { + color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface +- +- function onClicked(): void { +- root.lock.pam.passwd.start(); +- } ++ onClicked: root.lock.pam.passwd.start() + } + + MaterialIcon { +@@ -213,7 +212,7 @@ ColumnLayout { + + Item { + Layout.fillWidth: true +- Layout.topMargin: -Appearance.spacing.large ++ Layout.topMargin: -Tokens.spacing.large + + implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) + +@@ -270,7 +269,7 @@ ColumnLayout { + color: Colours.palette.m3onSurfaceVariant + animateProp: "opacity" + +- font.family: Appearance.font.family.mono ++ font.family: Tokens.font.family.mono + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + lineHeight: 1.2 +@@ -325,8 +324,8 @@ ColumnLayout { + opacity: 0 + color: Colours.palette.m3error + +- font.pointSize: Appearance.font.size.small +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.small ++ font.family: Tokens.font.family.mono + horizontalAlignment: Qt.AlignHCenter + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + +@@ -355,8 +354,6 @@ ColumnLayout { + } + + Connections { +- target: root.lock.pam +- + function onFlashMsg(): void { + exitAnim.stop(); + if (message.scale < 1) +@@ -364,6 +361,8 @@ ColumnLayout { + else + flashAnim.restart(); + } ++ ++ target: root.lock.pam + } + + Anim { +@@ -395,13 +394,13 @@ ColumnLayout { + target: message + property: "scale" + to: 0.7 +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + Anim { + target: message + property: "opacity" + to: 0 +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + } + } +@@ -410,7 +409,7 @@ ColumnLayout { + component FlashAnim: NumberAnimation { + target: message + property: "opacity" +- duration: Appearance.anim.durations.small ++ duration: Tokens.anim.durations.small + easing.type: Easing.Linear + } + } +diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml +index a024ddc2..0ff7daad 100644 +--- a/modules/lock/Content.qml ++++ b/modules/lock/Content.qml +@@ -1,26 +1,26 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + RowLayout { + id: root + + required property var lock + +- spacing: Appearance.spacing.large * 2 ++ spacing: Tokens.spacing.large * 2 + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + Layout.fillWidth: true + implicitHeight: weather.implicitHeight + +- topLeftRadius: Appearance.rounding.large +- radius: Appearance.rounding.small ++ topLeftRadius: Tokens.rounding.large ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainer + + WeatherInfo { +@@ -34,7 +34,7 @@ RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainer + + Fetch {} +@@ -44,8 +44,8 @@ RowLayout { + Layout.fillWidth: true + implicitHeight: media.implicitHeight + +- bottomLeftRadius: Appearance.rounding.large +- radius: Appearance.rounding.small ++ bottomLeftRadius: Tokens.rounding.large ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainer + + Media { +@@ -62,14 +62,14 @@ RowLayout { + + ColumnLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + Layout.fillWidth: true + implicitHeight: resources.implicitHeight + +- topRightRadius: Appearance.rounding.large +- radius: Appearance.rounding.small ++ topRightRadius: Tokens.rounding.large ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainer + + Resources { +@@ -81,8 +81,8 @@ RowLayout { + Layout.fillWidth: true + Layout.fillHeight: true + +- bottomRightRadius: Appearance.rounding.large +- radius: Appearance.rounding.small ++ bottomRightRadius: Tokens.rounding.large ++ radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainer + + NotifDock { +diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml +index ded56084..0afbe455 100644 +--- a/modules/lock/Fetch.qml ++++ b/modules/lock/Fetch.qml +@@ -1,41 +1,41 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Services.UPower ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services +-import qs.config + import qs.utils +-import Quickshell.Services.UPower +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + + anchors.fill: parent +- anchors.margins: Appearance.padding.large * 2 +- anchors.topMargin: Appearance.padding.large ++ anchors.margins: Tokens.padding.large * 2 ++ anchors.topMargin: Tokens.padding.large + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true + Layout.fillHeight: false +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { +- implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2 ++ implicitWidth: prompt.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: prompt.implicitHeight + Tokens.padding.normal * 2 + + color: Colours.palette.m3primary +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + + MonoText { + id: prompt + + anchors.centerIn: parent + text: ">" +- font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal ++ font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal + color: Colours.palette.m3onPrimary + } + } +@@ -43,7 +43,7 @@ ColumnLayout { + MonoText { + Layout.fillWidth: true + text: "caelestiafetch.sh" +- font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal ++ font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal + elide: Text.ElideRight + } + +@@ -51,7 +51,7 @@ ColumnLayout { + Layout.fillHeight: true + active: !iconLoader.active + +- sourceComponent: OsLogo {} ++ sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon + } + } + +@@ -66,15 +66,15 @@ ColumnLayout { + Layout.fillHeight: true + active: root.width > 320 + +- sourceComponent: OsLogo {} ++ sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon + } + + ColumnLayout { + Layout.fillWidth: true +- Layout.topMargin: Appearance.padding.normal +- Layout.bottomMargin: Appearance.padding.normal ++ Layout.topMargin: Tokens.padding.normal ++ Layout.bottomMargin: Tokens.padding.normal + Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + WrappedLoader { + Layout.fillWidth: true +@@ -125,41 +125,54 @@ ColumnLayout { + active: root.height > 180 + + sourceComponent: RowLayout { +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + Repeater { +- model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large))) ++ model: Math.max(0, Math.min(8, root.width / (Tokens.font.size.larger * 2 + Tokens.spacing.large))) + + StyledRect { + required property int index + + implicitWidth: implicitHeight +- implicitHeight: Appearance.font.size.larger * 2 ++ implicitHeight: Tokens.font.size.larger * 2 + color: Colours.palette[`term${index}`] +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + } + } + } + } + +- component WrappedLoader: Loader { +- visible: active ++ Component { ++ id: caelestiaLogo ++ ++ Logo { ++ width: height ++ } ++ } ++ ++ Component { ++ id: distroIcon ++ ++ ColouredIcon { ++ source: SysInfo.osLogo ++ implicitSize: height ++ colour: Colours.palette.m3primary ++ layer.enabled: Config.lock.recolourLogo ++ } + } + +- component OsLogo: ColouredIcon { +- source: SysInfo.osLogo +- implicitSize: height +- colour: Colours.palette.m3primary +- layer.enabled: Config.lock.recolourLogo || SysInfo.isDefaultLogo ++ component WrappedLoader: Loader { ++ asynchronous: true ++ visible: active + } + + component FetchText: MonoText { + Layout.fillWidth: true +- font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal ++ font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal + elide: Text.ElideRight + } + + component MonoText: StyledText { +- font.family: Appearance.font.family.mono ++ font.family: Tokens.font.family.mono + } + } +diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml +index 358093f3..1b6c34a7 100644 +--- a/modules/lock/InputField.qml ++++ b/modules/lock/InputField.qml +@@ -1,11 +1,11 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import Quickshell + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root +@@ -20,8 +20,6 @@ Item { + clip: true + + Connections { +- target: root.pam +- + function onBufferChanged(): void { + if (root.pam.buffer.length > root.buffer.length) { + charList.bindImWidth(); +@@ -32,6 +30,8 @@ Item { + + root.buffer = root.pam.buffer; + } ++ ++ target: root.pam + } + + StyledText { +@@ -49,8 +49,8 @@ Item { + + animate: true + color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.normal ++ font.family: Tokens.font.family.mono + + opacity: root.buffer ? 0 : 1 + +@@ -74,10 +74,10 @@ Item { + anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 + + implicitWidth: fullWidth +- implicitHeight: Appearance.font.size.normal ++ implicitHeight: Tokens.font.size.normal + + orientation: Qt.Horizontal +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + interactive: false + + model: ScriptModel { +@@ -91,7 +91,7 @@ Item { + implicitHeight: charList.implicitHeight + + color: Colours.palette.m3onSurface +- radius: Appearance.rounding.small / 2 ++ radius: Tokens.rounding.small / 2 + + opacity: 0 + scale: 0 +@@ -134,8 +134,7 @@ Item { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml +index 6fd5277f..f852cb7f 100644 +--- a/modules/lock/Lock.qml ++++ b/modules/lock/Lock.qml +@@ -1,9 +1,10 @@ + pragma ComponentBehavior: Bound + +-import qs.components.misc ++import QtQuick + import Quickshell + import Quickshell.Io + import Quickshell.Wayland ++import qs.components.misc + + Scope { + property alias lock: lock +@@ -25,21 +26,37 @@ Scope { + lock: lock + } + ++ Loader { ++ asynchronous: true ++ active: true ++ onLoaded: active = false ++ ++ // Force a load of a screencopy so the one in the lock works ++ // My guess is the ICC backend loads async on first request, which if the lock is ++ // the first request it fails to capture (because it's async and the compositor ++ // refuses capture when locked) ++ sourceComponent: ScreencopyView { ++ captureSource: Quickshell.screens[0] ++ } ++ } ++ ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "lock" + description: "Lock the current session" + onPressed: lock.locked = true + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "unlock" + description: "Unlock the current session" + onPressed: lock.unlock() + } + + IpcHandler { +- target: "lock" +- + function lock(): void { + lock.locked = true; + } +@@ -51,5 +68,7 @@ Scope { + function isLocked(): bool { + return lock.locked; + } ++ ++ target: "lock" + } + } +diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml +index 279c5513..322773fd 100644 +--- a/modules/lock/LockSurface.qml ++++ b/modules/lock/LockSurface.qml +@@ -1,11 +1,11 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import Quickshell.Wayland + import QtQuick + import QtQuick.Effects ++import Quickshell.Wayland ++import Caelestia.Config ++import qs.components ++import qs.services + + WlSessionLockSurface { + id: root +@@ -15,14 +15,17 @@ WlSessionLockSurface { + + readonly property alias unlocking: unlockAnim.running + ++ contentItem.Config.screen: screen.name ++ contentItem.Tokens.screen: screen.name ++ + color: "transparent" + + Connections { +- target: root.lock +- + function onUnlock(): void { + unlockAnim.start(); + } ++ ++ target: root.lock + } + + SequentialAnimation { +@@ -33,8 +36,7 @@ WlSessionLockSurface { + target: lockContent + properties: "implicitWidth,implicitHeight" + to: lockContent.size +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + Anim { + target: lockBg +@@ -45,30 +47,29 @@ WlSessionLockSurface { + target: content + property: "scale" + to: 0 +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + Anim { + target: content + property: "opacity" + to: 0 +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + Anim { + target: lockIcon + property: "opacity" + to: 1 +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + Anim { + target: background + property: "opacity" + to: 0 +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + SequentialAnimation { + PauseAnimation { +- duration: Appearance.anim.durations.small ++ duration: Tokens.anim.durations.small + } + Anim { + target: lockContent +@@ -93,7 +94,7 @@ WlSessionLockSurface { + target: background + property: "opacity" + to: 1 +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + SequentialAnimation { + ParallelAnimation { +@@ -101,15 +102,14 @@ WlSessionLockSurface { + target: lockContent + property: "scale" + to: 1 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + Anim { + target: lockContent + property: "rotation" + to: 360 +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.standardAccel ++ duration: Tokens.anim.durations.expressiveFastSpatial ++ easing: Tokens.anim.standardAccel + } + } + ParallelAnimation { +@@ -117,7 +117,7 @@ WlSessionLockSurface { + target: lockIcon + property: "rotation" + to: 360 +- easing.bezierCurve: Appearance.anim.curves.standardDecel ++ easing: Tokens.anim.standardDecel + } + Anim { + target: lockIcon +@@ -133,27 +133,24 @@ WlSessionLockSurface { + target: content + property: "scale" + to: 1 +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + Anim { + target: lockBg + property: "radius" +- to: Appearance.rounding.large * 1.5 ++ to: lockContent.Tokens.rounding.large * 1.5 + } + Anim { + target: lockContent + property: "implicitWidth" +- to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio ++ type: Anim.DefaultSpatial + } + Anim { + target: lockContent + property: "implicitHeight" +- to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult ++ type: Anim.DefaultSpatial + } + } + } +@@ -179,8 +176,8 @@ WlSessionLockSurface { + Item { + id: lockContent + +- readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 +- readonly property int radius: size / 4 * Appearance.rounding.scale ++ readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 ++ readonly property int radius: size / 4 * Tokens.rounding.scale + + anchors.centerIn: parent + implicitWidth: size +@@ -210,7 +207,7 @@ WlSessionLockSurface { + + anchors.centerIn: parent + text: "lock" +- font.pointSize: Appearance.font.size.extraLarge * 4 ++ font.pointSize: Tokens.font.size.extraLarge * 4 + font.bold: true + rotation: 180 + } +@@ -219,8 +216,8 @@ WlSessionLockSurface { + id: content + + anchors.centerIn: parent +- width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 +- height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 ++ width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 ++ height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 + + lock: root + opacity: 0 +diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml +index b7e58bbc..c55333c7 100644 +--- a/modules/lock/Media.qml ++++ b/modules/lock/Media.qml +@@ -1,11 +1,12 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root +@@ -18,12 +19,14 @@ Item { + + Image { + anchors.fill: parent +- source: Players.active?.trackArtUrl ?? "" ++ source: Players.getArtUrl(Players.active) + + asynchronous: true + fillMode: Image.PreserveAspectCrop +- sourceSize.width: width +- sourceSize.height: height ++ sourceSize: { ++ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; ++ return Qt.size(width * dpr, height * dpr); ++ } + + layer.enabled: true + layer.effect: OpacityMask { +@@ -34,7 +37,7 @@ Item { + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.extraLarge ++ type: Anim.StandardExtraLarge + } + } + } +@@ -69,14 +72,14 @@ Item { + + anchors.left: parent.left + anchors.right: parent.right +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + + StyledText { +- Layout.topMargin: Appearance.padding.large +- Layout.bottomMargin: Appearance.spacing.larger ++ Layout.topMargin: Tokens.padding.large ++ Layout.bottomMargin: Tokens.spacing.larger + text: qsTr("Now playing") + color: Colours.palette.m3onSurfaceVariant +- font.family: Appearance.font.family.mono ++ font.family: Tokens.font.family.mono + font.weight: 500 + } + +@@ -86,8 +89,8 @@ Item { + text: Players.active?.trackArtist ?? qsTr("No media") + color: Colours.palette.m3primary + horizontalAlignment: Text.AlignHCenter +- font.pointSize: Appearance.font.size.large +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.large ++ font.family: Tokens.font.family.mono + font.weight: 600 + elide: Text.ElideRight + } +@@ -97,22 +100,21 @@ Item { + animate: true + text: Players.active?.trackTitle ?? qsTr("No media") + horizontalAlignment: Text.AlignHCenter +- font.pointSize: Appearance.font.size.larger +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.larger ++ font.family: Tokens.font.family.mono + elide: Text.ElideRight + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter +- Layout.topMargin: Appearance.spacing.large * 1.2 +- Layout.bottomMargin: Appearance.padding.large ++ Layout.topMargin: Tokens.spacing.large * 1.2 ++ Layout.bottomMargin: Tokens.padding.large + +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + PlayerControl { + icon: "skip_previous" +- +- function onClicked(): void { ++ onClicked: { + if (Players.active?.canGoPrevious) + Players.active.previous(); + } +@@ -124,8 +126,7 @@ Item { + colour: "Primary" + level: active ? 2 : 1 + active: Players.active?.isPlaying ?? false +- +- function onClicked(): void { ++ onClicked: { + if (Players.active?.canTogglePlaying) + Players.active.togglePlaying(); + } +@@ -133,8 +134,7 @@ Item { + + PlayerControl { + icon: "skip_next" +- +- function onClicked(): void { ++ onClicked: { + if (Players.active?.canGoNext) + Players.active.next(); + } +@@ -151,15 +151,14 @@ Item { + property string colour: "Secondary" + property int level: 1 + +- function onClicked(): void { +- } ++ signal clicked + +- Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) +- implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 +- implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2 ++ Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : active ? Tokens.padding.small * 2 : 0) ++ implicitWidth: controlIcon.implicitWidth + Tokens.padding.large * 2 ++ implicitHeight: controlIcon.implicitHeight + Tokens.padding.normal * 2 + + color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`] +- radius: active || controlState.pressed ? Appearance.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Appearance.rounding.scale) ++ radius: active || controlState.pressed ? Tokens.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale) + + Elevation { + anchors.fill: parent +@@ -172,10 +171,7 @@ Item { + id: controlState + + color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] +- +- function onClicked(): void { +- control.onClicked(); +- } ++ onClicked: control.clicked() + } + + MaterialIcon { +@@ -183,7 +179,7 @@ Item { + + anchors.centerIn: parent + color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + fill: control.active ? 1 : 0 + + Behavior on fill { +@@ -193,15 +189,13 @@ Item { + + Behavior on Layout.preferredWidth { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + Behavior on radius { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml +index 01f7e4b4..2254dfe0 100644 +--- a/modules/lock/NotifDock.qml ++++ b/modules/lock/NotifDock.qml +@@ -1,14 +1,15 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components + import qs.components.containers + import qs.components.effects + import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts ++import qs.utils + + ColumnLayout { + id: root +@@ -16,15 +17,15 @@ ColumnLayout { + required property var lock + + anchors.fill: parent +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") + color: Colours.palette.m3outline +- font.family: Appearance.font.family.mono ++ font.family: Tokens.font.family.mono + font.weight: 500 + elide: Text.ElideRight + } +@@ -35,22 +36,23 @@ ColumnLayout { + Layout.fillWidth: true + Layout.fillHeight: true + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: "transparent" + + Loader { ++ asynchronous: true + anchors.centerIn: parent + active: opacity > 0 +- opacity: Notifs.list.length > 0 ? 0 : 1 ++ opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 + + sourceComponent: ColumnLayout { +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + Image { + asynchronous: true +- source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) ++ source: Paths.absolutePath(Config.paths.lockNoNotifsPic) + fillMode: Image.PreserveAspectFit +- sourceSize.width: clipRect.width * 0.8 ++ sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) + + layer.enabled: true + layer.effect: Colouriser { +@@ -61,25 +63,25 @@ ColumnLayout { + + StyledText { + Layout.alignment: Qt.AlignHCenter +- text: qsTr("No Notifications") ++ text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") + color: Colours.palette.m3outlineVariant +- font.pointSize: Appearance.font.size.large +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.large ++ font.family: Tokens.font.family.mono + font.weight: 500 + } + } + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.extraLarge ++ type: Anim.StandardExtraLarge + } + } + } + + StyledListView { + anchors.fill: parent +- +- spacing: Appearance.spacing.small ++ visible: !Config.lock.hideNotifs ++ spacing: Tokens.spacing.small + clip: true + + model: ScriptModel { +@@ -101,8 +103,7 @@ ColumnLayout { + property: "scale" + from: 0 + to: 1 +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + +@@ -124,8 +125,7 @@ ColumnLayout { + } + Anim { + property: "y" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + +@@ -136,8 +136,7 @@ ColumnLayout { + } + Anim { + property: "y" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml +index 77960906..971b921a 100644 +--- a/modules/lock/NotifGroup.qml ++++ b/modules/lock/NotifGroup.qml +@@ -1,15 +1,15 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Quickshell.Services.Notifications ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import Quickshell.Widgets +-import Quickshell.Services.Notifications +-import QtQuick +-import QtQuick.Layouts + + StyledRect { + id: root +@@ -17,18 +17,39 @@ StyledRect { + required property string modelData + + readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) +- readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" +- readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" +- readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" ++ readonly property var props: { ++ let img = ""; ++ let icon = ""; ++ let hasCritical = false; ++ let hasNormal = false; ++ for (const n of notifs) { ++ if (!img && n.image.length > 0) ++ img = n.image; ++ if (!icon && n.appIcon.length > 0) ++ icon = n.appIcon; ++ if (n.urgency === NotificationUrgency.Critical) ++ hasCritical = true; ++ else if (n.urgency === NotificationUrgency.Normal) ++ hasNormal = true; ++ } ++ return { ++ img, ++ icon, ++ urgency: hasCritical ? "critical" : hasNormal ? "normal" : "low" ++ }; ++ } ++ readonly property string image: props.img ++ readonly property string appIcon: props.icon ++ readonly property string urgency: props.urgency + + property bool expanded + + anchors.left: parent?.left + anchors.right: parent?.right +- implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 + + clip: true +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + + RowLayout { +@@ -37,14 +58,14 @@ StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Item { + Layout.alignment: Qt.AlignLeft | Qt.AlignTop +- implicitWidth: Config.notifs.sizes.image +- implicitHeight: Config.notifs.sizes.image ++ implicitWidth: TokenConfig.sizes.notifs.image ++ implicitHeight: TokenConfig.sizes.notifs.image + + Component { + id: imageComp +@@ -52,10 +73,14 @@ StyledRect { + Image { + source: Qt.resolvedUrl(root.image) + fillMode: Image.PreserveAspectCrop ++ sourceSize: { ++ const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); ++ return Qt.size(size, size); ++ } + cache: false + asynchronous: true +- width: Config.notifs.sizes.image +- height: Config.notifs.sizes.image ++ width: TokenConfig.sizes.notifs.image ++ height: TokenConfig.sizes.notifs.image + } + } + +@@ -63,7 +88,7 @@ StyledRect { + id: appIconComp + + ColouredIcon { +- implicitSize: Math.round(Config.notifs.sizes.image * 0.6) ++ implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) + source: Quickshell.iconPath(root.appIcon) + colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.appIcon.endsWith("symbolic") +@@ -76,36 +101,38 @@ StyledRect { + MaterialIcon { + text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) + color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + + ClippingRectangle { + anchors.fill: parent + color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) : Colours.palette.m3secondaryContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + Loader { ++ asynchronous: true + anchors.centerIn: parent + sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp + } + } + + Loader { ++ asynchronous: true + anchors.right: parent.right + anchors.bottom: parent.bottom + active: root.appIcon && root.image + + sourceComponent: StyledRect { +- implicitWidth: Config.notifs.sizes.badge +- implicitHeight: Config.notifs.sizes.badge ++ implicitWidth: Tokens.sizes.notifs.badge ++ implicitHeight: Tokens.sizes.notifs.badge + + color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3secondaryContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + ColouredIcon { + anchors.centerIn: parent +- implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) ++ implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) + source: Quickshell.iconPath(root.appIcon) + colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.appIcon.endsWith("symbolic") +@@ -115,21 +142,21 @@ StyledRect { + } + + ColumnLayout { +- Layout.topMargin: -Appearance.padding.small +- Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing) ++ Layout.topMargin: -Tokens.padding.small ++ Layout.bottomMargin: -Tokens.padding.small / 2 - (root.expanded ? 0 : spacing) + Layout.fillWidth: true +- spacing: Math.round(Appearance.spacing.small / 2) ++ spacing: Math.round(Tokens.spacing.small / 2) + + RowLayout { + Layout.bottomMargin: -parent.spacing + Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: root.modelData + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + } + +@@ -137,45 +164,42 @@ StyledRect { + animate: true + text: root.notifs[0]?.timeStr ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledRect { +- implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 +- implicitHeight: groupCount.implicitHeight + Appearance.padding.small ++ implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 ++ implicitHeight: groupCount.implicitHeight + Tokens.padding.small + + color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0 + Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 + + StateLayer { + color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface +- +- function onClicked(): void { +- root.expanded = !root.expanded; +- } ++ onClicked: root.expanded = !root.expanded + } + + RowLayout { + id: expandBtn + + anchors.centerIn: parent +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { + id: groupCount + +- Layout.leftMargin: Appearance.padding.small / 2 ++ Layout.leftMargin: Tokens.padding.small / 2 + animate: true + text: root.notifs.length + color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + MaterialIcon { +- Layout.rightMargin: -Appearance.padding.small / 2 ++ Layout.rightMargin: -Tokens.padding.small / 2 + animate: true + text: root.expanded ? "expand_less" : "expand_more" + color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface +@@ -194,7 +218,7 @@ StyledRect { + + Repeater { + model: ScriptModel { +- values: root.notifs.slice(0, Config.notifs.groupPreviewNum) ++ values: root.notifs.slice(0, root.Config.notifs.groupPreviewNum) + } + + NotifLine { +@@ -247,6 +271,7 @@ StyledRect { + } + + Loader { ++ asynchronous: true + Layout.fillWidth: true + + opacity: root.expanded ? 1 : 0 +@@ -256,7 +281,7 @@ StyledRect { + sourceComponent: ColumnLayout { + Repeater { + model: ScriptModel { +- values: root.notifs.slice(Config.notifs.groupPreviewNum) ++ values: root.notifs.slice(root.Config.notifs.groupPreviewNum) + } + + NotifLine {} +@@ -272,15 +297,14 @@ StyledRect { + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + component NotifLine: StyledText { + id: notifLine + +- required property Notifs.Notif modelData ++ required property NotifData modelData + + Layout.fillWidth: true + textFormat: Text.MarkdownText +diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml +index 0186c2f8..f4c61747 100644 +--- a/modules/lock/Pam.qml ++++ b/modules/lock/Pam.qml +@@ -1,9 +1,9 @@ +-import qs.config ++import QtQuick + import Quickshell + import Quickshell.Io + import Quickshell.Wayland + import Quickshell.Services.Pam +-import QtQuick ++import Caelestia.Config + + Scope { + id: root +@@ -31,8 +31,8 @@ Scope { + } else { + buffer = buffer.slice(0, -1); + } +- } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { +- // No illegal characters (you are insane if you use unicode in your password) ++ } else if (/^[^\x00-\x1F\x7F-\x9F]+$/.test(event.text)) { ++ // Allow anything except control characters + buffer += event.text; + } + } +@@ -82,7 +82,7 @@ Scope { + property int errorTries + + function checkAvail(): void { +- if (!available || !Config.lock.enableFprint || !root.lock.secure) { ++ if (!available || !GlobalConfig.lock.enableFprint || !root.lock.secure) { + abort(); + return; + } +@@ -113,7 +113,7 @@ Scope { + // Isn't actually the real max tries as pam only reports completed + // when max tries is reached. + tries++; +- if (tries < Config.lock.maxFprintTries) { ++ if (tries < GlobalConfig.lock.maxFprintTries) { + // Restart if not actually real max tries + root.fprintState = "fail"; + start(); +@@ -132,7 +132,7 @@ Scope { + id: availProc + + command: ["sh", "-c", "fprintd-list $USER"] +- onExited: code => { ++ onExited: code => { // qmllint disable signal-handler-parameters + fprint.available = code === 0; + fprint.checkAvail(); + } +@@ -166,8 +166,6 @@ Scope { + } + + Connections { +- target: root.lock +- + function onSecureChanged(): void { + if (root.lock.secure) { + availProc.running = true; +@@ -181,13 +179,15 @@ Scope { + function onUnlock(): void { + fprint.abort(); + } ++ ++ target: root.lock + } + + Connections { +- target: Config.lock +- + function onEnableFprintChanged(): void { + fprint.checkAvail(); + } ++ ++ target: GlobalConfig.lock + } + } +diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml +index 82c004c2..1f38aa00 100644 +--- a/modules/lock/Resources.qml ++++ b/modules/lock/Resources.qml +@@ -1,20 +1,20 @@ ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.misc + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + GridLayout { + id: root + + anchors.left: parent.left + anchors.right: parent.right +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- rowSpacing: Appearance.spacing.large +- columnSpacing: Appearance.spacing.large ++ rowSpacing: Tokens.spacing.large ++ columnSpacing: Tokens.spacing.large + rows: 2 + columns: 2 + +@@ -23,28 +23,28 @@ GridLayout { + } + + Resource { +- Layout.topMargin: Appearance.padding.large ++ Layout.topMargin: Tokens.padding.large + icon: "memory" + value: SystemUsage.cpuPerc + colour: Colours.palette.m3primary + } + + Resource { +- Layout.topMargin: Appearance.padding.large ++ Layout.topMargin: Tokens.padding.large + icon: "thermostat" + value: Math.min(1, SystemUsage.cpuTemp / 90) + colour: Colours.palette.m3secondary + } + + Resource { +- Layout.bottomMargin: Appearance.padding.large ++ Layout.bottomMargin: Tokens.padding.large + icon: "memory_alt" + value: SystemUsage.memPerc + colour: Colours.palette.m3secondary + } + + Resource { +- Layout.bottomMargin: Appearance.padding.large ++ Layout.bottomMargin: Tokens.padding.large + icon: "hard_disk" + value: SystemUsage.storagePerc + colour: Colours.palette.m3tertiary +@@ -61,17 +61,17 @@ GridLayout { + implicitHeight: width + + color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + + CircularProgress { + id: circ + + anchors.fill: parent + value: res.value +- padding: Appearance.padding.large * 3 ++ padding: Tokens.padding.large * 3 + fgColour: res.colour + bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) +- strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal ++ strokeWidth: width < 200 ? Tokens.padding.smaller : Tokens.padding.normal + } + + MaterialIcon { +@@ -86,7 +86,7 @@ GridLayout { + + Behavior on value { + Anim { +- duration: Appearance.anim.durations.large ++ type: Anim.StandardLarge + } + } + } +diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml +index d6c25af2..d96a54df 100644 +--- a/modules/lock/WeatherInfo.qml ++++ b/modules/lock/WeatherInfo.qml +@@ -1,11 +1,10 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import qs.utils + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root +@@ -14,13 +13,14 @@ ColumnLayout { + + anchors.left: parent.left + anchors.right: parent.right +- anchors.margins: Appearance.padding.large * 2 ++ anchors.margins: Tokens.padding.large * 2 + +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Loader { +- Layout.topMargin: Appearance.padding.large * 2 +- Layout.bottomMargin: -Appearance.padding.large ++ asynchronous: true ++ Layout.topMargin: Tokens.padding.large * 2 ++ Layout.bottomMargin: -Tokens.padding.large + Layout.alignment: Qt.AlignHCenter + + active: root.rootHeight > 610 +@@ -29,24 +29,24 @@ ColumnLayout { + sourceComponent: StyledText { + text: qsTr("Weather") + color: Colours.palette.m3primary +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + font.weight: 500 + } + } + + RowLayout { + Layout.fillWidth: true +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + MaterialIcon { + animate: true + text: Weather.icon + color: Colours.palette.m3secondary +- font.pointSize: Appearance.font.size.extraLarge * 2.5 ++ font.pointSize: Tokens.font.size.extraLarge * 2.5 + } + + ColumnLayout { +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + Layout.fillWidth: true +@@ -54,7 +54,7 @@ ColumnLayout { + animate: true + text: Weather.description + color: Colours.palette.m3secondary +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + elide: Text.ElideRight + } +@@ -65,18 +65,19 @@ ColumnLayout { + animate: true + text: qsTr("Humidity: %1%").arg(Weather.humidity) + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + elide: Text.ElideRight + } + } + + Loader { +- Layout.rightMargin: Appearance.padding.smaller ++ asynchronous: true ++ Layout.rightMargin: Tokens.padding.smaller + active: root.width > 400 + visible: active + + sourceComponent: ColumnLayout { +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + Layout.fillWidth: true +@@ -85,7 +86,7 @@ ColumnLayout { + text: Weather.temp + color: Colours.palette.m3primary + horizontalAlignment: Text.AlignRight +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + font.weight: 500 + elide: Text.ElideLeft + } +@@ -97,7 +98,7 @@ ColumnLayout { + text: qsTr("Feels like: %1").arg(Weather.feelsLike) + color: Colours.palette.m3outline + horizontalAlignment: Text.AlignRight +- font.pointSize: Appearance.font.size.smaller ++ font.pointSize: Tokens.font.size.smaller + elide: Text.ElideLeft + } + } +@@ -107,15 +108,16 @@ ColumnLayout { + Loader { + id: forecastLoader + +- Layout.topMargin: Appearance.spacing.smaller +- Layout.bottomMargin: Appearance.padding.large * 2 ++ asynchronous: true ++ Layout.topMargin: Tokens.spacing.smaller ++ Layout.bottomMargin: Tokens.padding.large * 2 + Layout.fillWidth: true + + active: root.rootHeight > 820 + visible: active + + sourceComponent: RowLayout { +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + Repeater { + model: { +@@ -135,7 +137,7 @@ ColumnLayout { + required property var modelData + + Layout.fillWidth: true +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + StyledText { + Layout.fillWidth: true +@@ -145,21 +147,21 @@ ColumnLayout { + } + color: Colours.palette.m3outline + horizontalAlignment: Text.AlignHCenter +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + } + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: forecastHour.modelData?.icon ?? "cloud_alert" +- font.pointSize: Appearance.font.size.extraLarge * 1.5 ++ font.pointSize: Tokens.font.size.extraLarge * 1.5 + font.weight: 500 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter +- text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` ++ text: GlobalConfig.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` + color: Colours.palette.m3secondary +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + } + } + } +diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml +deleted file mode 100644 +index a44cb19b..00000000 +--- a/modules/notifications/Background.qml ++++ /dev/null +@@ -1,54 +0,0 @@ +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Shapes +- +-ShapePath { +- id: root +- +- required property Wrapper wrapper +- required property var sidebar +- readonly property real rounding: Config.border.rounding +- readonly property bool flatten: wrapper.height < rounding * 2 +- readonly property real roundingY: flatten ? wrapper.height / 2 : rounding +- +- strokeWidth: -1 +- fillColor: Colours.palette.m3surface +- +- PathLine { +- relativeX: -(root.wrapper.width + root.rounding) +- relativeY: 0 +- } +- PathArc { +- relativeX: root.rounding +- relativeY: root.roundingY +- radiusX: root.rounding +- radiusY: Math.min(root.rounding, root.wrapper.height) +- } +- PathLine { +- relativeX: 0 +- relativeY: root.wrapper.height - root.roundingY * 2 +- } +- PathArc { +- relativeX: root.sidebar.notifsRoundingX +- relativeY: root.roundingY +- radiusX: root.sidebar.notifsRoundingX +- radiusY: Math.min(root.rounding, root.wrapper.height) +- direction: PathArc.Counterclockwise +- } +- PathLine { +- relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width +- relativeY: 0 +- } +- PathArc { +- relativeX: root.rounding +- relativeY: root.rounding +- radiusX: root.rounding +- radiusY: root.rounding +- } +- +- Behavior on fillColor { +- CAnim {} +- } +-} +diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml +index 2d4590e0..aeaa7dea 100644 +--- a/modules/notifications/Content.qml ++++ b/modules/notifications/Content.qml +@@ -1,47 +1,47 @@ ++import QtQuick ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components + import qs.components.containers + import qs.components.widgets + import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Widgets +-import QtQuick + + Item { + id: root + +- required property PersistentProperties visibilities +- required property Item panels +- readonly property int padding: Appearance.padding.large ++ required property DrawerVisibilities visibilities ++ required property Item osdPanel ++ required property Item sessionPanel ++ readonly property int padding: Tokens.padding.large + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.right: parent.right + +- implicitWidth: Config.notifs.sizes.width + padding * 2 ++ implicitWidth: Tokens.sizes.notifs.width + padding * 2 + implicitHeight: { + const count = list.count; + if (count === 0) + return 0; + +- let height = (count - 1) * Appearance.spacing.smaller; ++ let height = (count - 1) * Tokens.spacing.smaller; + for (let i = 0; i < count; i++) +- height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; ++ height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; + +- if (visibilities && panels) { +- if (visibilities.osd) { +- const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; +- if (height > h) +- height = h; +- } ++ if (visibilities.osd) { ++ const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; ++ if (height > h) ++ height = h; ++ } + +- if (visibilities.session) { +- const h = panels.session.y - Config.border.rounding * 2 - padding * 2; +- if (height > h) +- height = h; +- } ++ if (visibilities.session) { ++ const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; ++ if (height > h) ++ height = h; + } + +- return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); ++ return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + } + + ClippingWrapperRectangle { +@@ -49,7 +49,7 @@ Item { + anchors.margins: root.padding + + color: "transparent" +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + StyledListView { + id: list +@@ -62,79 +62,9 @@ Item { + + orientation: Qt.Vertical + spacing: 0 +- cacheBuffer: QsWindow.window?.screen.height ?? 0 +- +- delegate: Item { +- id: wrapper +- +- required property Notifs.Notif modelData +- required property int index +- readonly property alias nonAnimHeight: notif.nonAnimHeight +- property int idx +- +- onIndexChanged: { +- if (index !== -1) +- idx = index; +- } +- +- implicitWidth: notif.implicitWidth +- implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) +- +- ListView.onRemove: removeAnim.start() +- +- SequentialAnimation { +- id: removeAnim +- +- PropertyAction { +- target: wrapper +- property: "ListView.delayRemove" +- value: true +- } +- PropertyAction { +- target: wrapper +- property: "enabled" +- value: false +- } +- PropertyAction { +- target: wrapper +- property: "implicitHeight" +- value: 0 +- } +- PropertyAction { +- target: wrapper +- property: "z" +- value: 1 +- } +- Anim { +- target: notif +- property: "x" +- to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 +- duration: Appearance.anim.durations.normal +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- PropertyAction { +- target: wrapper +- property: "ListView.delayRemove" +- value: false +- } +- } +- +- ClippingRectangle { +- anchors.top: parent.top +- anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller +- +- color: "transparent" +- radius: notif.radius +- implicitWidth: notif.implicitWidth +- implicitHeight: notif.implicitHeight ++ cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0 + +- Notification { +- id: notif +- +- modelData: wrapper.modelData +- } +- } +- } ++ delegate: NotifWrapper {} + + move: Transition { + Anim { +@@ -159,9 +89,9 @@ Item { + + let height = 0; + for (let i = 0; i < count; i++) { +- height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; ++ height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; + +- if (height - Appearance.spacing.smaller >= scrollY) ++ if (height - Tokens.spacing.smaller >= scrollY) + return i; + } + +@@ -180,9 +110,9 @@ Item { + + let height = 0; + for (let i = count - 1; i >= 0; i--) { +- height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; ++ height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; + +- if (height - Appearance.spacing.smaller >= scrollY) ++ if (height - Tokens.spacing.smaller >= scrollY) + return count - i - 1; + } + +@@ -196,9 +126,80 @@ Item { + Anim {} + } + ++ component NotifWrapper: Item { ++ id: wrapper ++ ++ required property NotifData modelData ++ required property int index ++ readonly property alias nonAnimHeight: notif.nonAnimHeight ++ property int idx ++ ++ onIndexChanged: { ++ if (index !== -1) ++ idx = index; ++ } ++ ++ implicitWidth: notif.implicitWidth ++ implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.smaller) ++ ++ ListView.onRemove: removeAnim.start() ++ ++ SequentialAnimation { ++ id: removeAnim ++ ++ PropertyAction { ++ target: wrapper ++ property: "ListView.delayRemove" ++ value: true ++ } ++ PropertyAction { ++ target: wrapper ++ property: "enabled" ++ value: false ++ } ++ PropertyAction { ++ target: wrapper ++ property: "implicitHeight" ++ value: 0 ++ } ++ PropertyAction { ++ target: wrapper ++ property: "z" ++ value: 1 ++ } ++ Anim { ++ target: notif ++ property: "x" ++ to: (notif.x >= 0 ? wrapper.Tokens.sizes.notifs.width : -wrapper.Tokens.sizes.notifs.width) * 2 ++ duration: Tokens.anim.durations.normal ++ easing: Tokens.anim.emphasized ++ } ++ PropertyAction { ++ target: wrapper ++ property: "ListView.delayRemove" ++ value: false ++ } ++ } ++ ++ ClippingRectangle { ++ anchors.top: parent.top ++ anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.smaller ++ ++ color: "transparent" ++ radius: notif.radius ++ implicitWidth: notif.implicitWidth ++ implicitHeight: notif.implicitHeight ++ ++ Notification { ++ id: notif ++ ++ modelData: wrapper.modelData ++ } ++ } ++ } ++ + component Anim: NumberAnimation { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ duration: Tokens.anim.durations.expressiveDefaultSpatial ++ easing: Tokens.anim.expressiveDefaultSpatial + } + } +diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml +index 8c2d3ec2..fe6fd056 100644 +--- a/modules/notifications/Notification.qml ++++ b/modules/notifications/Notification.qml +@@ -1,31 +1,32 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import QtQuick.Shapes ++import Quickshell ++import Quickshell.Services.Notifications ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import Quickshell.Widgets +-import Quickshell.Services.Notifications +-import QtQuick +-import QtQuick.Layouts + + StyledRect { + id: root + +- required property Notifs.Notif modelData ++ required property NotifData modelData + readonly property bool hasImage: modelData.image.length > 0 + readonly property bool hasAppIcon: modelData.appIcon.length > 0 ++ readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText + readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 + property bool expanded: Config.notifs.openExpanded + + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.normal +- implicitWidth: Config.notifs.sizes.width ++ radius: Tokens.rounding.normal ++ implicitWidth: Tokens.sizes.notifs.width + implicitHeight: inner.implicitHeight + +- x: Config.notifs.sizes.width ++ x: Tokens.sizes.notifs.width + Component.onCompleted: { + x = 0; + modelData.lock(this); +@@ -34,7 +35,7 @@ StyledRect { + + Behavior on x { + Anim { +- easing.bezierCurve: Appearance.anim.curves.emphasizedDecel ++ easing: Tokens.anim.emphasizedDecel + } + } + +@@ -66,7 +67,7 @@ StyledRect { + if (!containsMouse) + root.modelData.timer.start(); + +- if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) ++ if (Math.abs(root.x) < Tokens.sizes.notifs.width * Config.notifs.clearThreshold) + root.x = 0; + else + root.modelData.popup = false; +@@ -79,11 +80,11 @@ StyledRect { + } + } + onClicked: event => { +- if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) ++ if (!GlobalConfig.notifs.actionOnClick || event.button !== Qt.LeftButton) + return; + + const actions = root.modelData.actions; +- if (actions?.length === 1) ++ if (actions.length === 1) + actions[0].invoke(); + } + +@@ -93,37 +94,42 @@ StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + implicitHeight: root.nonAnimHeight + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + Loader { + id: image + ++ asynchronous: true + active: root.hasImage + + anchors.left: parent.left + anchors.top: parent.top +- width: Config.notifs.sizes.image +- height: Config.notifs.sizes.image ++ width: TokenConfig.sizes.notifs.image ++ height: TokenConfig.sizes.notifs.image + visible: root.hasImage || root.hasAppIcon + +- sourceComponent: ClippingRectangle { +- radius: Appearance.rounding.full +- implicitWidth: Config.notifs.sizes.image +- implicitHeight: Config.notifs.sizes.image ++ sourceComponent: StyledClippingRect { ++ radius: Tokens.rounding.full ++ color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer ++ implicitWidth: TokenConfig.sizes.notifs.image ++ implicitHeight: TokenConfig.sizes.notifs.image + + Image { + anchors.fill: parent + source: Qt.resolvedUrl(root.modelData.image) + fillMode: Image.PreserveAspectCrop ++ sourceSize: { ++ const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); ++ return Qt.size(size, size); ++ } + cache: false + asynchronous: true + } +@@ -133,6 +139,7 @@ StyledRect { + Loader { + id: appIcon + ++ asynchronous: true + active: root.hasAppIcon || !root.hasImage + + anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter +@@ -141,14 +148,15 @@ StyledRect { + anchors.bottom: root.hasImage ? image.bottom : undefined + + sourceComponent: StyledRect { +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer +- implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image +- implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image ++ implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image ++ implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image + + Loader { + id: icon + ++ asynchronous: true + active: root.hasAppIcon + + anchors.centerIn: parent +@@ -165,16 +173,53 @@ StyledRect { + } + + Loader { ++ asynchronous: true + active: !root.hasAppIcon + anchors.centerIn: parent +- anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 +- anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 ++ anchors.horizontalCenterOffset: -Tokens.font.size.large * 0.02 ++ anchors.verticalCenterOffset: Tokens.font.size.large * 0.02 + + sourceComponent: MaterialIcon { + text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) + + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large ++ } ++ } ++ } ++ } ++ ++ Shape { ++ id: progressIndicator ++ ++ anchors.centerIn: appIcon ++ width: appIcon.implicitWidth + progressShape.strokeWidth * 2 ++ height: appIcon.implicitHeight + progressShape.strokeWidth * 2 ++ preferredRendererType: Shape.CurveRenderer ++ ++ ShapePath { ++ id: progressShape ++ ++ capStyle: ShapePath.RoundCap ++ fillColor: "transparent" ++ strokeWidth: 2 ++ strokeColor: Colours.palette.m3primary ++ ++ PathAngleArc { ++ id: progressArc ++ ++ radiusX: progressIndicator.width / 2 - root.Tokens.padding.small / 2 ++ centerX: progressIndicator.width / 2 ++ radiusY: progressIndicator.height / 2 - root.Tokens.padding.small / 2 ++ centerY: progressIndicator.height / 2 ++ ++ startAngle: -90 ++ sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360 ++ ++ Behavior on sweepAngle { ++ Anim { ++ easing: Tokens.anim.emphasizedDecel ++ } + } + } + } +@@ -185,13 +230,13 @@ StyledRect { + + anchors.top: parent.top + anchors.left: image.right +- anchors.leftMargin: Appearance.spacing.smaller ++ anchors.leftMargin: Tokens.spacing.smaller + + animate: true + text: appNameMetrics.elidedText + maximumLineCount: 1 + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + + opacity: root.expanded ? 1 : 0 + +@@ -207,7 +252,7 @@ StyledRect { + font.family: appName.font.family + font.pointSize: appName.font.pointSize + elide: Text.ElideRight +- elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 ++ elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 + } + + StyledText { +@@ -215,7 +260,7 @@ StyledRect { + + anchors.top: parent.top + anchors.left: image.right +- anchors.leftMargin: Appearance.spacing.smaller ++ anchors.leftMargin: Tokens.spacing.smaller + + animate: true + text: summaryMetrics.elidedText +@@ -241,10 +286,8 @@ StyledRect { + target: summary + property: "maximumLineCount" + } +- AnchorAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard ++ AnchorAnim { ++ type: AnchorAnim.Standard + } + } + +@@ -260,7 +303,7 @@ StyledRect { + font.family: summary.font.family + font.pointSize: summary.font.pointSize + elide: Text.ElideRight +- elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 ++ elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 + } + + StyledText { +@@ -268,11 +311,11 @@ StyledRect { + + anchors.top: parent.top + anchors.left: summary.right +- anchors.leftMargin: Appearance.spacing.small ++ anchors.leftMargin: Tokens.spacing.small + + text: "•" + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + + states: State { + name: "expanded" +@@ -285,10 +328,8 @@ StyledRect { + } + + transitions: Transition { +- AnchorAnimation { +- duration: Appearance.anim.durations.normal +- easing.type: Easing.BezierSpline +- easing.bezierCurve: Appearance.anim.curves.standard ++ AnchorAnim { ++ type: AnchorAnim.Standard + } + } + } +@@ -298,13 +339,13 @@ StyledRect { + + anchors.top: parent.top + anchors.left: timeSep.right +- anchors.leftMargin: Appearance.spacing.small ++ anchors.leftMargin: Tokens.spacing.small + + animate: true + horizontalAlignment: Text.AlignLeft + text: root.modelData.timeStr + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + Item { +@@ -317,12 +358,9 @@ StyledRect { + implicitHeight: expandIcon.height + + StateLayer { +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- +- function onClicked() { +- root.expanded = !root.expanded; +- } ++ onClicked: root.expanded = !root.expanded + } + + MaterialIcon { +@@ -332,7 +370,7 @@ StyledRect { + + animate: true + text: root.expanded ? "expand_less" : "expand_more" +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + } + +@@ -342,13 +380,13 @@ StyledRect { + anchors.left: summary.left + anchors.right: expandBtn.left + anchors.top: summary.bottom +- anchors.rightMargin: Appearance.spacing.small ++ anchors.rightMargin: Tokens.spacing.small + + animate: true +- textFormat: Text.MarkdownText ++ textFormat: root.bodyTextFormat + text: bodyPreviewMetrics.elidedText + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + + opacity: root.expanded ? 0 : 1 + +@@ -373,13 +411,13 @@ StyledRect { + anchors.left: summary.left + anchors.right: expandBtn.left + anchors.top: summary.bottom +- anchors.rightMargin: Appearance.spacing.small ++ anchors.rightMargin: Tokens.spacing.small + + animate: true +- textFormat: Text.MarkdownText ++ textFormat: root.bodyTextFormat + text: root.modelData.body + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + height: text ? implicitHeight : 0 + +@@ -403,9 +441,9 @@ StyledRect { + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: body.bottom +- anchors.topMargin: Appearance.spacing.small ++ anchors.topMargin: Tokens.spacing.small + +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + opacity: root.expanded ? 1 : 0 + +@@ -416,6 +454,7 @@ StyledRect { + Action { + modelData: QtObject { + readonly property string text: qsTr("Close") ++ + function invoke(): void { + root.modelData.close(); + } +@@ -438,21 +477,18 @@ StyledRect { + + required property var modelData + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + +- Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 +- Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 +- implicitWidth: actionText.width + Appearance.padding.normal * 2 +- implicitHeight: actionText.height + Appearance.padding.small * 2 ++ Layout.preferredWidth: actionText.width + Tokens.padding.normal * 2 ++ Layout.preferredHeight: actionText.height + Tokens.padding.small * 2 ++ implicitWidth: actionText.width + Tokens.padding.normal * 2 ++ implicitHeight: actionText.height + Tokens.padding.small * 2 + + StateLayer { +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface +- +- function onClicked(): void { +- action.modelData.invoke(); +- } ++ onClicked: action.modelData.invoke() + } + + StyledText { +@@ -461,7 +497,7 @@ StyledRect { + anchors.centerIn: parent + text: actionTextMetrics.elidedText + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + TextMetrics { +@@ -473,7 +509,7 @@ StyledRect { + elide: Text.ElideRight + elideWidth: { + const numActions = root.modelData.actions.length + 1; +- return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2; ++ return (inner.width - actions.spacing * (numActions - 1)) / numActions - root.Tokens.padding.normal * 2; + } + } + } +diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml +index 61acc56e..97417e9c 100644 +--- a/modules/notifications/Wrapper.qml ++++ b/modules/notifications/Wrapper.qml +@@ -1,39 +1,22 @@ +-import qs.components +-import qs.config + import QtQuick ++import qs.components + + Item { + id: root + +- required property var visibilities +- required property Item panels ++ required property DrawerVisibilities visibilities ++ required property Item sidebarPanel ++ property alias osdPanel: content.osdPanel ++ property alias sessionPanel: content.sessionPanel + + visible: height > 0 +- implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) ++ anchors.topMargin: -5 ++ implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) + implicitHeight: content.implicitHeight + +- states: State { +- name: "hidden" +- when: root.visibilities.sidebar && Config.sidebar.enabled +- +- PropertyChanges { +- root.implicitHeight: 0 +- } +- } +- +- transitions: Transition { +- Anim { +- target: root +- property: "implicitHeight" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- } +- + Content { + id: content + + visibilities: root.visibilities +- panels: root.panels + } + } +diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml +deleted file mode 100644 +index 78955c7a..00000000 +--- a/modules/osd/Background.qml ++++ /dev/null +@@ -1,60 +0,0 @@ +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Shapes +- +-ShapePath { +- id: root +- +- required property Wrapper wrapper +- readonly property real rounding: Config.border.rounding +- readonly property bool flatten: wrapper.width < rounding * 2 +- readonly property real roundingX: flatten ? wrapper.width / 2 : rounding +- +- strokeWidth: -1 +- fillColor: Colours.palette.m3surface +- +- PathArc { +- relativeX: -root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- } +- PathLine { +- relativeX: -(root.wrapper.width - root.roundingX * 2) +- relativeY: 0 +- } +- PathArc { +- relativeX: -root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- direction: PathArc.Counterclockwise +- } +- PathLine { +- relativeX: 0 +- relativeY: root.wrapper.height - root.rounding * 2 +- } +- PathArc { +- relativeX: root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- direction: PathArc.Counterclockwise +- } +- PathLine { +- relativeX: root.wrapper.width - root.roundingX * 2 +- relativeY: 0 +- } +- PathArc { +- relativeX: root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- } +- +- Behavior on fillColor { +- CAnim {} +- } +-} +diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml +index 770fb696..3ad3ef88 100644 +--- a/modules/osd/Content.qml ++++ b/modules/osd/Content.qml +@@ -1,18 +1,18 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config + import qs.utils +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root + + required property Brightness.Monitor monitor +- required property var visibilities ++ required property DrawerVisibilities visibilities + + required property real volume + required property bool muted +@@ -20,20 +20,17 @@ Item { + required property bool sourceMuted + required property real brightness + +- implicitWidth: layout.implicitWidth + Appearance.padding.large * 2 +- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 ++ implicitWidth: layout.implicitWidth + Tokens.padding.large * 2 ++ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + + ColumnLayout { + id: layout + + anchors.centerIn: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + // Speaker volume + CustomMouseArea { +- implicitWidth: Config.osd.sizes.sliderWidth +- implicitHeight: Config.osd.sizes.sliderHeight +- + function onWheel(event: WheelEvent) { + if (event.angleDelta.y > 0) + Audio.incrementVolume(); +@@ -41,12 +38,15 @@ Item { + Audio.decrementVolume(); + } + ++ implicitWidth: Tokens.sizes.osd.sliderWidth ++ implicitHeight: Tokens.sizes.osd.sliderHeight ++ + FilledSlider { + anchors.fill: parent + + icon: Icons.getVolumeIcon(value, root.muted) + value: root.volume +- to: Config.services.maxVolume ++ to: GlobalConfig.services.maxVolume + onMoved: Audio.setVolume(value) + } + } +@@ -56,9 +56,6 @@ Item { + shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) + + sourceComponent: CustomMouseArea { +- implicitWidth: Config.osd.sizes.sliderWidth +- implicitHeight: Config.osd.sizes.sliderHeight +- + function onWheel(event: WheelEvent) { + if (event.angleDelta.y > 0) + Audio.incrementSourceVolume(); +@@ -66,12 +63,15 @@ Item { + Audio.decrementSourceVolume(); + } + ++ implicitWidth: Tokens.sizes.osd.sliderWidth ++ implicitHeight: Tokens.sizes.osd.sliderHeight ++ + FilledSlider { + anchors.fill: parent + + icon: Icons.getMicVolumeIcon(value, root.sourceMuted) + value: root.sourceVolume +- to: Config.services.maxVolume ++ to: GlobalConfig.services.maxVolume + onMoved: Audio.setSourceVolume(value) + } + } +@@ -82,19 +82,19 @@ Item { + shouldBeActive: Config.osd.enableBrightness + + sourceComponent: CustomMouseArea { +- implicitWidth: Config.osd.sizes.sliderWidth +- implicitHeight: Config.osd.sizes.sliderHeight +- + function onWheel(event: WheelEvent) { + const monitor = root.monitor; + if (!monitor) + return; + if (event.angleDelta.y > 0) +- monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); ++ monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); + else if (event.angleDelta.y < 0) +- monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); ++ monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); + } + ++ implicitWidth: Tokens.sizes.osd.sliderWidth ++ implicitHeight: Tokens.sizes.osd.sliderHeight ++ + FilledSlider { + anchors.fill: parent + +@@ -109,14 +109,15 @@ Item { + component WrappedLoader: Loader { + required property bool shouldBeActive + +- Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 ++ asynchronous: true ++ Layout.preferredHeight: shouldBeActive ? Tokens.sizes.osd.sliderHeight : 0 + opacity: shouldBeActive ? 1 : 0 + active: opacity > 0 + visible: active + + Behavior on Layout.preferredHeight { + Anim { +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ type: Anim.Emphasized + } + } + +diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml +index 2519609d..3ea0e1a3 100644 +--- a/modules/osd/Wrapper.qml ++++ b/modules/osd/Wrapper.qml +@@ -1,19 +1,23 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell +-import QtQuick + + Item { + id: root + + required property ShellScreen screen +- required property var visibilities ++ required property DrawerVisibilities visibilities ++ required property bool sidebarOrSessionVisible ++ + property bool hovered + readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) + readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) ++ property real offsetScale: shouldBeActive ? 0 : 1 ++ property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0 + + property real volume + property bool muted +@@ -34,45 +38,19 @@ Item { + brightness = root.monitor?.brightness ?? 0; + } + +- visible: width > 0 +- implicitWidth: 0 ++ visible: offsetScale < 1 ++ anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale ++ implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight ++ opacity: 1 - offsetScale + +- states: State { +- name: "visible" +- when: root.shouldBeActive +- +- PropertyChanges { +- root.implicitWidth: content.implicitWidth ++ Behavior on offsetScale { ++ Anim { ++ type: Anim.DefaultSpatial + } + } + +- transitions: [ +- Transition { +- from: "" +- to: "visible" +- +- Anim { +- target: root +- property: "implicitWidth" +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- }, +- Transition { +- from: "visible" +- to: "" +- +- Anim { +- target: root +- property: "implicitWidth" +- easing.bezierCurve: Appearance.anim.curves.emphasized +- } +- } +- ] +- + Connections { +- target: Audio +- + function onMutedChanged(): void { + root.show(); + root.muted = Audio.muted; +@@ -92,21 +70,23 @@ Item { + root.show(); + root.sourceVolume = Audio.sourceVolume; + } ++ ++ target: Audio + } + + Connections { +- target: root.monitor +- + function onBrightnessChanged(): void { + root.show(); + root.brightness = root.monitor?.brightness ?? 0; + } ++ ++ target: root.monitor + } + + Timer { + id: timer + +- interval: Config.osd.hideDelay ++ interval: root.Config.osd.hideDelay + onTriggered: { + if (!root.hovered) + root.visibilities.osd = false; +@@ -119,7 +99,8 @@ Item { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + +- Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) ++ asynchronous: true ++ active: root.shouldBeActive || root.visible + + sourceComponent: Content { + monitor: root.monitor +diff --git a/modules/session/Background.qml b/modules/session/Background.qml +deleted file mode 100644 +index 78955c7a..00000000 +--- a/modules/session/Background.qml ++++ /dev/null +@@ -1,60 +0,0 @@ +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Shapes +- +-ShapePath { +- id: root +- +- required property Wrapper wrapper +- readonly property real rounding: Config.border.rounding +- readonly property bool flatten: wrapper.width < rounding * 2 +- readonly property real roundingX: flatten ? wrapper.width / 2 : rounding +- +- strokeWidth: -1 +- fillColor: Colours.palette.m3surface +- +- PathArc { +- relativeX: -root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- } +- PathLine { +- relativeX: -(root.wrapper.width - root.roundingX * 2) +- relativeY: 0 +- } +- PathArc { +- relativeX: -root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- direction: PathArc.Counterclockwise +- } +- PathLine { +- relativeX: 0 +- relativeY: root.wrapper.height - root.rounding * 2 +- } +- PathArc { +- relativeX: root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- direction: PathArc.Counterclockwise +- } +- PathLine { +- relativeX: root.wrapper.width - root.roundingX * 2 +- relativeY: 0 +- } +- PathArc { +- relativeX: root.roundingX +- relativeY: root.rounding +- radiusX: Math.min(root.rounding, root.wrapper.width) +- radiusY: root.rounding +- } +- +- Behavior on fillColor { +- CAnim {} +- } +-} +diff --git a/modules/session/Content.qml b/modules/session/Content.qml +index 6c56d442..e1ba2809 100644 +--- a/modules/session/Content.qml ++++ b/modules/session/Content.qml +@@ -1,24 +1,24 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import QtQuick + + Column { + id: root + +- required property PersistentProperties visibilities ++ required property DrawerVisibilities visibilities + +- padding: Appearance.padding.large +- spacing: Appearance.spacing.large ++ padding: Tokens.padding.large ++ spacing: Tokens.spacing.large + + SessionButton { + id: logout + +- icon: "logout" ++ icon: Config.session.icons.logout + command: Config.session.commands.logout + + KeyNavigation.down: shutdown +@@ -26,19 +26,19 @@ Column { + Component.onCompleted: forceActiveFocus() + + Connections { +- target: root.visibilities +- + function onLauncherChanged(): void { + if (!root.visibilities.launcher) + logout.forceActiveFocus(); + } ++ ++ target: root.visibilities + } + } + + SessionButton { + id: shutdown + +- icon: "power_settings_new" ++ icon: Config.session.icons.shutdown + command: Config.session.commands.shutdown + + KeyNavigation.up: logout +@@ -46,21 +46,21 @@ Column { + } + + AnimatedImage { +- width: Config.session.sizes.button +- height: Config.session.sizes.button +- sourceSize.width: width +- sourceSize.height: height ++ width: Tokens.sizes.session.button ++ height: Tokens.sizes.session.button ++ sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) + + playing: visible + asynchronous: true +- speed: 0.7 ++ speed: Config.general.sessionGifSpeed + source: Paths.absolutePath(Config.paths.sessionGif) ++ fillMode: AnimatedImage.PreserveAspectFit + } + + SessionButton { + id: hibernate + +- icon: "downloading" ++ icon: Config.session.icons.hibernate + command: Config.session.commands.hibernate + + KeyNavigation.up: shutdown +@@ -70,7 +70,7 @@ Column { + SessionButton { + id: reboot + +- icon: "cached" ++ icon: Config.session.icons.reboot + command: Config.session.commands.reboot + + KeyNavigation.up: hibernate +@@ -82,10 +82,10 @@ Column { + required property string icon + required property list command + +- implicitWidth: Config.session.sizes.button +- implicitHeight: Config.session.sizes.button ++ implicitWidth: Tokens.sizes.session.button ++ implicitHeight: Tokens.sizes.session.button + +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer + + Keys.onEnterPressed: Quickshell.execDetached(button.command) +@@ -96,10 +96,10 @@ Column { + return; + + if (event.modifiers & Qt.ControlModifier) { +- if (event.key === Qt.Key_J && KeyNavigation.down) { ++ if ((event.key === Qt.Key_J || event.key === Qt.Key_N) && KeyNavigation.down) { + KeyNavigation.down.focus = true; + event.accepted = true; +- } else if (event.key === Qt.Key_K && KeyNavigation.up) { ++ } else if ((event.key === Qt.Key_K || event.key === Qt.Key_P) && KeyNavigation.up) { + KeyNavigation.up.focus = true; + event.accepted = true; + } +@@ -117,10 +117,7 @@ Column { + StateLayer { + radius: parent.radius + color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- +- function onClicked(): void { +- Quickshell.execDetached(button.command); +- } ++ onClicked: Quickshell.execDetached(button.command) + } + + MaterialIcon { +@@ -128,7 +125,7 @@ Column { + + text: button.icon + color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + font.weight: 500 + } + } +diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml +index 14b03a80..41d9ba1a 100644 +--- a/modules/session/Wrapper.qml ++++ b/modules/session/Wrapper.qml +@@ -1,60 +1,39 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config +-import Quickshell + import QtQuick ++import Caelestia.Config ++import qs.components + + Item { + id: root + +- required property PersistentProperties visibilities +- required property var panels ++ required property DrawerVisibilities visibilities ++ required property bool sidebarVisible + readonly property real nonAnimWidth: content.implicitWidth + +- visible: width > 0 +- implicitWidth: 0 +- implicitHeight: content.implicitHeight ++ readonly property bool shouldBeActive: visibilities.session && Config.session.enabled ++ property real offsetScale: shouldBeActive ? 0 : 1 ++ property real sidebarOffset: sidebarVisible ? 14 : 0 + +- states: State { +- name: "visible" +- when: root.visibilities.session && Config.session.enabled ++ visible: offsetScale < 1 ++ anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale ++ implicitWidth: content.implicitWidth ++ implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open ++ opacity: 1 - offsetScale + +- PropertyChanges { +- root.implicitWidth: root.nonAnimWidth ++ Behavior on offsetScale { ++ Anim { ++ type: Anim.DefaultSpatial + } + } + +- transitions: [ +- Transition { +- from: "" +- to: "visible" +- +- Anim { +- target: root +- property: "implicitWidth" +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- }, +- Transition { +- from: "visible" +- to: "" +- +- Anim { +- target: root +- property: "implicitWidth" +- easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized +- } +- } +- ] +- + Loader { + id: content + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + +- Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible) ++ active: root.shouldBeActive || root.visible + + sourceComponent: Content { + visibilities: root.visibilities +diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml +deleted file mode 100644 +index beefdf5c..00000000 +--- a/modules/sidebar/Background.qml ++++ /dev/null +@@ -1,52 +0,0 @@ +-import qs.components +-import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Shapes +- +-ShapePath { +- id: root +- +- required property Wrapper wrapper +- required property var panels +- +- readonly property real rounding: Config.border.rounding +- +- readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width +- readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding +- +- readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width +- readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding +- +- strokeWidth: -1 +- fillColor: Colours.palette.m3surface +- +- PathLine { +- relativeX: -root.wrapper.width - root.notifsRoundingX +- relativeY: 0 +- } +- PathArc { +- relativeX: root.notifsRoundingX +- relativeY: root.rounding +- radiusX: root.notifsRoundingX +- radiusY: root.rounding +- } +- PathLine { +- relativeX: 0 +- relativeY: root.wrapper.height - root.rounding * 2 +- } +- PathArc { +- relativeX: -root.utilsRoundingX +- relativeY: root.rounding +- radiusX: root.utilsRoundingX +- radiusY: root.rounding +- } +- PathLine { +- relativeX: root.wrapper.width + root.utilsRoundingX +- relativeY: 0 +- } +- +- Behavior on fillColor { +- CAnim {} +- } +-} +diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml +index 1b7feed6..f6b8eb05 100644 +--- a/modules/sidebar/Content.qml ++++ b/modules/sidebar/Content.qml +@@ -1,26 +1,26 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root + + required property Props props +- required property var visibilities ++ required property DrawerVisibilities visibilities + + ColumnLayout { + id: layout + + anchors.fill: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledRect { + Layout.fillWidth: true + Layout.fillHeight: true + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainerLow + + NotifDock { +@@ -30,7 +30,7 @@ Item { + } + + StyledRect { +- Layout.topMargin: Appearance.padding.large - layout.spacing ++ Layout.topMargin: Tokens.padding.large - layout.spacing + Layout.fillWidth: true + implicitHeight: 1 + +diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml +index 5a317640..2a390926 100644 +--- a/modules/sidebar/Notif.qml ++++ b/modules/sidebar/Notif.qml +@@ -1,42 +1,43 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import Quickshell + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.services + + StyledRect { + id: root + +- required property Notifs.Notif modelData ++ required property NotifData modelData + required property Props props + required property bool expanded +- required property var visibilities ++ required property DrawerVisibilities visibilities + +- readonly property StyledText body: expandedContent.item?.body ?? null +- readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height ++ readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null ++ readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.normal * 2 : summaryHeightMetrics.height + + implicitHeight: nonAnimHeight + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: { +- const c = root.modelData.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); ++ const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); + return expanded ? c : Qt.alpha(c, 0); + } + ++ state: expanded ? "expanded" : "" ++ + states: State { + name: "expanded" +- when: root.expanded + + PropertyChanges { +- summary.anchors.margins: Appearance.padding.normal +- dummySummary.anchors.margins: Appearance.padding.normal +- compactBody.anchors.margins: Appearance.padding.normal +- timeStr.anchors.margins: Appearance.padding.normal +- expandedContent.anchors.margins: Appearance.padding.normal +- summary.width: root.width - Appearance.padding.normal * 2 - timeStr.implicitWidth - Appearance.spacing.small ++ summary.anchors.margins: root.Tokens.padding.normal ++ dummySummary.anchors.margins: root.Tokens.padding.normal ++ compactBody.anchors.margins: root.Tokens.padding.normal ++ timeStr.anchors.margins: root.Tokens.padding.normal ++ expandedContent.anchors.margins: root.Tokens.padding.normal ++ summary.width: root.width - root.Tokens.padding.normal * 2 - timeStr.implicitWidth - root.Tokens.spacing.small + summary.maximumLineCount: Number.MAX_SAFE_INTEGER + } + } +@@ -61,8 +62,8 @@ StyledRect { + anchors.left: parent.left + + width: parent.width +- text: root.modelData.summary +- color: root.modelData.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface ++ text: root.modelData?.summary ?? "" ++ color: root.modelData?.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + elide: Text.ElideRight + wrapMode: Text.WordWrap + maximumLineCount: 1 +@@ -75,7 +76,7 @@ StyledRect { + anchors.left: parent.left + + visible: false +- text: root.modelData.summary ++ text: root.modelData?.summary ?? "" + } + + WrappedLoader { +@@ -85,11 +86,11 @@ StyledRect { + anchors.top: parent.top + anchors.left: dummySummary.right + anchors.right: parent.right +- anchors.leftMargin: Appearance.spacing.small ++ anchors.leftMargin: Tokens.spacing.small + + sourceComponent: StyledText { +- text: root.modelData.body.replace(/\n/g, " ") +- color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline ++ text: String(root.modelData?.body ?? "").replace(/\n/g, " ") ++ color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + elide: Text.ElideRight + } + } +@@ -103,9 +104,9 @@ StyledRect { + + sourceComponent: StyledText { + animate: true +- text: root.modelData.timeStr ++ text: root.modelData?.timeStr ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + } + +@@ -116,49 +117,88 @@ StyledRect { + anchors.top: summary.bottom + anchors.left: parent.left + anchors.right: parent.right +- anchors.topMargin: Appearance.spacing.small / 2 ++ anchors.topMargin: Tokens.spacing.small / 2 + +- sourceComponent: ColumnLayout { +- readonly property alias body: body ++ sourceComponent: ExpandedBody {} ++ } + +- spacing: Appearance.spacing.smaller ++ Behavior on implicitHeight { ++ Anim { ++ type: Anim.DefaultSpatial ++ } ++ } + +- StyledText { +- id: body ++ component ExpandedBody: ColumnLayout { ++ readonly property alias body: bodyText + +- Layout.fillWidth: true +- textFormat: Text.MarkdownText +- text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") +- color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline +- wrapMode: Text.WordWrap ++ spacing: Tokens.spacing.smaller + +- onLinkActivated: link => { +- Quickshell.execDetached(["app2unit", "-O", "--", link]); +- root.visibilities.sidebar = false; +- } +- } ++ StyledText { ++ id: bodyText ++ ++ Layout.fillWidth: true ++ textFormat: Text.MarkdownText ++ text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") ++ color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline ++ wrapMode: Text.WordWrap + +- NotifActionList { +- notif: root.modelData ++ onLinkActivated: link => { ++ Quickshell.execDetached(["app2unit", "-O", "--", link]); ++ root.visibilities.sidebar = false; + } + } +- } + +- Behavior on implicitHeight { +- Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ NotifActionList { ++ notif: root.modelData + } + } + + component WrappedLoader: Loader { ++ id: comp ++ + required property bool shouldBeActive + +- opacity: shouldBeActive ? 1 : 0 +- active: opacity > 0 ++ active: false ++ opacity: 0 + +- Behavior on opacity { +- Anim {} ++ // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set ++ states: State { ++ name: "active" ++ when: comp.shouldBeActive ++ ++ PropertyChanges { ++ comp.opacity: 1 ++ comp.active: true ++ } + } ++ ++ transitions: [ ++ Transition { ++ from: "" ++ to: "active" ++ ++ SequentialAnimation { ++ PropertyAction { ++ property: "active" ++ } ++ Anim { ++ property: "opacity" ++ } ++ } ++ }, ++ Transition { ++ from: "active" ++ to: "" ++ ++ SequentialAnimation { ++ Anim { ++ property: "opacity" ++ } ++ PropertyAction { ++ property: "active" ++ } ++ } ++ } ++ ] + } + } +diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml +index d1f1e1f5..084fe678 100644 +--- a/modules/sidebar/NotifActionList.qml ++++ b/modules/sidebar/NotifActionList.qml +@@ -1,19 +1,19 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components + import qs.components.containers + import qs.components.effects + import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts + + Item { + id: root + +- required property Notifs.Notif notif ++ required property NotifData notif + + Layout.fillWidth: true + implicitHeight: flickable.contentHeight +@@ -94,14 +94,14 @@ Item { + id: actionList + + anchors.fill: parent +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Repeater { + model: [ + { + isClose: true + }, +- ...root.notif.actions, ++ ...(root.notif?.actions ?? []), + { + isCopy: true + } +@@ -114,11 +114,11 @@ Item { + + Layout.fillWidth: true + Layout.fillHeight: true +- implicitWidth: actionInner.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: actionInner.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: actionInner.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: actionInner.implicitHeight + Tokens.padding.small * 2 + +- Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Appearance.padding.large : 0) +- radius: actionStateLayer.pressed ? Appearance.rounding.small / 2 : Appearance.rounding.small ++ Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0) ++ radius: actionStateLayer.pressed ? Tokens.rounding.small / 2 : Tokens.rounding.small + color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4) + + Timer { +@@ -131,7 +131,7 @@ Item { + StateLayer { + id: actionStateLayer + +- function onClicked(): void { ++ onClicked: { + if (action.modelData.isClose) { + root.notif.close(); + } else if (action.modelData.isCopy) { +@@ -150,7 +150,7 @@ Item { + id: actionInner + + anchors.centerIn: parent +- sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp ++ sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp + } + + Component { +@@ -167,6 +167,7 @@ Item { + id: iconComp + + IconImage { ++ asynchronous: true + source: Quickshell.iconPath(action.modelData.identifier) + } + } +@@ -182,15 +183,13 @@ Item { + + Behavior on Layout.preferredWidth { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + Behavior on radius { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml +index d039d15d..4509ce26 100644 +--- a/modules/sidebar/NotifDock.qml ++++ b/modules/sidebar/NotifDock.qml +@@ -1,25 +1,26 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts ++import qs.utils + + Item { + id: root + + required property Props props +- required property var visibilities ++ required property DrawerVisibilities visibilities + readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) + + anchors.fill: parent +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + Component.onCompleted: Notifs.list.forEach(n => n.popup = false) + +@@ -29,7 +30,7 @@ Item { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right +- anchors.margins: Appearance.padding.small ++ anchors.margins: Tokens.padding.small + + implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) + +@@ -43,8 +44,8 @@ Item { + + text: root.notifCount + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.normal ++ font.family: Tokens.font.family.mono + font.weight: 500 + + Behavior on anchors.leftMargin { +@@ -62,12 +63,12 @@ Item { + anchors.verticalCenter: parent.verticalCenter + anchors.left: count.right + anchors.right: parent.right +- anchors.leftMargin: Appearance.spacing.small ++ anchors.leftMargin: Tokens.spacing.small + + text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.normal +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.normal ++ font.family: Tokens.font.family.mono + font.weight: 500 + elide: Text.ElideRight + } +@@ -80,24 +81,25 @@ Item { + anchors.right: parent.right + anchors.top: title.bottom + anchors.bottom: parent.bottom +- anchors.topMargin: Appearance.spacing.smaller ++ anchors.topMargin: Tokens.spacing.smaller + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + color: "transparent" + + Loader { ++ asynchronous: true + anchors.centerIn: parent + active: opacity > 0 + opacity: root.notifCount > 0 ? 0 : 1 + + sourceComponent: ColumnLayout { +- spacing: Appearance.spacing.large ++ spacing: Tokens.spacing.large + + Image { + asynchronous: true +- source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) ++ source: Paths.absolutePath(Config.paths.noNotifsPic) + fillMode: Image.PreserveAspectFit +- sourceSize.width: clipRect.width * 0.8 ++ sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) + + layer.enabled: true + layer.effect: Colouriser { +@@ -110,15 +112,15 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: qsTr("No Notifications") + color: Colours.palette.m3outlineVariant +- font.pointSize: Appearance.font.size.large +- font.family: Appearance.font.family.mono ++ font.pointSize: Tokens.font.size.large ++ font.family: Tokens.font.family.mono + font.weight: 500 + } + } + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.extraLarge ++ type: Anim.StandardExtraLarge + } + } + } +@@ -150,25 +152,33 @@ Item { + id: clearTimer + + repeat: true +- interval: 50 ++ triggeredOnStart: true ++ interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(Notifs.notClosed.length))) + onTriggered: { +- let next = null; +- for (let i = 0; i < notifList.repeater.count; i++) { +- next = notifList.repeater.itemAt(i); +- if (!next?.closed) +- break; +- } +- if (next) +- next.closeAll(); +- else ++ const first = Notifs.notClosed[0]; ++ if (!first) { + stop(); ++ return; ++ } ++ ++ const appName = first.appName; ++ let cleared = 0; ++ for (const n of Notifs.notClosed.filter(n => n.appName === appName)) { ++ n.close(); ++ cleared++; ++ if (cleared > 30) { ++ interval = 5; ++ return; ++ } ++ } + } + } + + Loader { ++ asynchronous: true + anchors.right: parent.right + anchors.bottom: parent.bottom +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + + scale: root.notifCount > 0 ? 1 : 0.5 + opacity: root.notifCount > 0 ? 1 : 0 +@@ -178,9 +188,9 @@ Item { + id: clearBtn + + icon: "clear_all" +- radius: Appearance.rounding.normal +- padding: Appearance.padding.normal +- font.pointSize: Math.round(Appearance.font.size.large * 1.2) ++ radius: Tokens.rounding.normal ++ padding: Tokens.padding.normal ++ font.pointSize: Math.round(Tokens.font.size.large * 1.2) + onClicked: clearTimer.start() + + Elevation { +@@ -193,14 +203,13 @@ Item { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial ++ duration: Tokens.anim.durations.expressiveFastSpatial + } + } + } +diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml +index b927e91a..2677698b 100644 +--- a/modules/sidebar/NotifDockList.qml ++++ b/modules/sidebar/NotifDockList.qml +@@ -1,44 +1,52 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Caelestia.Components ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell +-import QtQuick + +-Item { ++LazyListView { + id: root + + required property Props props + required property Flickable container +- required property var visibilities ++ required property DrawerVisibilities visibilities ++ ++ anchors.left: parent?.left ++ anchors.right: parent?.right ++ implicitHeight: contentHeight ++ ++ spacing: Tokens.spacing.small ++ readyDelay: 1 ++ cacheBuffer: 400 ++ asynchronous: true ++ ++ onViewportAdjustNeeded: d => { ++ if (contentYAnim.running) ++ contentYAnim.complete(); ++ contentYAnim.to = Math.max(0, container.contentY + d); ++ contentYAnim.start(); ++ } + +- readonly property alias repeater: repeater +- readonly property int spacing: Appearance.spacing.small +- property bool flag ++ useCustomViewport: true ++ viewport: Qt.rect(0, container.contentY, width, container.height) + +- anchors.left: parent.left +- anchors.right: parent.right +- implicitHeight: { +- const item = repeater.itemAt(repeater.count - 1); +- return item ? item.y + item.implicitHeight : 0; +- } ++ removeDuration: Tokens.anim.durations.normal + +- Repeater { +- id: repeater +- +- model: ScriptModel { +- values: { +- const map = new Map(); +- for (const n of Notifs.notClosed) +- map.set(n.appName, null); +- for (const n of Notifs.list) +- map.set(n.appName, null); +- return [...map.keys()]; +- } +- onValuesChanged: root.flagChanged() ++ model: ScriptModel { ++ values: { ++ const map = new Map(); ++ for (const n of Notifs.notClosed) ++ map.set(n.appName, null); ++ for (const n of Notifs.list) ++ map.set(n.appName, null); ++ return [...map.keys()]; + } ++ } + ++ delegate: Component { + MouseArea { + id: notif + +@@ -46,36 +54,20 @@ Item { + required property string modelData + + readonly property bool closed: notifInner.notifCount === 0 +- readonly property alias nonAnimHeight: notifInner.nonAnimHeight + property int startY + + function closeAll(): void { +- for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) +- n.close(); +- } +- +- y: { +- root.flag; // Force update +- let y = 0; +- for (let i = 0; i < index; i++) { +- const item = repeater.itemAt(i); +- if (!item.closed) +- y += item.nonAnimHeight + root.spacing; +- } +- return y; ++ clearTimer.start(); + } + +- containmentMask: QtObject { +- function contains(p: point): bool { +- if (!root.container.contains(notif.mapToItem(root.container, p))) +- return false; +- return notifInner.contains(p); +- } +- } +- +- implicitWidth: root.width ++ LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight ++ LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight ++ LazyListView.visibleHeight: notifInner.implicitHeight + implicitHeight: notifInner.implicitHeight + ++ opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1 ++ scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1 ++ + hoverEnabled: true + cursorShape: pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton +@@ -106,37 +98,21 @@ Item { + closeAll(); + } + +- ParallelAnimation { +- running: true +- +- Anim { +- target: notif +- property: "opacity" +- from: 0 +- to: 1 +- } +- Anim { +- target: notif +- property: "scale" +- from: 0 +- to: 1 +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- } +- +- ParallelAnimation { +- running: notif.closed +- +- Anim { +- target: notif +- property: "opacity" +- to: 0 +- } +- Anim { +- target: notif +- property: "scale" +- to: 0.6 ++ Timer { ++ id: clearTimer ++ ++ interval: 15 ++ repeat: true ++ triggeredOnStart: true ++ onTriggered: { ++ const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData); ++ if (notifs.length === 0) { ++ stop(); ++ return; ++ } ++ ++ for (const n of notifs.slice(0, 30)) ++ n.close(); + } + } + +@@ -149,19 +125,37 @@ Item { + visibilities: root.visibilities + } + +- Behavior on x { ++ Behavior on y { ++ enabled: notif.LazyListView.ready ++ + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + +- Behavior on y { ++ Behavior on opacity { ++ Anim {} ++ } ++ ++ Behavior on scale { ++ Anim { ++ type: Anim.DefaultSpatial ++ } ++ } ++ ++ Behavior on x { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } + } ++ ++ Anim { ++ id: contentYAnim ++ ++ target: root.container ++ property: "contentY" ++ type: Anim.DefaultSpatial ++ } + } +diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml +index 16aac33b..2920af74 100644 +--- a/modules/sidebar/NotifGroup.qml ++++ b/modules/sidebar/NotifGroup.qml +@@ -1,14 +1,14 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Services.Notifications ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services +-import qs.config + import qs.utils +-import Quickshell +-import Quickshell.Services.Notifications +-import QtQuick +-import QtQuick.Layouts + + StyledRect { + id: root +@@ -16,18 +16,25 @@ StyledRect { + required property string modelData + required property Props props + required property Flickable container +- required property var visibilities ++ required property DrawerVisibilities visibilities + + readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) +- readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) +- readonly property string image: notifs.find(n => !n.closed && n.image.length > 0)?.image ?? "" +- readonly property string appIcon: notifs.find(n => !n.closed && n.appIcon.length > 0)?.appIcon ?? "" +- readonly property int urgency: notifs.some(n => !n.closed && n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low ++ readonly property list activeNotifs: notifs.filter(n => !n.closed) ++ readonly property int notifCount: activeNotifs.length ++ readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? "" ++ readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" ++ readonly property int urgency: { ++ if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical)) ++ return NotificationUrgency.Critical; ++ if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal)) ++ return NotificationUrgency.Normal; ++ return NotificationUrgency.Low; ++ } + + readonly property int nonAnimHeight: { +- const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0); +- const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; +- return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2); ++ const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); ++ const columnHeight = headerHeight + notifList.layoutHeight; ++ return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); + } + readonly property bool expanded: props.expandedNotifs.includes(modelData) + +@@ -47,26 +54,32 @@ StyledRect { + + anchors.left: parent?.left + anchors.right: parent?.right +- implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 ++ implicitHeight: nonAnimHeight + + clip: true +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + ++ Behavior on implicitHeight { ++ Anim { ++ type: Anim.DefaultSpatial ++ } ++ } ++ + RowLayout { + id: content + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top +- anchors.margins: Appearance.padding.normal ++ anchors.margins: Tokens.padding.normal + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Item { + Layout.alignment: Qt.AlignLeft | Qt.AlignTop +- implicitWidth: Config.notifs.sizes.image +- implicitHeight: Config.notifs.sizes.image ++ implicitWidth: TokenConfig.sizes.notifs.image ++ implicitHeight: TokenConfig.sizes.notifs.image + + Component { + id: imageComp +@@ -74,10 +87,14 @@ StyledRect { + Image { + source: Qt.resolvedUrl(root.image) + fillMode: Image.PreserveAspectCrop ++ sourceSize: { ++ const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); ++ return Qt.size(size, size); ++ } + cache: false + asynchronous: true +- width: Config.notifs.sizes.image +- height: Config.notifs.sizes.image ++ width: TokenConfig.sizes.notifs.image ++ height: TokenConfig.sizes.notifs.image + } + } + +@@ -85,7 +102,7 @@ StyledRect { + id: appIconComp + + ColouredIcon { +- implicitSize: Math.round(Config.notifs.sizes.image * 0.6) ++ implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) + source: Quickshell.iconPath(root.appIcon) + colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.appIcon.endsWith("symbolic") +@@ -96,38 +113,40 @@ StyledRect { + id: materialIconComp + + MaterialIcon { +- text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) ++ text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency) + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + + StyledClippingRect { + anchors.fill: parent + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) : Colours.palette.m3secondaryContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + Loader { ++ asynchronous: true + anchors.centerIn: parent + sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp + } + } + + Loader { ++ asynchronous: true + anchors.right: parent.right + anchors.bottom: parent.bottom + active: root.appIcon && root.image + + sourceComponent: StyledRect { +- implicitWidth: Config.notifs.sizes.badge +- implicitHeight: Config.notifs.sizes.badge ++ implicitWidth: Tokens.sizes.notifs.badge ++ implicitHeight: Tokens.sizes.notifs.badge + + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + ColouredIcon { + anchors.centerIn: parent +- implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) ++ implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) + source: Quickshell.iconPath(root.appIcon) + colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer + layer.enabled: root.appIcon.endsWith("symbolic") +@@ -136,94 +155,87 @@ StyledRect { + } + } + +- ColumnLayout { ++ Column { + id: column + +- Layout.topMargin: -Appearance.padding.small +- Layout.bottomMargin: -Appearance.padding.small / 2 + Layout.fillWidth: true +- spacing: 0 ++ spacing: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 ++ ++ Behavior on spacing { ++ Anim {} ++ } + + RowLayout { + id: header + +- Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0 +- Layout.fillWidth: true +- spacing: Appearance.spacing.smaller ++ anchors.left: parent.left ++ anchors.right: parent.right ++ spacing: Tokens.spacing.smaller + + StyledText { + Layout.fillWidth: true + text: root.modelData + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + } + + StyledText { + animate: true +- text: root.notifs.find(n => !n.closed)?.timeStr ?? "" ++ text: root.activeNotifs[0]?.timeStr ?? "" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + StyledRect { +- implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 +- implicitHeight: groupCount.implicitHeight + Appearance.padding.small ++ implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 ++ implicitHeight: groupCount.implicitHeight + Tokens.padding.small + + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + + StateLayer { + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface +- +- function onClicked(): void { +- root.toggleExpand(!root.expanded); +- } ++ onClicked: root.toggleExpand(!root.expanded) + } + + RowLayout { + id: expandBtn + + anchors.centerIn: parent +- spacing: Appearance.spacing.small / 2 ++ spacing: Tokens.spacing.small / 2 + + StyledText { + id: groupCount + +- Layout.leftMargin: Appearance.padding.small / 2 ++ Layout.leftMargin: Tokens.padding.small / 2 + animate: true + text: root.notifCount + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + } + + MaterialIcon { +- Layout.rightMargin: -Appearance.padding.small / 2 ++ Layout.rightMargin: -Tokens.padding.small / 2 + text: "expand_more" + color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface + rotation: root.expanded ? 180 : 0 +- Layout.topMargin: root.expanded ? -Math.floor(Appearance.padding.smaller / 2) : 0 ++ Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.smaller / 2) : 0 + + Behavior on rotation { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + Behavior on Layout.topMargin { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } + } + } +- +- Behavior on Layout.bottomMargin { +- Anim {} +- } + } + + NotifGroupList { +diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml +index e586b5f7..72b8b3a8 100644 +--- a/modules/sidebar/NotifGroupList.qml ++++ b/modules/sidebar/NotifGroupList.qml +@@ -1,114 +1,82 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import Quickshell ++import Caelestia.Components ++import Caelestia.Config + import qs.components + import qs.services +-import qs.config +-import Quickshell +-import QtQuick +-import QtQuick.Layouts + +-Item { ++LazyListView { + id: root + + required property Props props + required property list notifs + required property bool expanded + required property Flickable container +- required property var visibilities +- +- readonly property real nonAnimHeight: { +- let h = -root.spacing; +- for (let i = 0; i < repeater.count; i++) { +- const item = repeater.itemAt(i); +- if (!item.modelData.closed && !item.previewHidden) +- h += item.nonAnimHeight + root.spacing; +- } +- return h; +- } +- +- readonly property int spacing: Math.round(Appearance.spacing.small / 2) +- property bool showAllNotifs +- property bool flag ++ required property DrawerVisibilities visibilities + + signal requestToggleExpand(expand: bool) + +- onExpandedChanged: { +- if (expanded) { +- clearTimer.stop(); +- showAllNotifs = true; +- } else { +- clearTimer.start(); +- } +- } ++ anchors.left: parent.left ++ anchors.right: parent.right ++ implicitHeight: contentHeight + +- Layout.fillWidth: true +- implicitHeight: nonAnimHeight ++ spacing: Math.round(Tokens.spacing.small / 2) ++ asynchronous: true + +- Timer { +- id: clearTimer ++ readyDelay: 1 ++ cacheBuffer: 400 ++ removeDuration: Tokens.anim.durations.normal + +- interval: Appearance.anim.durations.normal +- onTriggered: root.showAllNotifs = false ++ useCustomViewport: true ++ viewport: { ++ tWatcher.transform; // mapToItem is not reactive so use this to trigger updates ++ return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height); + } + +- Repeater { +- id: repeater ++ model: ScriptModel { ++ values: { ++ if (root.expanded) ++ return root.notifs; ++ ++ let count = 0; ++ let i = 0; ++ const previewNum = root.Config.notifs.groupPreviewNum; ++ while (i < root.notifs.length && count < previewNum) { ++ if (!(root.notifs[i]?.closed ?? true)) ++ count++; ++ i++; ++ } + +- model: ScriptModel { +- values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) +- onValuesChanged: root.flagChanged() ++ return root.notifs.slice(0, i); + } ++ } + ++ delegate: Component { + MouseArea { + id: notif + + required property int index +- required property Notifs.Notif modelData +- +- readonly property alias nonAnimHeight: notifInner.nonAnimHeight +- readonly property bool previewHidden: { +- if (root.expanded) +- return false; ++ required property NotifData modelData + +- let extraHidden = 0; +- for (let i = 0; i < index; i++) +- if (root.notifs[i].closed) +- extraHidden++; +- +- return index >= Config.notifs.groupPreviewNum + extraHidden; +- } + property int startY + +- y: { +- root.flag; // Force update +- let y = 0; +- for (let i = 0; i < index; i++) { +- const item = repeater.itemAt(i); +- if (!item.modelData.closed && !item.previewHidden) +- y += item.nonAnimHeight + root.spacing; +- } +- return y; +- } ++ Component.onCompleted: modelData?.lock(this) ++ Component.onDestruction: modelData?.unlock(this) + +- containmentMask: QtObject { +- function contains(p: point): bool { +- if (!root.container.contains(notif.mapToItem(root.container, p))) +- return false; +- return notifInner.contains(p); +- } +- } +- +- opacity: previewHidden ? 0 : 1 +- scale: previewHidden ? 0.7 : 1 +- +- implicitWidth: root.width ++ LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight ++ LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight + implicitHeight: notifInner.implicitHeight + ++ opacity: LazyListView.removing || LazyListView.adding ? 0 : 1 ++ scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1 ++ + hoverEnabled: true + cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + preventStealing: !root.expanded +- enabled: !modelData.closed ++ enabled: !(modelData?.closed ?? true) + + drag.target: this + drag.axis: Drag.XAxis +@@ -118,7 +86,7 @@ Item { + if (event.button === Qt.RightButton) + root.requestToggleExpand(!root.expanded); + else if (event.button === Qt.MiddleButton) +- modelData.close(); ++ modelData?.close(); + } + onPositionChanged: event => { + if (pressed && !root.expanded) { +@@ -131,32 +99,12 @@ Item { + if (Math.abs(x) < width * Config.notifs.clearThreshold) + x = 0; + else +- modelData.close(); +- } +- +- Component.onCompleted: modelData.lock(this) +- Component.onDestruction: modelData.unlock(this) +- +- ParallelAnimation { +- Component.onCompleted: running = !notif.previewHidden +- +- Anim { +- target: notif +- property: "opacity" +- from: 0 +- to: 1 +- } +- Anim { +- target: notif +- property: "scale" +- from: 0.7 +- to: 1 +- } ++ modelData?.close(); + } + + ParallelAnimation { +- running: notif.modelData.closed +- onFinished: notif.modelData.unlock(notif) ++ running: notif.modelData?.closed ?? false ++ onFinished: notif.modelData?.unlock(notif) + + Anim { + target: notif +@@ -180,6 +128,14 @@ Item { + visibilities: root.visibilities + } + ++ Behavior on y { ++ enabled: notif.LazyListView.ready ++ ++ Anim { ++ type: Anim.DefaultSpatial ++ } ++ } ++ + Behavior on opacity { + Anim {} + } +@@ -190,24 +146,16 @@ Item { + + Behavior on x { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- } +- +- Behavior on y { +- Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } + } + +- Behavior on implicitHeight { +- Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } ++ TransformWatcher { ++ id: tWatcher ++ ++ a: root.container.contentItem ++ b: root + } + } +diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml +index 9303c6b9..108c51a2 100644 +--- a/modules/sidebar/Wrapper.qml ++++ b/modules/sidebar/Wrapper.qml +@@ -1,66 +1,42 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config + import QtQuick ++import Caelestia.Config ++import qs.components + + Item { + id: root + +- required property var visibilities +- required property var panels ++ required property DrawerVisibilities visibilities + readonly property Props props: Props {} + +- visible: width > 0 +- implicitWidth: 0 ++ readonly property bool shouldBeActive: visibilities.sidebar && Config.sidebar.enabled ++ property real offsetScale: shouldBeActive ? 0 : 1 + +- states: State { +- name: "visible" +- when: root.visibilities.sidebar && Config.sidebar.enabled ++ visible: offsetScale < 1 ++ anchors.rightMargin: (-implicitWidth - 5) * offsetScale ++ implicitWidth: Tokens.sizes.sidebar.width ++ opacity: 1 - offsetScale + +- PropertyChanges { +- root.implicitWidth: Config.sidebar.sizes.width ++ Behavior on offsetScale { ++ Anim { ++ type: Anim.DefaultSpatial + } + } + +- transitions: [ +- Transition { +- from: "" +- to: "visible" +- +- Anim { +- target: root +- property: "implicitWidth" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial +- } +- }, +- Transition { +- from: "visible" +- to: "" +- +- Anim { +- target: root +- property: "implicitWidth" +- easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized +- } +- } +- ] +- + Loader { + id: content + + anchors.top: parent.top + anchors.bottom: parent.bottom + anchors.left: parent.left +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + anchors.bottomMargin: 0 + +- active: true +- Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) ++ active: root.shouldBeActive || root.visible + + sourceComponent: Content { +- implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2 ++ implicitWidth: Tokens.sizes.sidebar.width - Tokens.padding.large * 2 + props: root.props + visibilities: root.visibilities + } +diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml +index fbce8961..5b58b41e 100644 +--- a/modules/utilities/Background.qml ++++ b/modules/utilities/Background.qml +@@ -1,15 +1,14 @@ +-import qs.components +-import qs.services +-import qs.config + import QtQuick + import QtQuick.Shapes ++import qs.components ++import qs.services + + ShapePath { + id: root + + required property Wrapper wrapper + required property var sidebar +- readonly property real rounding: Config.border.rounding ++ required property real rounding + readonly property bool flatten: wrapper.height < rounding * 2 + readonly property real roundingY: flatten ? wrapper.height / 2 : rounding + +diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml +index 902656de..e03c546f 100644 +--- a/modules/utilities/Content.qml ++++ b/modules/utilities/Content.qml +@@ -1,14 +1,17 @@ + import "cards" +-import qs.config + import QtQuick + import QtQuick.Layouts ++import Caelestia.Config ++import qs.components ++import qs.modules.bar.popouts as BarPopouts + + Item { + id: root + + required property var props +- required property var visibilities +- required property Item popouts ++ required property DrawerVisibilities visibilities ++ required property BarPopouts.Wrapper popouts ++ required property matrix4x4 deformMatrix + + implicitWidth: layout.implicitWidth + implicitHeight: layout.implicitHeight +@@ -17,7 +20,7 @@ Item { + id: layout + + anchors.fill: parent +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + IdleInhibit {} + +@@ -35,5 +38,6 @@ Item { + + RecordingDeleteModal { + props: root.props ++ deformMatrix: root.deformMatrix + } + } +diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml +index 127afe93..13125f2c 100644 +--- a/modules/utilities/RecordingDeleteModal.qml ++++ b/modules/utilities/RecordingDeleteModal.qml +@@ -1,20 +1,22 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import QtQuick.Shapes ++import Caelestia ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.components.effects + import qs.services +-import qs.config +-import Caelestia +-import QtQuick +-import QtQuick.Layouts +-import QtQuick.Shapes + + Loader { + id: root + + required property var props ++ required property matrix4x4 deformMatrix + ++ asynchronous: true + anchors.fill: parent + + opacity: root.props.recordingConfirmDelete ? 1 : 0 +@@ -32,14 +34,16 @@ Loader { + + Item { + anchors.fill: parent +- anchors.margins: -Appearance.padding.large +- anchors.rightMargin: -Appearance.padding.large - Config.border.thickness +- anchors.bottomMargin: -Appearance.padding.large - Config.border.thickness ++ anchors.margins: -Tokens.padding.large ++ anchors.rightMargin: -Tokens.padding.large - Config.border.thickness ++ anchors.bottomMargin: -Tokens.padding.large - Config.border.thickness + opacity: 0.5 + + StyledRect { + anchors.fill: parent +- topLeftRadius: Config.border.rounding ++ anchors.rightMargin: -parent.width * (1 - root.deformMatrix.m11) / 2 // Additional bit to account for deform ++ anchors.bottomMargin: -parent.height * 0.1 // Additional bit to account for overshoot ++ topLeftRadius: Tokens.rounding.large + color: Colours.palette.m3scrim + } + +@@ -50,13 +54,14 @@ Loader { + preferredRendererType: Shape.CurveRenderer + asynchronous: true + ++ // Bottom left + ShapePath { +- startX: -Config.border.rounding * 2 +- startY: shape.height - Config.border.thickness ++ startX: -root.Config.border.smoothing * 2 ++ startY: shape.height - root.Config.border.thickness + strokeWidth: 0 + fillGradient: LinearGradient { + orientation: LinearGradient.Horizontal +- x1: -Config.border.rounding * 2 ++ x1: -root.Config.border.smoothing * 2 + + GradientStop { + position: 0 +@@ -69,31 +74,34 @@ Loader { + } + + PathLine { +- relativeX: Config.border.rounding ++ relativeX: root.Config.border.smoothing + relativeY: 0 + } +- PathArc { +- relativeY: -Config.border.rounding +- radiusX: Config.border.rounding +- radiusY: Config.border.rounding +- direction: PathArc.Counterclockwise ++ PathCubic { ++ relativeX: root.Config.border.smoothing ++ relativeY: -root.Config.border.smoothing ++ relativeControl1X: root.Config.border.smoothing * 0.93 ++ relativeControl1Y: -root.Config.border.smoothing * 0.07 ++ relativeControl2X: root.Config.border.smoothing * 0.93 ++ relativeControl2Y: -root.Config.border.smoothing * 0.07 + } + PathLine { + relativeX: 0 +- relativeY: Config.border.rounding + Config.border.thickness ++ relativeY: root.Config.border.smoothing + root.Config.border.thickness + } + PathLine { +- relativeX: -Config.border.rounding * 2 ++ relativeX: -root.Config.border.smoothing * 2 + relativeY: 0 + } + } + ++ // Top right curve + ShapePath { +- startX: shape.width - Config.border.rounding - Config.border.thickness ++ startX: shape.width - root.Config.border.smoothing - root.Config.border.thickness + (1 - root.deformMatrix.m11) * shape.width / 2 + strokeWidth: 0 + fillGradient: LinearGradient { + orientation: LinearGradient.Vertical +- y1: -Config.border.rounding * 2 ++ y1: -root.Config.border.smoothing * 2 + + GradientStop { + position: 0 +@@ -105,19 +113,20 @@ Loader { + } + } + +- PathArc { +- relativeX: Config.border.rounding +- relativeY: -Config.border.rounding +- radiusX: Config.border.rounding +- radiusY: Config.border.rounding +- direction: PathArc.Counterclockwise ++ PathCubic { ++ relativeX: root.Config.border.smoothing ++ relativeY: -root.Config.border.smoothing ++ relativeControl1X: root.Config.border.smoothing * 0.93 ++ relativeControl1Y: -root.Config.border.smoothing * 0.07 ++ relativeControl2X: root.Config.border.smoothing * 0.93 ++ relativeControl2Y: -root.Config.border.smoothing * 0.07 + } + PathLine { + relativeX: 0 +- relativeY: -Config.border.rounding ++ relativeY: -root.Config.border.smoothing + } + PathLine { +- relativeX: Config.border.thickness ++ relativeX: root.Config.border.thickness + relativeY: 0 + } + PathLine { +@@ -129,15 +138,15 @@ Loader { + + StyledRect { + anchors.centerIn: parent +- radius: Appearance.rounding.large ++ radius: Tokens.rounding.large + color: Colours.palette.m3surfaceContainerHigh + + scale: 0 + Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0) + +- width: Math.min(parent.width - Appearance.padding.large * 2, implicitWidth) +- implicitWidth: deleteConfirmationLayout.implicitWidth + Appearance.padding.large * 3 +- implicitHeight: deleteConfirmationLayout.implicitHeight + Appearance.padding.large * 3 ++ width: Math.min(parent.width - Tokens.padding.large * 2, implicitWidth) ++ implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.large * 3 ++ implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.large * 3 + + MouseArea { + anchors.fill: parent +@@ -154,26 +163,26 @@ Loader { + id: deleteConfirmationLayout + + anchors.fill: parent +- anchors.margins: Appearance.padding.large * 1.5 +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.large * 1.5 ++ spacing: Tokens.spacing.normal + + StyledText { + text: qsTr("Delete recording?") +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path) + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + } + + RowLayout { +- Layout.topMargin: Appearance.spacing.normal ++ Layout.topMargin: Tokens.spacing.normal + Layout.alignment: Qt.AlignRight +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + TextButton { + text: qsTr("Cancel") +@@ -194,8 +203,7 @@ Loader { + + Behavior on scale { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml +index 842a550f..cace5685 100644 +--- a/modules/utilities/Wrapper.qml ++++ b/modules/utilities/Wrapper.qml +@@ -1,16 +1,20 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia.Config ++import qs.components ++import qs.modules.sidebar as Sidebar ++import qs.modules.bar.popouts as BarPopouts + + Item { + id: root + +- required property var visibilities +- required property Item sidebar +- required property Item popouts ++ required property DrawerVisibilities visibilities ++ required property Sidebar.Wrapper sidebar ++ required property BarPopouts.Wrapper popouts ++ property real horizontalStretch ++ property matrix4x4 deformMatrix + + readonly property PersistentProperties props: PersistentProperties { + property bool recordingListExpanded: false +@@ -21,59 +25,48 @@ Item { + reloadableId: "utilities" + } + readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) ++ property real offsetScale: shouldBeActive ? 0 : 1 ++ property real sidebarLerp + +- visible: height > 0 +- implicitHeight: 0 +- implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width +- +- onStateChanged: { +- if (state === "visible" && timer.running) { +- timer.triggered(); +- timer.stop(); +- } +- } ++ visible: offsetScale < 1 ++ anchors.bottomMargin: (-implicitHeight - 5) * offsetScale ++ implicitHeight: content.implicitHeight + content.anchors.margins * 2 ++ implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Tokens.sizes.utilities.width * (1 - sidebarLerp) ++ opacity: 1 - offsetScale + + states: State { +- name: "visible" +- when: root.shouldBeActive ++ name: "attachedToSidebar" ++ when: root.visibilities.sidebar + + PropertyChanges { +- root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2 ++ root.sidebarLerp: 1 + } + } + + transitions: [ + Transition { + from: "" +- to: "visible" + + Anim { +- target: root +- property: "implicitHeight" +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ property: "sidebarLerp" ++ duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 ++ easing: Tokens.anim.standardAccel + } + }, + Transition { +- from: "visible" + to: "" + + Anim { +- target: root +- property: "implicitHeight" +- easing.bezierCurve: Appearance.anim.curves.emphasized ++ property: "sidebarLerp" ++ duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 ++ easing: Tokens.anim.standardDecel + } + } + ] + +- Timer { +- id: timer +- +- running: true +- interval: Appearance.anim.durations.extraLarge +- onTriggered: { +- content.active = Qt.binding(() => root.shouldBeActive || root.visible); +- content.visible = true; ++ Behavior on offsetScale { ++ Anim { ++ type: Anim.DefaultSpatial + } + } + +@@ -82,16 +75,17 @@ Item { + + anchors.top: parent.top + anchors.left: parent.left +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- visible: false +- active: true ++ asynchronous: true ++ active: root.shouldBeActive || root.visible + + sourceComponent: Content { +- implicitWidth: root.implicitWidth - Appearance.padding.large * 2 ++ implicitWidth: root.implicitWidth - content.anchors.margins * 2 + props: root.props + visibilities: root.visibilities + popouts: root.popouts ++ deformMatrix: root.deformMatrix + } + } + } +diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml +index 0344e3ad..c37ef3c5 100644 +--- a/modules/utilities/cards/IdleInhibit.qml ++++ b/modules/utilities/cards/IdleInhibit.qml +@@ -1,17 +1,17 @@ ++import QtQuick ++import QtQuick.Layouts ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config +-import QtQuick +-import QtQuick.Layouts + + StyledRect { + id: root + + Layout.fillWidth: true +- implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2 ++ implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + clip: true + +@@ -21,14 +21,14 @@ StyledRect { + anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + StyledRect { + implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + MaterialIcon { +@@ -37,7 +37,7 @@ StyledRect { + anchors.centerIn: parent + text: "coffee" + color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + +@@ -48,7 +48,7 @@ StyledRect { + StyledText { + Layout.fillWidth: true + text: qsTr("Keep Awake") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + elide: Text.ElideRight + } + +@@ -56,7 +56,7 @@ StyledRect { + Layout.fillWidth: true + text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") + color: Colours.palette.m3onSurfaceVariant +- font.pointSize: Appearance.font.size.small ++ font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + } + } +@@ -70,11 +70,12 @@ StyledRect { + Loader { + id: activeChip + ++ asynchronous: true + anchors.bottom: parent.bottom + anchors.left: parent.left +- anchors.topMargin: Appearance.spacing.larger +- anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight +- anchors.leftMargin: Appearance.padding.large ++ anchors.topMargin: Tokens.spacing.larger ++ anchors.bottomMargin: IdleInhibitor.enabled ? Tokens.padding.large : -implicitHeight ++ anchors.leftMargin: Tokens.padding.large + + opacity: IdleInhibitor.enabled ? 1 : 0 + scale: IdleInhibitor.enabled ? 1 : 0.5 +@@ -82,32 +83,31 @@ StyledRect { + Component.onCompleted: active = Qt.binding(() => opacity > 0) + + sourceComponent: StyledRect { +- implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2 +- implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2 ++ implicitWidth: activeText.implicitWidth + Tokens.padding.normal * 2 ++ implicitHeight: activeText.implicitHeight + Tokens.padding.small * 2 + +- radius: Appearance.rounding.full ++ radius: Tokens.rounding.full + color: Colours.palette.m3primary + + StyledText { + id: activeText + + anchors.centerIn: parent +- text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) ++ text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, GlobalConfig.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) + color: Colours.palette.m3onPrimary +- font.pointSize: Math.round(Appearance.font.size.small * 0.9) ++ font.pointSize: Math.round(Tokens.font.size.small * 0.9) + } + } + + Behavior on anchors.bottomMargin { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + + Behavior on opacity { + Anim { +- duration: Appearance.anim.durations.small ++ type: Anim.StandardSmall + } + } + +@@ -118,8 +118,7 @@ StyledRect { + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml +index 7caccc83..417f4147 100644 +--- a/modules/utilities/cards/Record.qml ++++ b/modules/utilities/cards/Record.qml +@@ -20,8 +20,9 @@ StyledRect { + color: Colours.tPalette.m3surfaceContainer + + property bool actuallyRecording: Recorder.running ++ readonly property bool recordingBusy: Recorder.running || Recorder.starting + property string lastError: "" +- property string currentVideoMode: Config.utilities.recording.videoMode ++ property string currentVideoMode: Recorder.videoMode || Config.utilities.recording.videoMode || "fullscreen" + + // Computed audio mode based on settings + readonly property string currentAudioMode: { +@@ -52,7 +53,7 @@ StyledRect { + } + + radius: Appearance.rounding.full +- color: root.actuallyRecording ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer ++ color: root.recordingBusy ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + + MaterialIcon { + id: icon +@@ -61,7 +62,7 @@ StyledRect { + anchors.horizontalCenterOffset: -0.5 + anchors.verticalCenterOffset: 1.5 + text: "screen_record" +- color: root.actuallyRecording ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer ++ color: root.recordingBusy ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + font.pointSize: Appearance.font.size.large + } + } +@@ -81,11 +82,12 @@ StyledRect { + Layout.fillWidth: true + text: { + if (root.lastError !== "") return qsTr("Error: %1").arg(root.lastError); ++ if (Recorder.starting) return root.startingText(Recorder.videoMode || root.currentVideoMode); + if (Recorder.paused) return qsTr("Recording paused"); + if (root.actuallyRecording) { +- const videoText = root.currentVideoMode; +- const audioText = root.currentAudioMode === "none" ? "no audio" : root.currentAudioMode; +- return qsTr("Recording %1 - %2").arg(videoText).arg(audioText); ++ const videoText = root.videoModeLabel(Recorder.videoMode || root.currentVideoMode); ++ const audioText = root.audioModeLabel(Recorder.audioMode || root.currentAudioMode); ++ return qsTr("Recording %1 with %2").arg(videoText).arg(audioText); + } + return qsTr("Recording off"); + } +@@ -96,7 +98,7 @@ StyledRect { + } + + SplitButton { +- disabled: root.actuallyRecording ++ disabled: root.recordingBusy + active: menuItems.find(m => m.mode === Config.utilities.recording.videoMode) ?? menuItems[0] + menu.onItemSelected: item => { + Config.utilities.recording.videoMode = item.mode; +@@ -110,21 +112,21 @@ StyledRect { + icon: "fullscreen" + text: qsTr("Record fullscreen") + activeText: qsTr("Fullscreen") +- onClicked: startRecording() ++ onClicked: startRecording(mode) + }, + MenuItem { + property string mode: "region" + icon: "screenshot_region" + text: qsTr("Record region") + activeText: qsTr("Region") +- onClicked: startRecording() ++ onClicked: startRecording(mode) + }, + MenuItem { + property string mode: "window" + icon: "web_asset" + text: qsTr("Record window") + activeText: qsTr("Window") +- onClicked: startRecording() ++ onClicked: startRecording(mode) + } + ] + } +@@ -156,7 +158,7 @@ StyledRect { + // Audio Sources Section + ColumnLayout { + Layout.fillWidth: true +- visible: !root.actuallyRecording ++ visible: !root.recordingBusy + spacing: Appearance.spacing.small + + RowLayout { +@@ -171,19 +173,31 @@ StyledRect { + Item { Layout.fillWidth: true } + + IconButton { +- icon: root.props.recordingAudioExpanded ? "expand_less" : "expand_more" +- type: IconButton.Tonal +- font.pointSize: Appearance.font.size.small ++ icon: root.props.recordingAudioExpanded ? "unfold_less" : "unfold_more" ++ type: IconButton.Text ++ label.animate: true + onClicked: { + root.props.recordingAudioExpanded = !root.props.recordingAudioExpanded; + } + } + } + +- ColumnLayout { ++ Item { ++ id: audioSourcesContainer ++ + Layout.fillWidth: true +- visible: root.props.recordingAudioExpanded +- spacing: Appearance.spacing.smaller ++ Layout.preferredHeight: root.props.recordingAudioExpanded ? audioSourcesLayout.implicitHeight : 0 ++ clip: true ++ enabled: root.props.recordingAudioExpanded ++ opacity: root.props.recordingAudioExpanded ? 1 : 0 ++ visible: root.props.recordingAudioExpanded || height > 0 ++ ++ ColumnLayout { ++ id: audioSourcesLayout ++ ++ width: parent.width ++ y: root.props.recordingAudioExpanded ? 0 : -Appearance.spacing.small ++ spacing: Appearance.spacing.smaller + + // System Audio (Default Sink) + RowLayout { +@@ -288,13 +302,26 @@ StyledRect { + } + } + } ++ ++ Behavior on y { ++ Anim { duration: Appearance.anim.durations.small } ++ } ++ } ++ ++ Behavior on Layout.preferredHeight { ++ Anim { type: Anim.DefaultSpatial } ++ } ++ ++ Behavior on opacity { ++ Anim { duration: Appearance.anim.durations.small } ++ } + } + } + + Loader { + id: listOrControls + +- property bool running: root.actuallyRecording ++ property bool running: root.recordingBusy + + Layout.fillWidth: true + Layout.preferredHeight: implicitHeight +@@ -371,7 +398,7 @@ StyledRect { + + StyledRect { + radius: Appearance.rounding.full +- color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error ++ color: Recorder.starting ? Colours.palette.m3secondary : Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error + + implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2 + implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2 +@@ -380,8 +407,8 @@ StyledRect { + id: recText + anchors.centerIn: parent + animate: true +- text: Recorder.paused ? "PAUSED" : "REC" +- color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError ++ text: Recorder.starting ? "WAIT" : Recorder.paused ? "PAUSED" : "REC" ++ color: Recorder.starting ? Colours.palette.m3onSecondary : Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError + font.family: Appearance.font.family.mono + } + +@@ -390,7 +417,7 @@ StyledRect { + } + + SequentialAnimation on opacity { +- running: !Recorder.paused && root.actuallyRecording ++ running: !Recorder.starting && !Recorder.paused && root.actuallyRecording + alwaysRunToEnd: true + loops: Animation.Infinite + Anim { +@@ -410,6 +437,9 @@ StyledRect { + + StyledText { + text: { ++ if (Recorder.starting) ++ return root.startingText(Recorder.videoMode || root.currentVideoMode); ++ + const elapsed = Recorder.elapsed; + const hours = Math.floor(elapsed / 3600); + const mins = Math.floor((elapsed % 3600) / 60); +@@ -429,6 +459,7 @@ StyledRect { + } + + IconButton { ++ disabled: Recorder.starting + label.animate: true + icon: Recorder.paused ? "play_arrow" : "pause" + toggle: true +@@ -450,19 +481,45 @@ StyledRect { + } + } + +- function startRecording() { ++ function videoModeLabel(mode) { ++ switch (mode) { ++ case "region": return qsTr("Region"); ++ case "window": return qsTr("Window"); ++ default: return qsTr("Fullscreen"); ++ } ++ } ++ ++ function audioModeLabel(mode) { ++ switch (mode) { ++ case "combined": return qsTr("system audio + microphone"); ++ case "system": return qsTr("system audio"); ++ case "mic": return qsTr("microphone"); ++ default: return qsTr("no audio"); ++ } ++ } ++ ++ function startingText(mode) { ++ switch (mode) { ++ case "region": return qsTr("Select a recording region"); ++ case "window": return qsTr("Select a window to record"); ++ default: return qsTr("Starting fullscreen recording"); ++ } ++ } ++ ++ function startRecording(videoMode) { + // Clear any previous errors + root.lastError = ""; + +- const videoMode = Config.utilities.recording.videoMode || "fullscreen"; ++ const selectedVideoMode = videoMode || Config.utilities.recording.videoMode || "fullscreen"; + const audioMode = root.currentAudioMode; + +- root.currentVideoMode = videoMode; ++ Config.utilities.recording.videoMode = selectedVideoMode; ++ root.currentVideoMode = selectedVideoMode; + +- console.log("Starting recording - Video:", videoMode, "Audio:", audioMode); ++ console.log("Starting recording - Video:", selectedVideoMode, "Audio:", audioMode); + + // Call Recorder service +- const success = Recorder.start(videoMode, audioMode); ++ const success = Recorder.start(selectedVideoMode, audioMode); + + if (!success) { + root.lastError = "Failed to start recording"; +@@ -516,6 +573,6 @@ StyledRect { + Component.onCompleted: { + // Sync initial state + root.actuallyRecording = Recorder.running; +- root.currentVideoMode = Config.utilities.recording.videoMode || "fullscreen"; ++ root.currentVideoMode = Recorder.videoMode || Config.utilities.recording.videoMode || "fullscreen"; + } + } +diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml +index b9d757a4..951f7f09 100644 +--- a/modules/utilities/cards/RecordingList.qml ++++ b/modules/utilities/cards/RecordingList.qml +@@ -1,23 +1,22 @@ + pragma ComponentBehavior: Bound + ++import QtQuick ++import QtQuick.Layouts ++import Quickshell ++import Quickshell.Widgets ++import Caelestia.Config ++import Caelestia.Models + import qs.components +-import qs.components.controls + import qs.components.containers ++import qs.components.controls + import qs.services +-import qs.config + import qs.utils +-import Caelestia +-import Caelestia.Models +-import Quickshell +-import Quickshell.Widgets +-import QtQuick +-import QtQuick.Layouts + + ColumnLayout { + id: root + + required property var props +- required property var visibilities ++ required property DrawerVisibilities visibilities + + spacing: 0 + +@@ -28,19 +27,19 @@ ColumnLayout { + onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded + + RowLayout { +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: "list" +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + text: qsTr("Recordings") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + + IconButton { +@@ -62,8 +61,8 @@ ColumnLayout { + } + + Layout.fillWidth: true +- Layout.rightMargin: -Appearance.spacing.small +- implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3) ++ Layout.rightMargin: -Tokens.spacing.small ++ implicitHeight: (Tokens.font.size.larger + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) + clip: true + + StyledScrollBar.vertical: StyledScrollBar { +@@ -78,14 +77,14 @@ ColumnLayout { + + anchors.left: list.contentItem.left + anchors.right: list.contentItem.right +- anchors.rightMargin: Appearance.spacing.small +- spacing: Appearance.spacing.small / 2 ++ anchors.rightMargin: Tokens.spacing.small ++ spacing: Tokens.spacing.small / 2 + + Component.onCompleted: baseName = modelData.baseName + + StyledText { + Layout.fillWidth: true +- Layout.rightMargin: Appearance.spacing.small / 2 ++ Layout.rightMargin: Tokens.spacing.small / 2 + text: { + const time = recording.baseName; + const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/); +@@ -105,7 +104,7 @@ ColumnLayout { + onClicked: { + root.visibilities.utilities = false; + root.visibilities.sidebar = false; +- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]); ++ Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.playback, recording.modelData.path]); + } + } + +@@ -115,7 +114,7 @@ ColumnLayout { + onClicked: { + root.visibilities.utilities = false; + root.visibilities.sidebar = false; +- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]); ++ Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.explorer, recording.modelData.path]); + } + } + +@@ -163,19 +162,20 @@ ColumnLayout { + } + + Loader { ++ asynchronous: true + anchors.centerIn: parent + + opacity: list.count === 0 ? 1 : 0 + active: opacity > 0 + + sourceComponent: ColumnLayout { +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "scan_delete" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + + opacity: root.props.recordingListExpanded ? 1 : 0 + scale: root.props.recordingListExpanded ? 1 : 0 +@@ -195,7 +195,7 @@ ColumnLayout { + } + + RowLayout { +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter +@@ -233,8 +233,7 @@ ColumnLayout { + + Behavior on implicitHeight { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml +index 5b57528b..8c294eb3 100644 +--- a/modules/utilities/cards/Toggles.qml ++++ b/modules/utilities/cards/Toggles.qml +@@ -1,112 +1,169 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import QtQuick.Layouts ++import Quickshell.Bluetooth ++import Caelestia.Config + import qs.components + import qs.components.controls + import qs.services +-import qs.config +-import qs.modules.controlcenter +-import Quickshell +-import Quickshell.Bluetooth +-import QtQuick +-import QtQuick.Layouts ++import qs.modules.bar.popouts as BarPopouts + + StyledRect { + id: root + +- required property var visibilities +- required property Item popouts ++ required property DrawerVisibilities visibilities ++ required property BarPopouts.Wrapper popouts ++ ++ readonly property var quickToggles: { ++ const seenIds = new Set(); ++ ++ return Config.utilities.quickToggles.filter(item => { ++ if (!(item.enabled ?? true)) ++ return false; ++ ++ if (seenIds.has(item.id)) { ++ return false; ++ } ++ ++ if (item.id === "vpn") { ++ return GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); ++ } ++ ++ seenIds.add(item.id); ++ return true; ++ }); ++ } ++ readonly property int splitIndex: Math.ceil(quickToggles.length / 2) ++ readonly property bool needExtraRow: quickToggles.length > 6 + + Layout.fillWidth: true +- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 ++ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: layout + + anchors.fill: parent +- anchors.margins: Appearance.padding.large +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.large ++ spacing: Tokens.spacing.normal + + StyledText { + text: qsTr("Quick Toggles") +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + +- RowLayout { +- Layout.alignment: Qt.AlignHCenter +- spacing: Appearance.spacing.small +- +- Toggle { +- icon: "wifi" +- checked: Network.wifiEnabled +- onClicked: Network.toggleWifi() +- } ++ QuickToggleRow { ++ rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles ++ } + +- Toggle { +- icon: "bluetooth" +- checked: Bluetooth.defaultAdapter?.enabled ?? false +- onClicked: { +- const adapter = Bluetooth.defaultAdapter; +- if (adapter) +- adapter.enabled = !adapter.enabled; +- } +- } ++ QuickToggleRow { ++ visible: root.needExtraRow ++ rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] ++ } ++ } + +- Toggle { +- icon: "mic" +- checked: !Audio.sourceMuted +- onClicked: { +- const audio = Audio.source?.audio; +- if (audio) +- audio.muted = !audio.muted; +- } +- } ++ component QuickToggleRow: RowLayout { ++ property var rowModel: [] + +- Toggle { +- icon: "settings" +- inactiveOnColour: Colours.palette.m3onSurfaceVariant +- toggle: false +- onClicked: { +- root.visibilities.utilities = false; +- root.popouts.detach("network"); +- } +- } ++ Layout.fillWidth: true ++ spacing: Tokens.spacing.small + +- Toggle { +- icon: "gamepad" +- checked: GameMode.enabled +- onClicked: GameMode.enabled = !GameMode.enabled +- } ++ Repeater { ++ model: parent.rowModel + +- Toggle { +- icon: "notifications_off" +- checked: Notifs.dnd +- onClicked: Notifs.dnd = !Notifs.dnd +- } ++ delegate: DelegateChooser { ++ role: "id" + +- Toggle { +- icon: "vpn_key" +- checked: VPN.connected +- enabled: !VPN.connecting +- visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) +- onClicked: VPN.toggle() ++ DelegateChoice { ++ roleValue: "wifi" ++ delegate: Toggle { ++ icon: "wifi" ++ checked: Nmcli.wifiEnabled ++ onClicked: Nmcli.toggleWifi() ++ } ++ } ++ DelegateChoice { ++ roleValue: "bluetooth" ++ delegate: Toggle { ++ icon: "bluetooth" ++ checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type ++ onClicked: { ++ const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type ++ if (adapter) ++ adapter.enabled = !adapter.enabled; ++ } ++ } ++ } ++ DelegateChoice { ++ roleValue: "mic" ++ delegate: Toggle { ++ icon: "mic" ++ checked: !Audio.sourceMuted ++ onClicked: { ++ const audio = Audio.source?.audio; ++ if (audio) ++ audio.muted = !audio.muted; ++ } ++ } ++ } ++ DelegateChoice { ++ roleValue: "settings" ++ delegate: Toggle { ++ icon: "settings" ++ inactiveOnColour: Colours.palette.m3onSurfaceVariant ++ toggle: false ++ onClicked: { ++ root.visibilities.utilities = false; ++ root.popouts.detach("network"); ++ } ++ } ++ } ++ DelegateChoice { ++ roleValue: "gameMode" ++ delegate: Toggle { ++ icon: "gamepad" ++ checked: GameMode.enabled ++ onClicked: GameMode.enabled = !GameMode.enabled ++ } ++ } ++ DelegateChoice { ++ roleValue: "dnd" ++ delegate: Toggle { ++ icon: "notifications_off" ++ checked: Notifs.dnd ++ onClicked: Notifs.dnd = !Notifs.dnd ++ } ++ } ++ DelegateChoice { ++ roleValue: "vpn" ++ delegate: Toggle { ++ icon: "vpn_key" ++ checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error" ++ enabled: !VPN.connecting ++ toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" ++ inactiveOnColour: Colours.palette.m3onSurfaceVariant ++ onClicked: VPN.toggle() ++ } ++ } + } + } + } + + component Toggle: IconButton { + Layout.fillWidth: true +- Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) +- radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal ++ Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) ++ radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal + inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + toggle: true +- radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial +- radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial ++ radiusAnim.easing: Tokens.anim.expressiveFastSpatial + + Behavior on Layout.preferredWidth { + Anim { +- duration: Appearance.anim.durations.expressiveFastSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial ++ type: Anim.FastSpatial + } + } + } +diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml +index f4755000..5247e773 100644 +--- a/modules/utilities/toasts/ToastItem.qml ++++ b/modules/utilities/toasts/ToastItem.qml +@@ -1,10 +1,10 @@ ++import QtQuick ++import QtQuick.Layouts ++import Caelestia ++import Caelestia.Config + import qs.components + import qs.components.effects + import qs.services +-import qs.config +-import Caelestia +-import QtQuick +-import QtQuick.Layouts + + StyledRect { + id: root +@@ -13,9 +13,9 @@ StyledRect { + + anchors.left: parent.left + anchors.right: parent.right +- implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 + +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: { + if (root.modelData.type === Toast.Success) + return Colours.palette.m3successContainer; +@@ -50,13 +50,13 @@ StyledRect { + id: layout + + anchors.fill: parent +- anchors.margins: Appearance.padding.smaller +- anchors.leftMargin: Appearance.padding.normal +- anchors.rightMargin: Appearance.padding.normal +- spacing: Appearance.spacing.normal ++ anchors.margins: Tokens.padding.smaller ++ anchors.leftMargin: Tokens.padding.normal ++ anchors.rightMargin: Tokens.padding.normal ++ spacing: Tokens.spacing.normal + + StyledRect { +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + color: { + if (root.modelData.type === Toast.Success) + return Colours.palette.m3success; +@@ -68,7 +68,7 @@ StyledRect { + } + + implicitWidth: implicitHeight +- implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 ++ implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 + + MaterialIcon { + id: icon +@@ -84,7 +84,7 @@ StyledRect { + return Colours.palette.m3onError; + return Colours.palette.m3onSurfaceVariant; + } +- font.pointSize: Math.round(Appearance.font.size.large * 1.2) ++ font.pointSize: Math.round(Tokens.font.size.large * 1.2) + } + } + +@@ -106,7 +106,7 @@ StyledRect { + return Colours.palette.m3onErrorContainer; + return Colours.palette.m3onSurface; + } +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + elide: Text.ElideRight + } + +diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml +index 2915404e..21f2934e 100644 +--- a/modules/utilities/toasts/Toasts.qml ++++ b/modules/utilities/toasts/Toasts.qml +@@ -1,18 +1,29 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.config +-import Caelestia +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root + +- readonly property int spacing: Appearance.spacing.small ++ readonly property int spacing: Tokens.spacing.small + property bool flag + +- implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 ++ function shouldShowToast(toast: Toast): bool { ++ if (!Notifs.hasFullscreen()) ++ return true; ++ if (Config.utilities.toasts.fullscreen === "all") ++ return true; ++ if (Config.utilities.toasts.fullscreen === "important") ++ return toast.type === Toast.Warning || toast.type === Toast.Error; ++ return false; ++ } ++ ++ implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.normal * 2 + implicitHeight: { + let h = -spacing; + for (let i = 0; i < repeater.count; i++) { +@@ -31,10 +42,12 @@ Item { + const toasts = []; + let count = 0; + for (const toast of Toaster.toasts) { ++ if (!root.shouldShowToast(toast)) ++ continue; + toasts.push(toast); + if (!toast.closed) { + count++; +- if (count > Config.utilities.maxToasts) ++ if (count > root.Config.utilities.maxToasts) + break; + } + } +@@ -98,8 +111,7 @@ Item { + properties: "opacity,scale" + from: 0 + to: 1 +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + + ParallelAnimation { +@@ -135,8 +147,7 @@ Item { + + Behavior on anchors.bottomMargin { + Anim { +- duration: Appearance.anim.durations.expressiveDefaultSpatial +- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial ++ type: Anim.DefaultSpatial + } + } + } +diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml +index 89acfe6d..c4fbbd53 100644 +--- a/modules/windowinfo/Buttons.qml ++++ b/modules/windowinfo/Buttons.qml +@@ -1,9 +1,11 @@ +-import qs.components +-import qs.services +-import qs.config +-import Quickshell.Widgets ++pragma ComponentBehavior: Bound ++ + import QtQuick + import QtQuick.Layouts ++import Quickshell.Widgets ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root +@@ -12,14 +14,14 @@ ColumnLayout { + property bool moveToWsExpanded + + anchors.fill: parent +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + RowLayout { +- Layout.topMargin: Appearance.padding.large +- Layout.leftMargin: Appearance.padding.large +- Layout.rightMargin: Appearance.padding.large ++ Layout.topMargin: Tokens.padding.large ++ Layout.leftMargin: Tokens.padding.large ++ Layout.rightMargin: Tokens.padding.large + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true +@@ -29,17 +31,14 @@ ColumnLayout { + + StyledRect { + color: Colours.palette.m3primary +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + +- implicitWidth: moveToWsIcon.implicitWidth + Appearance.padding.small * 2 +- implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small ++ implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small * 2 ++ implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.small + + StateLayer { + color: Colours.palette.m3onPrimary +- +- function onClicked(): void { +- root.moveToWsExpanded = !root.moveToWsExpanded; +- } ++ onClicked: root.moveToWsExpanded = !root.moveToWsExpanded + } + + MaterialIcon { +@@ -50,27 +49,27 @@ ColumnLayout { + animate: true + text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right" + color: Colours.palette.m3onPrimary +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } + + WrapperItem { + Layout.fillWidth: true +- Layout.leftMargin: Appearance.padding.large * 2 +- Layout.rightMargin: Appearance.padding.large * 2 ++ Layout.leftMargin: Tokens.padding.large * 2 ++ Layout.rightMargin: Tokens.padding.large * 2 + + Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0 + clip: true + +- topMargin: Appearance.spacing.normal +- bottomMargin: Appearance.spacing.normal ++ topMargin: Tokens.spacing.normal ++ bottomMargin: Tokens.spacing.normal + + GridLayout { + id: wsGrid + +- rowSpacing: Appearance.spacing.smaller +- columnSpacing: Appearance.spacing.normal ++ rowSpacing: Tokens.spacing.smaller ++ columnSpacing: Tokens.spacing.normal + columns: 5 + + Repeater { +@@ -81,14 +80,14 @@ ColumnLayout { + readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 + readonly property bool isCurrent: root.client?.workspace.id === wsId + ++ onClicked: { ++ Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); ++ } ++ + color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer + onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer + text: wsId + disabled: isCurrent +- +- function onClicked(): void { +- Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); +- } + } + } + } +@@ -100,24 +99,22 @@ ColumnLayout { + + RowLayout { + Layout.fillWidth: true +- Layout.leftMargin: Appearance.padding.large +- Layout.rightMargin: Appearance.padding.large +- Layout.bottomMargin: Appearance.padding.large ++ Layout.leftMargin: Tokens.padding.large ++ Layout.rightMargin: Tokens.padding.large ++ Layout.bottomMargin: Tokens.padding.large + +- spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small ++ spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.normal : Tokens.spacing.small + + Button { + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") +- +- function onClicked(): void { +- Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); +- } ++ onClicked: Hypr.dispatch(`togglefloating address:0x${root.client?.address}`) + } + + Loader { +- active: root.client?.lastIpcObject.floating ++ asynchronous: true ++ active: root.client?.lastIpcObject.floating ?? false + Layout.fillWidth: active + Layout.leftMargin: active ? 0 : -parent.spacing + Layout.rightMargin: active ? 0 : -parent.spacing +@@ -126,10 +123,7 @@ ColumnLayout { + color: Colours.palette.m3secondaryContainer + onColor: Colours.palette.m3onSecondaryContainer + text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") +- +- function onClicked(): void { +- Hypr.dispatch(`pin address:0x${root.client?.address}`); +- } ++ onClicked: Hypr.dispatch(`pin address:0x${root.client?.address}`) + } + } + +@@ -137,10 +131,7 @@ ColumnLayout { + color: Colours.palette.m3errorContainer + onColor: Colours.palette.m3onErrorContainer + text: qsTr("Kill") +- +- function onClicked(): void { +- Hypr.dispatch(`killwindow address:0x${root.client?.address}`); +- } ++ onClicked: Hypr.dispatch(`killwindow address:0x${root.client?.address}`) + } + } + +@@ -149,22 +140,18 @@ ColumnLayout { + property alias disabled: stateLayer.disabled + property alias text: label.text + +- function onClicked(): void { +- } ++ signal clicked + +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + + Layout.fillWidth: true +- implicitHeight: label.implicitHeight + Appearance.padding.small * 2 ++ implicitHeight: label.implicitHeight + Tokens.padding.small * 2 + + StateLayer { + id: stateLayer + + color: parent.onColor +- +- function onClicked(): void { +- parent.onClicked(); +- } ++ onClicked: parent.clicked() + } + + StyledText { +@@ -174,7 +161,7 @@ ColumnLayout { + + animate: true + color: parent.onColor +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + } + } +diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml +index f9ee66a6..1820409f 100644 +--- a/modules/windowinfo/Details.qml ++++ b/modules/windowinfo/Details.qml +@@ -1,9 +1,9 @@ +-import qs.components +-import qs.services +-import qs.config +-import Quickshell.Hyprland + import QtQuick + import QtQuick.Layouts ++import Quickshell.Hyprland ++import Caelestia.Config ++import qs.components ++import qs.services + + ColumnLayout { + id: root +@@ -11,15 +11,15 @@ ColumnLayout { + required property HyprlandToplevel client + + anchors.fill: parent +- spacing: Appearance.spacing.small ++ spacing: Tokens.spacing.small + + Label { +- Layout.topMargin: Appearance.padding.large * 2 ++ Layout.topMargin: Tokens.padding.large * 2 + + text: root.client?.title ?? qsTr("No active client") + wrapMode: Text.WrapAtWordBoundaryOrAnywhere + +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + font.weight: 500 + } + +@@ -27,16 +27,16 @@ ColumnLayout { + text: root.client?.lastIpcObject.class ?? qsTr("No active client") + color: Colours.palette.m3tertiary + +- font.pointSize: Appearance.font.size.larger ++ font.pointSize: Tokens.font.size.larger + } + + StyledRect { + Layout.fillWidth: true + Layout.preferredHeight: 1 +- Layout.leftMargin: Appearance.padding.large * 2 +- Layout.rightMargin: Appearance.padding.large * 2 +- Layout.topMargin: Appearance.spacing.normal +- Layout.bottomMargin: Appearance.spacing.large ++ Layout.leftMargin: Tokens.padding.large * 2 ++ Layout.rightMargin: Tokens.padding.large * 2 ++ Layout.topMargin: Tokens.spacing.normal ++ Layout.bottomMargin: Tokens.spacing.large + + color: Colours.palette.m3secondary + } +@@ -130,11 +130,11 @@ ColumnLayout { + required property string text + property alias color: icon.color + +- Layout.leftMargin: Appearance.padding.large +- Layout.rightMargin: Appearance.padding.large ++ Layout.leftMargin: Tokens.padding.large ++ Layout.rightMargin: Tokens.padding.large + Layout.fillWidth: true + +- spacing: Appearance.spacing.smaller ++ spacing: Tokens.spacing.smaller + + MaterialIcon { + id: icon +@@ -149,13 +149,13 @@ ColumnLayout { + + text: detail.text + elide: Text.ElideRight +- font.pointSize: Appearance.font.size.normal ++ font.pointSize: Tokens.font.size.normal + } + } + + component Label: StyledText { +- Layout.leftMargin: Appearance.padding.large +- Layout.rightMargin: Appearance.padding.large ++ Layout.leftMargin: Tokens.padding.large ++ Layout.rightMargin: Tokens.padding.large + Layout.fillWidth: true + elide: Text.ElideRight + horizontalAlignment: Text.AlignHCenter +diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml +index 4cc0aab8..fed85891 100644 +--- a/modules/windowinfo/Preview.qml ++++ b/modules/windowinfo/Preview.qml +@@ -1,13 +1,13 @@ + pragma ComponentBehavior: Bound + +-import qs.components +-import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Wayland +-import Quickshell.Hyprland + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Quickshell.Hyprland ++import Quickshell.Wayland ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root +@@ -15,7 +15,7 @@ Item { + required property ShellScreen screen + required property HyprlandToplevel client + +- Layout.preferredWidth: preview.implicitWidth + Appearance.padding.large * 2 ++ Layout.preferredWidth: preview.implicitWidth + Tokens.padding.large * 2 + Layout.fillHeight: true + + StyledClippingRect { +@@ -24,15 +24,16 @@ Item { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.bottom: label.top +- anchors.topMargin: Appearance.padding.large +- anchors.bottomMargin: Appearance.spacing.normal ++ anchors.topMargin: Tokens.padding.large ++ anchors.bottomMargin: Tokens.spacing.normal + + implicitWidth: view.implicitWidth + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.small ++ radius: Tokens.rounding.small + + Loader { ++ asynchronous: true + anchors.centerIn: parent + active: !root.client + +@@ -43,14 +44,14 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: "web_asset_off" + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.extraLarge * 3 ++ font.pointSize: Tokens.font.size.extraLarge * 3 + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("No active client") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.extraLarge ++ font.pointSize: Tokens.font.size.extraLarge + font.weight: 500 + } + +@@ -58,7 +59,7 @@ Item { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Try switching to a window") + color: Colours.palette.m3outline +- font.pointSize: Appearance.font.size.large ++ font.pointSize: Tokens.font.size.large + } + } + } +@@ -68,7 +69,7 @@ Item { + + anchors.centerIn: parent + +- captureSource: root.client?.wayland ?? null ++ captureSource: root.client?.wayland ?? null // qmllint disable unresolved-type + live: true + + constraintSize.width: root.client ? parent.height * Math.min(root.screen.width / root.screen.height, root.client?.lastIpcObject.size[0] / root.client?.lastIpcObject.size[1]) : parent.height +@@ -81,7 +82,7 @@ Item { + + anchors.horizontalCenter: parent.horizontalCenter + anchors.bottom: parent.bottom +- anchors.bottomMargin: Appearance.padding.large ++ anchors.bottomMargin: Tokens.padding.large + + animate: true + text: { +diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml +index 919b3fbb..7ee35c29 100644 +--- a/modules/windowinfo/WindowInfo.qml ++++ b/modules/windowinfo/WindowInfo.qml +@@ -1,10 +1,10 @@ +-import qs.components +-import qs.services +-import qs.config +-import Quickshell +-import Quickshell.Hyprland + import QtQuick + import QtQuick.Layouts ++import Quickshell ++import Quickshell.Hyprland ++import Caelestia.Config ++import qs.components ++import qs.services + + Item { + id: root +@@ -13,15 +13,15 @@ Item { + required property HyprlandToplevel client + + implicitWidth: child.implicitWidth +- implicitHeight: screen.height * Config.winfo.sizes.heightMult ++ implicitHeight: screen.height * Tokens.sizes.winfo.heightMult + + RowLayout { + id: child + + anchors.fill: parent +- anchors.margins: Appearance.padding.large ++ anchors.margins: Tokens.padding.large + +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + + Preview { + screen: root.screen +@@ -29,9 +29,9 @@ Item { + } + + ColumnLayout { +- spacing: Appearance.spacing.normal ++ spacing: Tokens.spacing.normal + +- Layout.preferredWidth: Config.winfo.sizes.detailsWidth ++ Layout.preferredWidth: Tokens.sizes.winfo.detailsWidth + Layout.fillHeight: true + + StyledRect { +@@ -39,7 +39,7 @@ Item { + Layout.fillHeight: true + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + Details { + client: root.client +@@ -51,7 +51,7 @@ Item { + Layout.preferredHeight: buttons.implicitHeight + + color: Colours.tPalette.m3surfaceContainer +- radius: Appearance.rounding.normal ++ radius: Tokens.rounding.normal + + Buttons { + id: buttons +diff --git a/nix/app2unit.nix b/nix/app2unit.nix +deleted file mode 100644 +index 51b4241c..00000000 +--- a/nix/app2unit.nix ++++ /dev/null +@@ -1,14 +0,0 @@ +-{ +- pkgs, # To ensure the nixpkgs version of app2unit +- fetchFromGitHub, +- ... +-}: +-pkgs.app2unit.overrideAttrs (final: prev: rec { +- version = "1.0.3"; # Fix old issue related to missing env var +- src = fetchFromGitHub { +- owner = "Vladimir-csp"; +- repo = "app2unit"; +- tag = "v${version}"; +- hash = "sha256-7eEVjgs+8k+/NLteSBKgn4gPaPLHC+3Uzlmz6XB0930="; +- }; +-}) +diff --git a/nix/default.nix b/nix/default.nix +index 67747b2d..3a153dd0 100644 +--- a/nix/default.nix ++++ b/nix/default.nix +@@ -126,8 +126,6 @@ in + prePatch = '' + substituteInPlace assets/pam.d/fprint \ + --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so +- substituteInPlace shell.qml \ +- --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' + ''; + + postInstall = '' +diff --git a/plugin/src/Caelestia/Blobs/CMakeLists.txt b/plugin/src/Caelestia/Blobs/CMakeLists.txt +new file mode 100644 +index 00000000..9506f7f4 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/CMakeLists.txt +@@ -0,0 +1,20 @@ ++qml_module(caelestia-blobs ++ URI Caelestia.Blobs ++ SOURCES ++ blobgroup.cpp ++ blobshape.cpp ++ blobrect.cpp ++ blobinvertedrect.cpp ++ blobmaterial.cpp ++ LIBRARIES ++ Qt::Quick ++) ++ ++qt_add_shaders(caelestia-blobs "blob_shaders" ++ BATCHABLE OPTIMIZED NOHLSL NOMSL ++ GLSL "300es,330" ++ PREFIX "/" ++ FILES ++ shaders/blob.frag ++ shaders/blob.vert ++) +diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp +new file mode 100644 +index 00000000..a4703c86 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp +@@ -0,0 +1,104 @@ ++#include "blobgroup.hpp" ++#include "blobinvertedrect.hpp" ++#include "blobshape.hpp" ++ ++BlobGroup::BlobGroup(QObject* parent) ++ : QObject(parent) {} ++ ++BlobGroup::~BlobGroup() { ++ for (auto* shape : std::as_const(m_shapes)) ++ shape->m_group = nullptr; ++ if (m_invertedRect) ++ static_cast(m_invertedRect)->m_group = nullptr; ++} ++ ++void BlobGroup::setSmoothing(qreal s) { ++ if (qFuzzyCompare(m_smoothing, s)) ++ return; ++ m_smoothing = s; ++ emit smoothingChanged(); ++ markDirty(); ++} ++ ++void BlobGroup::setColor(const QColor& c) { ++ if (m_color == c) ++ return; ++ m_color = c; ++ emit colorChanged(); ++ markDirty(); ++} ++ ++void BlobGroup::addShape(BlobShape* shape) { ++ if (!shape || m_shapes.contains(shape)) ++ return; ++ m_shapes.append(shape); ++ markDirty(); ++} ++ ++void BlobGroup::removeShape(BlobShape* shape) { ++ m_shapes.removeOne(shape); ++ markDirty(); ++} ++ ++void BlobGroup::setInvertedRect(BlobInvertedRect* rect) { ++ if (m_invertedRect == rect) ++ return; ++ m_invertedRect = rect; ++ markDirty(); ++} ++ ++void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) { ++ if (m_invertedRect != rect) ++ return; ++ m_invertedRect = nullptr; ++ markDirty(); ++} ++ ++void BlobGroup::markDirty() { ++ m_physicsUpdated = false; ++ for (auto* shape : std::as_const(m_shapes)) { ++ shape->polish(); ++ shape->update(); ++ } ++ if (m_invertedRect) { ++ static_cast(m_invertedRect)->polish(); ++ static_cast(m_invertedRect)->update(); ++ } ++} ++ ++void BlobGroup::markShapeDirty(BlobShape* source) { ++ m_physicsUpdated = false; ++ ++ source->polish(); ++ source->update(); ++ ++ // Use cached padded rects to find spatial neighbors ++ const float pad = static_cast(m_smoothing) * 2.0f; ++ const QRectF srcRect(static_cast(source->m_cachedPaddedX - pad), ++ static_cast(source->m_cachedPaddedY - pad), static_cast(source->m_cachedPaddedW + pad * 2.0f), ++ static_cast(source->m_cachedPaddedH + pad * 2.0f)); ++ ++ for (auto* shape : std::as_const(m_shapes)) { ++ if (shape == source) ++ continue; ++ const QRectF otherRect(static_cast(shape->m_cachedPaddedX), static_cast(shape->m_cachedPaddedY), ++ static_cast(shape->m_cachedPaddedW), static_cast(shape->m_cachedPaddedH)); ++ if (srcRect.intersects(otherRect)) { ++ shape->polish(); ++ shape->update(); ++ } ++ } ++ ++ if (m_invertedRect && static_cast(m_invertedRect) != source) { ++ static_cast(m_invertedRect)->polish(); ++ static_cast(m_invertedRect)->update(); ++ } ++} ++ ++void BlobGroup::ensurePhysicsUpdated() { ++ if (m_physicsUpdated) ++ return; ++ m_physicsUpdated = true; ++ for (auto* shape : std::as_const(m_shapes)) ++ shape->updatePhysics(); ++} +diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp +new file mode 100644 +index 00000000..e09125a9 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp +@@ -0,0 +1,53 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++ ++class BlobShape; ++class BlobInvertedRect; ++ ++class BlobGroup : public QObject { ++ Q_OBJECT ++ QML_ELEMENT ++ Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) ++ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) ++ ++public: ++ explicit BlobGroup(QObject* parent = nullptr); ++ ~BlobGroup() override; ++ ++ qreal smoothing() const { return m_smoothing; } ++ ++ void setSmoothing(qreal s); ++ ++ QColor color() const { return m_color; } ++ ++ void setColor(const QColor& c); ++ ++ void addShape(BlobShape* shape); ++ void removeShape(BlobShape* shape); ++ ++ void setInvertedRect(BlobInvertedRect* rect); ++ void clearInvertedRect(BlobInvertedRect* rect); ++ ++ const QList& shapes() const { return m_shapes; } ++ ++ BlobInvertedRect* invertedRect() const { return m_invertedRect; } ++ ++ void markDirty(); ++ void markShapeDirty(BlobShape* source); ++ void ensurePhysicsUpdated(); ++ ++signals: ++ void smoothingChanged(); ++ void colorChanged(); ++ ++private: ++ qreal m_smoothing = 32.0; ++ QColor m_color{ 0x44, 0x88, 0xff }; ++ QList m_shapes; ++ BlobInvertedRect* m_invertedRect = nullptr; ++ bool m_physicsUpdated = false; ++}; +diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp +new file mode 100644 +index 00000000..46ee73a4 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp +@@ -0,0 +1,184 @@ ++#include "blobinvertedrect.hpp" ++#include "blobgroup.hpp" ++#include "blobmaterial.hpp" ++ ++#include ++#include ++ ++#include ++#include ++ ++BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) ++ : BlobShape(parent) {} ++ ++static void setFrameIndices(quint16* idx) { ++ // Top strip: 0-1-4, 1-5-4 ++ idx[0] = 0; ++ idx[1] = 1; ++ idx[2] = 4; ++ idx[3] = 1; ++ idx[4] = 5; ++ idx[5] = 4; ++ // Right strip: 1-2-5, 2-6-5 ++ idx[6] = 1; ++ idx[7] = 2; ++ idx[8] = 5; ++ idx[9] = 2; ++ idx[10] = 6; ++ idx[11] = 5; ++ // Bottom strip: 2-3-6, 3-7-6 ++ idx[12] = 2; ++ idx[13] = 3; ++ idx[14] = 6; ++ idx[15] = 3; ++ idx[16] = 7; ++ idx[17] = 6; ++ // Left strip: 3-0-7, 0-4-7 ++ idx[18] = 3; ++ idx[19] = 0; ++ idx[20] = 7; ++ idx[21] = 0; ++ idx[22] = 4; ++ idx[23] = 7; ++} ++ ++QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { ++ if (!m_group) { ++ delete oldNode; ++ return nullptr; ++ } ++ ++ const float pad = static_cast(m_group->smoothing()); ++ ++ // Compute inner hole boundary in local coords ++ // Inset past the inner border edge by 2x smoothing to cover the blend zone ++ const float inset = pad * 2.0f; ++ const float holeLeft = static_cast(m_borderLeft) + inset; ++ const float holeTop = static_cast(m_borderTop) + inset; ++ const float holeRight = static_cast(width() - m_borderRight) - inset; ++ const float holeBot = static_cast(height() - m_borderBottom) - inset; ++ ++ // If the hole is too small or invalid, fall back to full quad ++ if (holeLeft >= holeRight || holeTop >= holeBot) ++ return BlobShape::updatePaintNode(oldNode, nullptr); ++ ++ auto* node = static_cast(oldNode); ++ ++ const bool needsRebuild = !node || node->geometry()->vertexCount() != 8; ++ ++ if (needsRebuild) { ++ delete oldNode; ++ node = new QSGGeometryNode; ++ ++ auto* geometry = ++ new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType); ++ geometry->setDrawingMode(QSGGeometry::DrawTriangles); ++ node->setGeometry(geometry); ++ node->setFlag(QSGNode::OwnsGeometry); ++ ++ setFrameIndices(geometry->indexDataAsUShort()); ++ ++ auto* material = new BlobMaterial; ++ material->setFlag(QSGMaterial::Blending); ++ node->setMaterial(material); ++ node->setFlag(QSGNode::OwnsMaterial); ++ } ++ ++ // Outer bounds (local coords) ++ const float x0 = static_cast(m_localPaddedRect.x()); ++ const float y0 = static_cast(m_localPaddedRect.y()); ++ const float x1 = x0 + static_cast(m_localPaddedRect.width()); ++ const float y1 = y0 + static_cast(m_localPaddedRect.height()); ++ const float w = x1 - x0; ++ const float h = y1 - y0; ++ ++ // Update vertex positions and texture coordinates ++ auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); ++ ++ // Outer corners ++ v[0].set(x0, y0, 0.0f, 0.0f); ++ v[1].set(x1, y0, 1.0f, 0.0f); ++ v[2].set(x1, y1, 1.0f, 1.0f); ++ v[3].set(x0, y1, 0.0f, 1.0f); ++ // Inner corners (hole) ++ v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); ++ v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); ++ v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); ++ v[7].set(holeLeft, holeBot, (holeLeft - x0) / w, (holeBot - y0) / h); ++ ++ node->markDirty(QSGNode::DirtyGeometry); ++ ++ // Update material uniforms ++ auto* material = static_cast(node->material()); ++ material->m_paddedX = m_cachedPaddedX; ++ material->m_paddedY = m_cachedPaddedY; ++ material->m_paddedW = m_cachedPaddedW; ++ material->m_paddedH = m_cachedPaddedH; ++ material->m_smoothFactor = pad; ++ material->m_myIndex = m_cachedMyIndex; ++ material->m_color = m_group->color(); ++ material->m_hasInverted = m_cachedHasInverted ? 1 : 0; ++ material->m_invertedRadius = m_cachedInvertedRadius; ++ memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); ++ memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); ++ ++ const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); ++ material->m_rectCount = count; ++ for (int i = 0; i < count; ++i) ++ material->m_rects[i] = m_cachedRects[i]; ++ ++ node->markDirty(QSGNode::DirtyMaterial); ++ ++ return node; ++} ++ ++BlobInvertedRect::~BlobInvertedRect() { ++ if (m_group) ++ m_group->clearInvertedRect(this); ++} ++ ++void BlobInvertedRect::setBorderLeft(qreal v) { ++ if (qFuzzyCompare(m_borderLeft, v)) ++ return; ++ m_borderLeft = v; ++ emit borderLeftChanged(); ++ if (m_group) ++ m_group->markDirty(); ++} ++ ++void BlobInvertedRect::setBorderRight(qreal v) { ++ if (qFuzzyCompare(m_borderRight, v)) ++ return; ++ m_borderRight = v; ++ emit borderRightChanged(); ++ if (m_group) ++ m_group->markDirty(); ++} ++ ++void BlobInvertedRect::setBorderTop(qreal v) { ++ if (qFuzzyCompare(m_borderTop, v)) ++ return; ++ m_borderTop = v; ++ emit borderTopChanged(); ++ if (m_group) ++ m_group->markDirty(); ++} ++ ++void BlobInvertedRect::setBorderBottom(qreal v) { ++ if (qFuzzyCompare(m_borderBottom, v)) ++ return; ++ m_borderBottom = v; ++ emit borderBottomChanged(); ++ if (m_group) ++ m_group->markDirty(); ++} ++ ++void BlobInvertedRect::registerWithGroup() { ++ if (m_group) ++ m_group->setInvertedRect(this); ++} ++ ++void BlobInvertedRect::unregisterFromGroup() { ++ if (m_group) ++ m_group->clearInvertedRect(this); ++} +diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp +new file mode 100644 +index 00000000..f7fa6c0a +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp +@@ -0,0 +1,54 @@ ++#pragma once ++ ++#include "blobshape.hpp" ++ ++#include ++ ++class BlobInvertedRect : public BlobShape { ++ Q_OBJECT ++ QML_ELEMENT ++ Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged) ++ Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged) ++ Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged) ++ Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged) ++ ++public: ++ explicit BlobInvertedRect(QQuickItem* parent = nullptr); ++ ~BlobInvertedRect() override; ++ ++ qreal borderLeft() const { return m_borderLeft; } ++ ++ void setBorderLeft(qreal v); ++ ++ qreal borderRight() const { return m_borderRight; } ++ ++ void setBorderRight(qreal v); ++ ++ qreal borderTop() const { return m_borderTop; } ++ ++ void setBorderTop(qreal v); ++ ++ qreal borderBottom() const { return m_borderBottom; } ++ ++ void setBorderBottom(qreal v); ++ ++signals: ++ void borderLeftChanged(); ++ void borderRightChanged(); ++ void borderTopChanged(); ++ void borderBottomChanged(); ++ ++protected: ++ bool isInvertedRect() const override { return true; } ++ ++ QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; ++ ++ void registerWithGroup() override; ++ void unregisterFromGroup() override; ++ ++private: ++ qreal m_borderLeft = 0; ++ qreal m_borderRight = 0; ++ qreal m_borderTop = 0; ++ qreal m_borderBottom = 0; ++}; +diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp +new file mode 100644 +index 00000000..721a2532 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp +@@ -0,0 +1,102 @@ ++#include "blobmaterial.hpp" ++ ++#include ++ ++static_assert(sizeof(decltype(BlobRectData::excludeMask)) == sizeof(float), ++ "BlobMaterial packs excludeMask into a float slot via memcpy"); ++ ++QSGMaterialType* BlobMaterial::type() const { ++ static QSGMaterialType s_type; ++ return &s_type; ++} ++ ++QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const { ++ return new BlobMaterialShader; ++} ++ ++int BlobMaterial::compare(const QSGMaterial* other) const { ++ if (this < other) ++ return -1; ++ if (this > other) ++ return 1; ++ return 0; ++} ++ ++BlobMaterialShader::BlobMaterialShader() { ++ setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb")); ++ setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); ++} ++ ++bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { ++ Q_UNUSED(oldMaterial); ++ auto* mat = static_cast(newMaterial); ++ QByteArray* buf = state.uniformData(); ++ Q_ASSERT(buf->size() >= 1440); ++ ++ if (state.isMatrixDirty()) { ++ const QMatrix4x4 m = state.combinedMatrix(); ++ memcpy(buf->data(), m.constData(), 64); ++ } ++ if (state.isOpacityDirty()) { ++ const float opacity = state.opacity(); ++ memcpy(buf->data() + 64, &opacity, 4); ++ } ++ ++ // Padded rect (offset 68) ++ memcpy(buf->data() + 68, &mat->m_paddedX, 4); ++ memcpy(buf->data() + 72, &mat->m_paddedY, 4); ++ memcpy(buf->data() + 76, &mat->m_paddedW, 4); ++ memcpy(buf->data() + 80, &mat->m_paddedH, 4); ++ ++ // Smooth factor (offset 84) ++ memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); ++ ++ // Rect count (offset 88) ++ memcpy(buf->data() + 88, &mat->m_rectCount, 4); ++ ++ // My index (offset 92) ++ memcpy(buf->data() + 92, &mat->m_myIndex, 4); ++ ++ // Color as vec4 (offset 96, 16 bytes) ++ const float color[4] = { ++ static_cast(mat->m_color.redF()), ++ static_cast(mat->m_color.greenF()), ++ static_cast(mat->m_color.blueF()), ++ static_cast(mat->m_color.alphaF()), ++ }; ++ memcpy(buf->data() + 96, color, 16); ++ ++ // Has inverted (offset 112) ++ memcpy(buf->data() + 112, &mat->m_hasInverted, 4); ++ ++ // Inverted radius (offset 116) ++ memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); ++ ++ // Padding at 120-127 (skip) ++ ++ // Inverted outer (offset 128, 16 bytes) ++ memcpy(buf->data() + 128, mat->m_invertedOuter, 16); ++ ++ // Inverted inner (offset 144, 16 bytes) ++ memcpy(buf->data() + 144, mat->m_invertedInner, 16); ++ ++ // Rect data (offset 160, each rect = 5 vec4s = 80 bytes) ++ const int count = qMin(mat->m_rectCount, 16); ++ for (int i = 0; i < count; ++i) { ++ const auto& r = mat->m_rects[i]; ++ const int base = 160 + i * 80; ++ // Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt) ++ float maskAsFloat; ++ memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); ++ const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; ++ const float d1[4] = { maskAsFloat, r.offsetX, r.offsetY, r.minEig }; ++ const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; ++ memcpy(buf->data() + base, d0, 16); ++ memcpy(buf->data() + base + 16, d1, 16); ++ memcpy(buf->data() + base + 32, r.invDeform, 16); ++ memcpy(buf->data() + base + 48, d3, 16); ++ memcpy(buf->data() + base + 64, r.radius, 16); ++ } ++ ++ return true; ++} +diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp +new file mode 100644 +index 00000000..bf1eda75 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp +@@ -0,0 +1,47 @@ ++#pragma once ++ ++#include ++#include ++#include ++ ++struct BlobRectData { ++ float cx = 0, cy = 0, hw = 0, hh = 0; ++ float offsetX = 0, offsetY = 0; ++ float minEig = 1.0f; ++ // Inverse of 2x2 deformation matrix, column-major for GLSL ++ float invDeform[4] = { 1, 0, 0, 1 }; ++ // Screen-space AABB half-extents of the deformed rect ++ float screenHalfX = 0, screenHalfY = 0; ++ // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU ++ float radius[4] = { 0, 0, 0, 0 }; ++ // Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. ++ // Used by the shader to skip smin between excluded pairs. ++ int excludeMask = 0; ++}; ++ ++class BlobMaterial : public QSGMaterial { ++public: ++ QSGMaterialType* type() const override; ++ QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override; ++ int compare(const QSGMaterial* other) const override; ++ ++ float m_paddedX = 0; ++ float m_paddedY = 0; ++ float m_paddedW = 0; ++ float m_paddedH = 0; ++ float m_smoothFactor = 32.0f; ++ int m_rectCount = 0; ++ int m_myIndex = -2; ++ QColor m_color{ 0x44, 0x88, 0xff }; ++ int m_hasInverted = 0; ++ float m_invertedRadius = 0; ++ float m_invertedOuter[4] = {}; ++ float m_invertedInner[4] = {}; ++ BlobRectData m_rects[16] = {}; ++}; ++ ++class BlobMaterialShader : public QSGMaterialShader { ++public: ++ BlobMaterialShader(); ++ bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override; ++}; +diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp +new file mode 100644 +index 00000000..15486d83 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobrect.cpp +@@ -0,0 +1,245 @@ ++#include "blobrect.hpp" ++#include "blobgroup.hpp" ++ ++#include ++#include ++ ++BlobRect::BlobRect(QQuickItem* parent) ++ : BlobShape(parent) {} ++ ++BlobRect::~BlobRect() { ++ if (m_group) ++ m_group->removeShape(this); ++} ++ ++void BlobRect::updatePolish() { ++ BlobShape::updatePolish(); ++ ++ if (m_physicsActive) { ++ // Check if deformation is visually imperceptible ++ float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); ++ float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); ++ ++ if (totalDelta < 0.004f && totalVel < 0.05f) { ++ // Snap to rest, no visible deformation ++ m_dm00 = 1.0f; ++ m_dm01 = 0.0f; ++ m_dm11 = 1.0f; ++ m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; ++ m_deformMatrix = QMatrix4x4(); ++ emit rawDeformMatrixChanged(); ++ updateCenteredDeformMatrix(); ++ m_physicsActive = false; ++ } else { ++ QMetaObject::invokeMethod( ++ this, ++ [this]() { ++ if (m_physicsActive && m_group) ++ m_group->markDirty(); ++ }, ++ Qt::QueuedConnection); ++ } ++ } ++} ++ ++void BlobRect::updatePhysics() { ++ const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); ++ ++ if (!m_hasPrevPos) { ++ m_prevScenePos = scenePos; ++ m_elapsed.start(); ++ m_hasPrevPos = true; ++ return; ++ } ++ ++ const float dt = static_cast(m_elapsed.restart()) / 1000.0f; ++ if (dt > 0.1f || dt < 0.001f) { ++ m_prevScenePos = scenePos; ++ // Still check atRest on skipped frames to avoid getting stuck ++ if (m_physicsActive) ++ checkAtRest(0.0f); ++ return; ++ } ++ ++ const float velX = static_cast(scenePos.x() - m_prevScenePos.x()) / dt; ++ const float velY = static_cast(scenePos.y() - m_prevScenePos.y()) / dt; ++ m_prevScenePos = scenePos; ++ ++ const float speed = std::sqrt(velX * velX + velY * velY); ++ ++ if (!m_physicsActive) { ++ if (speed < 5.0f) ++ return; ++ m_physicsActive = true; ++ } ++ ++ // Compute target deformation matrix from velocity ++ // R(θ) * diag(stretch, compress) * R(θ)^T ++ const float kStretchFactor = static_cast(m_deformScale); ++ constexpr float kMaxStretch = 0.35f; ++ ++ float target00 = 1.0f; ++ float target01 = 0.0f; ++ float target11 = 1.0f; ++ ++ if (speed > 5.0f) { ++ const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); ++ const float targetCompress = 1.0f / targetStretch; ++ ++ const float cosA = velX / speed; ++ const float sinA = velY / speed; ++ const float cos2 = cosA * cosA; ++ const float sin2 = sinA * sinA; ++ const float cs = cosA * sinA; ++ ++ target00 = targetStretch * cos2 + targetCompress * sin2; ++ target01 = (targetStretch - targetCompress) * cs; ++ target11 = targetStretch * sin2 + targetCompress * cos2; ++ } ++ ++ // Underdamped spring on each matrix component ++ const float kStiffness = static_cast(m_stiffness); ++ const float kDamping = static_cast(m_damping); ++ ++ const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; ++ m_dmVel00 += accel00 * dt; ++ m_dm00 += m_dmVel00 * dt; ++ ++ const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; ++ m_dmVel01 += accel01 * dt; ++ m_dm01 += m_dmVel01 * dt; ++ ++ const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; ++ m_dmVel11 += accel11 * dt; ++ m_dm11 += m_dmVel11 * dt; ++ ++ m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); ++ emit rawDeformMatrixChanged(); ++ updateCenteredDeformMatrix(); ++ ++ checkAtRest(speed); ++} ++ ++void BlobRect::setTopLeftRadius(qreal r) { ++ if (!qFuzzyCompare(m_topLeftRadius, r)) { ++ m_topLeftRadius = r; ++ emit topLeftRadiusChanged(); ++ if (m_group) ++ m_group->markDirty(); ++ } ++} ++ ++void BlobRect::setTopRightRadius(qreal r) { ++ if (!qFuzzyCompare(m_topRightRadius, r)) { ++ m_topRightRadius = r; ++ emit topRightRadiusChanged(); ++ if (m_group) ++ m_group->markDirty(); ++ } ++} ++ ++void BlobRect::setBottomLeftRadius(qreal r) { ++ if (!qFuzzyCompare(m_bottomLeftRadius, r)) { ++ m_bottomLeftRadius = r; ++ emit bottomLeftRadiusChanged(); ++ if (m_group) ++ m_group->markDirty(); ++ } ++} ++ ++void BlobRect::setBottomRightRadius(qreal r) { ++ if (!qFuzzyCompare(m_bottomRightRadius, r)) { ++ m_bottomRightRadius = r; ++ emit bottomRightRadiusChanged(); ++ if (m_group) ++ m_group->markDirty(); ++ } ++} ++ ++void BlobRect::cornerRadii(float out[4]) const { ++ const auto maxR = static_cast(std::min(width(), height())) * 0.5f; ++ const auto base = std::min(static_cast(m_radius), maxR); ++ out[0] = std::min(m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base, maxR); ++ out[1] = std::min(m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base, maxR); ++ out[2] = std::min(m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base, maxR); ++ out[3] = std::min(m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base, maxR); ++} ++ ++bool BlobRect::isExcluded(const BlobShape* other) const { ++ for (const auto& ptr : m_exclude) { ++ if (ptr == other) ++ return true; ++ } ++ return false; ++} ++ ++QQmlListProperty BlobRect::exclude() { ++ return QQmlListProperty( ++ this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); ++} ++ ++void BlobRect::excludeAppend(QQmlListProperty* prop, BlobRect* rect) { ++ auto* self = static_cast(prop->object); ++ self->m_exclude.append(rect); ++ if (self->m_group) ++ self->m_group->markDirty(); ++ emit self->excludeChanged(); ++} ++ ++qsizetype BlobRect::excludeCount(QQmlListProperty* prop) { ++ auto* self = static_cast(prop->object); ++ return self->m_exclude.size(); ++} ++ ++BlobRect* BlobRect::excludeAt(QQmlListProperty* prop, qsizetype index) { ++ auto* self = static_cast(prop->object); ++ return self->m_exclude.at(index); ++} ++ ++void BlobRect::excludeClear(QQmlListProperty* prop) { ++ auto* self = static_cast(prop->object); ++ if (self->m_exclude.isEmpty()) ++ return; ++ self->m_exclude.clear(); ++ if (self->m_group) ++ self->m_group->markDirty(); ++ emit self->excludeChanged(); ++} ++ ++void BlobRect::excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect) { ++ auto* self = static_cast(prop->object); ++ self->m_exclude[index] = rect; ++ if (self->m_group) ++ self->m_group->markDirty(); ++ emit self->excludeChanged(); ++} ++ ++void BlobRect::excludeRemoveLast(QQmlListProperty* prop) { ++ auto* self = static_cast(prop->object); ++ if (self->m_exclude.isEmpty()) ++ return; ++ self->m_exclude.removeLast(); ++ if (self->m_group) ++ self->m_group->markDirty(); ++ emit self->excludeChanged(); ++} ++ ++void BlobRect::checkAtRest(float speed) { ++ constexpr float kEpsilon = 0.002f; ++ const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && ++ std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && ++ std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f; ++ ++ if (atRest) { ++ m_dm00 = 1.0f; ++ m_dm01 = 0.0f; ++ m_dm11 = 1.0f; ++ m_dmVel00 = 0.0f; ++ m_dmVel01 = 0.0f; ++ m_dmVel11 = 0.0f; ++ m_deformMatrix = QMatrix4x4(); // identity ++ emit rawDeformMatrixChanged(); ++ updateCenteredDeformMatrix(); ++ m_physicsActive = false; ++ } ++} +diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp +new file mode 100644 +index 00000000..d2d6ad45 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobrect.hpp +@@ -0,0 +1,126 @@ ++#pragma once ++ ++#include "blobshape.hpp" ++ ++#include ++#include ++#include ++#include ++ ++class BlobRect : public BlobShape { ++ Q_OBJECT ++ QML_ELEMENT ++ Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged) ++ Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) ++ Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) ++ Q_PROPERTY(QQmlListProperty exclude READ exclude NOTIFY excludeChanged) ++ Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) ++ Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) ++ Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) ++ Q_PROPERTY( ++ qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged) ++ ++public: ++ explicit BlobRect(QQuickItem* parent = nullptr); ++ ~BlobRect() override; ++ ++ qreal stiffness() const { return m_stiffness; } ++ ++ void setStiffness(qreal s) { ++ if (!qFuzzyCompare(m_stiffness, s)) { ++ m_stiffness = s; ++ emit stiffnessChanged(); ++ } ++ } ++ ++ qreal damping() const { return m_damping; } ++ ++ void setDamping(qreal d) { ++ if (!qFuzzyCompare(m_damping, d)) { ++ m_damping = d; ++ emit dampingChanged(); ++ } ++ } ++ ++ qreal deformScale() const { return m_deformScale; } ++ ++ void setDeformScale(qreal s) { ++ if (!qFuzzyCompare(m_deformScale, s)) { ++ m_deformScale = s; ++ emit deformScaleChanged(); ++ } ++ } ++ ++ QQmlListProperty exclude(); ++ ++ bool isExcluded(const BlobShape* other) const override; ++ void cornerRadii(float out[4]) const override; ++ ++ qreal topLeftRadius() const { return m_topLeftRadius; } ++ ++ void setTopLeftRadius(qreal r); ++ ++ qreal topRightRadius() const { return m_topRightRadius; } ++ ++ void setTopRightRadius(qreal r); ++ ++ qreal bottomLeftRadius() const { return m_bottomLeftRadius; } ++ ++ void setBottomLeftRadius(qreal r); ++ ++ qreal bottomRightRadius() const { return m_bottomRightRadius; } ++ ++ void setBottomRightRadius(qreal r); ++ ++signals: ++ void stiffnessChanged(); ++ void dampingChanged(); ++ void deformScaleChanged(); ++ void excludeChanged(); ++ void topLeftRadiusChanged(); ++ void topRightRadiusChanged(); ++ void bottomLeftRadiusChanged(); ++ void bottomRightRadiusChanged(); ++ ++protected: ++ void updatePolish() override; ++ void updatePhysics() override; ++ ++private: ++ void checkAtRest(float speed); ++ ++ // Physics state ++ QPointF m_prevScenePos; ++ QElapsedTimer m_elapsed; ++ bool m_physicsActive = false; ++ bool m_hasPrevPos = false; ++ ++ // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, ++ // m11) Rest state is identity: m00=1, m01=0, m11=1 ++ float m_dm00 = 1.0f; ++ float m_dm01 = 0.0f; ++ float m_dm11 = 1.0f; ++ ++ // Spring velocities for each component ++ float m_dmVel00 = 0.0f; ++ float m_dmVel01 = 0.0f; ++ float m_dmVel11 = 0.0f; ++ ++ qreal m_stiffness = 200.0; ++ qreal m_damping = 16.0; ++ qreal m_deformScale = 0.0005; ++ ++ qreal m_topLeftRadius = -1; ++ qreal m_topRightRadius = -1; ++ qreal m_bottomLeftRadius = -1; ++ qreal m_bottomRightRadius = -1; ++ ++ QList> m_exclude; ++ ++ static void excludeAppend(QQmlListProperty* prop, BlobRect* rect); ++ static qsizetype excludeCount(QQmlListProperty* prop); ++ static BlobRect* excludeAt(QQmlListProperty* prop, qsizetype index); ++ static void excludeClear(QQmlListProperty* prop); ++ static void excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); ++ static void excludeRemoveLast(QQmlListProperty* prop); ++}; +diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp +new file mode 100644 +index 00000000..cfa11c44 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobshape.cpp +@@ -0,0 +1,392 @@ ++#include "blobshape.hpp" ++#include "blobgroup.hpp" ++#include "blobinvertedrect.hpp" ++ ++#include ++#include ++ ++#include ++#include ++ ++static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { ++ // Bounding box of the deformed shape: |M * corners| ++ const float dm00 = dm(0, 0), dm01 = dm(0, 1); ++ const float dm10 = dm(1, 0), dm11 = dm(1, 1); ++ const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh; ++ const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh; ++ const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3)); ++ const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3)); ++ return std::max(extraX, extraY); ++} ++ ++static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) { ++ const float dx = std::abs(px - cx) - hw; ++ const float dy = std::abs(py - cy) - hh; ++ const float mdx = std::max(dx, 0.0f); ++ const float mdy = std::max(dy, 0.0f); ++ return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f); ++} ++ ++static float cpuSmoothstep(float edge0, float edge1, float x) { ++ const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); ++ return t * t * (3.0f - 2.0f * t); ++} ++ ++BlobShape::BlobShape(QQuickItem* parent) ++ : QQuickItem(parent) { ++ setFlag(ItemHasContents); ++} ++ ++void BlobShape::setGroup(BlobGroup* g) { ++ if (m_group == g) ++ return; ++ if (m_group && isComponentComplete()) ++ unregisterFromGroup(); ++ m_group = g; ++ if (m_group && isComponentComplete()) ++ registerWithGroup(); ++ emit groupChanged(); ++ if (m_group) ++ m_group->markDirty(); ++} ++ ++void BlobShape::setRadius(qreal r) { ++ if (qFuzzyCompare(m_radius, r)) ++ return; ++ m_radius = r; ++ emit radiusChanged(); ++ if (m_group) ++ m_group->markDirty(); ++} ++ ++void BlobShape::componentComplete() { ++ QQuickItem::componentComplete(); ++ if (m_group) ++ registerWithGroup(); ++} ++ ++void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { ++ QQuickItem::geometryChange(newGeometry, oldGeometry); ++ updateCenteredDeformMatrix(); ++ if (m_group) { ++ // Accumulate sub-pixel drift so slow movements don't desync the shader ++ m_pendingDx += static_cast(newGeometry.x() - oldGeometry.x()); ++ m_pendingDy += static_cast(newGeometry.y() - oldGeometry.y()); ++ const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); ++ const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); ++ if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) { ++ m_pendingDx = 0; ++ m_pendingDy = 0; ++ m_group->markShapeDirty(this); ++ } ++ } ++} ++ ++void BlobShape::updateCenteredDeformMatrix() { ++ const auto cx = static_cast(width()) * 0.5f; ++ const auto cy = static_cast(height()) * 0.5f; ++ QMatrix4x4 result; ++ result.translate(cx, cy); ++ result *= m_deformMatrix; ++ result.translate(-cx, -cy); ++ if (m_centeredDeformMatrix != result) { ++ m_centeredDeformMatrix = result; ++ emit deformMatrixChanged(); ++ } ++} ++ ++void BlobShape::cornerRadii(float out[4]) const { ++ const auto maxR = static_cast(std::min(width(), height())) * 0.5f; ++ const auto r = std::min(static_cast(m_radius), maxR); ++ out[0] = r; ++ out[1] = r; ++ out[2] = r; ++ out[3] = r; ++} ++ ++void BlobShape::registerWithGroup() { ++ if (m_group) ++ m_group->addShape(this); ++} ++ ++void BlobShape::unregisterFromGroup() { ++ if (m_group) ++ m_group->removeShape(this); ++} ++ ++void BlobShape::updatePolish() { ++ if (!m_group) ++ return; ++ ++ // Ensure all shapes have up-to-date physics (only once per frame) ++ m_group->ensurePhysicsUpdated(); ++ ++ const QPointF scenePos = mapToScene(QPointF(0, 0)); ++ const float pad = static_cast(m_group->smoothing()); ++ ++ if (isInvertedRect()) { ++ m_cachedPaddedX = static_cast(scenePos.x()); ++ m_cachedPaddedY = static_cast(scenePos.y()); ++ m_cachedPaddedW = static_cast(width()); ++ m_cachedPaddedH = static_cast(height()); ++ m_localPaddedRect = QRectF(0, 0, width(), height()); ++ } else { ++ const float hw = static_cast(width()) * 0.5f; ++ const float hh = static_cast(height()) * 0.5f; ++ const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh); ++ ++ m_cachedPaddedX = static_cast(scenePos.x()) - totalPad; ++ m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; ++ m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; ++ m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; ++ m_localPaddedRect = QRectF(static_cast(-totalPad), static_cast(-totalPad), ++ width() + 2.0 * static_cast(totalPad), height() + 2.0 * static_cast(totalPad)); ++ } ++ ++ // Filter nearby normal rects ++ m_cachedRects.clear(); ++ m_cachedMyIndex = -2; ++ const QRectF myPadded(static_cast(m_cachedPaddedX), static_cast(m_cachedPaddedY), ++ static_cast(m_cachedPaddedW), static_cast(m_cachedPaddedH)); ++ ++ // Track shape pointers parallel to m_cachedRects for pairwise exclusion lookups ++ QVector rectShapes; ++ rectShapes.reserve(m_group->shapes().size()); ++ ++ for (BlobShape* other : m_group->shapes()) { ++ if (other->isInvertedRect()) ++ continue; ++ ++ // Skip zero-size rects ++ if (other->width() <= 0 || other->height() <= 0) ++ continue; ++ ++ if (isExcluded(other)) ++ continue; ++ ++ const QPointF otherScene = other->mapToScene(QPointF(0, 0)); ++ ++ bool include = false; ++ if (isInvertedRect()) { ++ include = true; ++ } else { ++ const float otherHW = static_cast(other->width()) * 0.5f; ++ const float otherHH = static_cast(other->height()) * 0.5f; ++ const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); ++ const QRectF otherPadded(otherScene.x() - static_cast(otherPad), ++ otherScene.y() - static_cast(otherPad), other->width() + 2.0 * static_cast(otherPad), ++ other->height() + 2.0 * static_cast(otherPad)); ++ include = myPadded.intersects(otherPadded); ++ } ++ ++ if (include) { ++ if (other == this) ++ m_cachedMyIndex = static_cast(m_cachedRects.size()); ++ ++ const QMatrix4x4& dm = other->m_deformMatrix; ++ const float a = dm(0, 0), b = dm(1, 0); ++ const float c = dm(0, 1), d = dm(1, 1); ++ ++ BlobRectData r; ++ r.cx = static_cast(otherScene.x() + other->width() / 2.0); ++ r.cy = static_cast(otherScene.y() + other->height() / 2.0); ++ r.hw = static_cast(other->width() / 2.0); ++ r.hh = static_cast(other->height() / 2.0); ++ other->cornerRadii(r.radius); ++ r.offsetX = dm(0, 3); ++ r.offsetY = dm(1, 3); ++ ++ // Pre-compute inverse deformation matrix ++ const float det = a * d - c * b; ++ const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; ++ r.invDeform[0] = d * invDet; ++ r.invDeform[1] = -b * invDet; ++ r.invDeform[2] = -c * invDet; ++ r.invDeform[3] = a * invDet; ++ ++ // Pre-compute minimum eigenvalue (avoids per-pixel sqrt) ++ const float halfTr = 0.5f * (a + d); ++ const float halfDiff = 0.5f * (a - d); ++ r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c); ++ ++ // Pre-compute screen-space AABB half-extents ++ r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh; ++ r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; ++ ++ m_cachedRects.append(r); ++ rectShapes.append(other); ++ } ++ } ++ ++ if (isInvertedRect()) ++ m_cachedMyIndex = -1; ++ ++ // Compute pairwise exclude masks. Bit j in entry i is set iff rect i excludes rect j ++ // or rect j excludes rect i. The shader uses this to avoid smin between excluded pairs. ++ const auto cachedCount = m_cachedRects.size(); ++ for (qsizetype i = 0; i < cachedCount; ++i) { ++ int mask = 0; ++ BlobShape* si = rectShapes[i]; ++ for (qsizetype j = 0; j < cachedCount; ++j) { ++ if (j == i) ++ continue; ++ BlobShape* sj = rectShapes[j]; ++ if (si->isExcluded(sj) || sj->isExcluded(si)) ++ mask |= (1 << j); ++ } ++ m_cachedRects[i].excludeMask = mask; ++ } ++ ++ // Cache inverted rect data ++ m_cachedHasInverted = false; ++ m_cachedInvertedRadius = 0; ++ memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); ++ memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); ++ ++ auto* inv = m_group->invertedRect(); ++ if (inv) { ++ const QPointF invScene = inv->mapToScene(QPointF(0, 0)); ++ const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); ++ const float outerCY = static_cast(invScene.y() + inv->height() / 2.0); ++ const float outerHW = static_cast(inv->width() / 2.0); ++ const float outerHH = static_cast(inv->height() / 2.0); ++ ++ const float innerCX = outerCX + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); ++ const float innerCY = outerCY + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); ++ const float innerHW = outerHW - static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); ++ const float innerHH = outerHH - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); ++ ++ // Check if this rect is near the border (within 2x smoothing of inner edge) ++ bool nearBorder = isInvertedRect(); ++ if (!nearBorder) { ++ const float margin = pad * 2.0f; ++ const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f; ++ const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f; ++ const float myHW = m_cachedPaddedW * 0.5f; ++ const float myHH = m_cachedPaddedH * 0.5f; ++ // Near border if any edge of padded rect is within margin of inner edge ++ nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) || ++ (myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin); ++ } ++ ++ if (nearBorder) { ++ m_cachedHasInverted = true; ++ m_cachedInvertedRadius = static_cast(inv->radius()); ++ ++ m_cachedInvertedOuter[0] = outerCX; ++ m_cachedInvertedOuter[1] = outerCY; ++ m_cachedInvertedOuter[2] = outerHW; ++ m_cachedInvertedOuter[3] = outerHH; ++ ++ m_cachedInvertedInner[0] = innerCX; ++ m_cachedInvertedInner[1] = innerCY; ++ m_cachedInvertedInner[2] = innerHW; ++ m_cachedInvertedInner[3] = innerHH; ++ } ++ } ++ ++ // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) ++ const float smoothFactor = pad; ++ constexpr float minR = 2.0f; ++ const auto rectCount = m_cachedRects.size(); ++ for (qsizetype i = 0; i < rectCount; ++i) { ++ auto& ri = m_cachedRects[i]; ++ const int riExcludeMask = ri.excludeMask; ++ float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; ++ ++ const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; ++ const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; ++ const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; ++ const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; ++ ++ for (qsizetype j = 0; j < rectCount; ++j) { ++ if (j == i) ++ continue; ++ if (riExcludeMask & (1 << j)) ++ continue; ++ const auto& rj = m_cachedRects[j]; ++ fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); ++ fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); ++ fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); ++ fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); ++ } ++ ++ if (m_cachedHasInverted) { ++ const float icx = m_cachedInvertedInner[0]; ++ const float icy = m_cachedInvertedInner[1]; ++ const float ihw = m_cachedInvertedInner[2]; ++ const float ihh = m_cachedInvertedInner[3]; ++ fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); ++ fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); ++ fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); ++ fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); ++ } ++ ++ // Combine base radii with fill factors into effective per-corner radii ++ ri.radius[0] = std::max(ri.radius[0] * fTr, minR); ++ ri.radius[1] = std::max(ri.radius[1] * fBr, minR); ++ ri.radius[2] = std::max(ri.radius[2] * fBl, minR); ++ ri.radius[3] = std::max(ri.radius[3] * fTl, minR); ++ } ++} ++ ++QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { ++ if (!m_group) { ++ delete oldNode; ++ return nullptr; ++ } ++ ++ auto* node = static_cast(oldNode); ++ if (!node) { ++ node = new QSGGeometryNode; ++ ++ auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); ++ geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); ++ node->setGeometry(geometry); ++ node->setFlag(QSGNode::OwnsGeometry); ++ ++ auto* material = new BlobMaterial; ++ material->setFlag(QSGMaterial::Blending); ++ node->setMaterial(material); ++ node->setFlag(QSGNode::OwnsMaterial); ++ } ++ ++ // Update geometry ++ auto* geometry = node->geometry(); ++ auto* v = geometry->vertexDataAsTexturedPoint2D(); ++ ++ const float x0 = static_cast(m_localPaddedRect.x()); ++ const float y0 = static_cast(m_localPaddedRect.y()); ++ const float x1 = x0 + static_cast(m_localPaddedRect.width()); ++ const float y1 = y0 + static_cast(m_localPaddedRect.height()); ++ ++ v[0].set(x0, y0, 0.0f, 0.0f); ++ v[1].set(x1, y0, 1.0f, 0.0f); ++ v[2].set(x0, y1, 0.0f, 1.0f); ++ v[3].set(x1, y1, 1.0f, 1.0f); ++ ++ node->markDirty(QSGNode::DirtyGeometry); ++ ++ // Update material ++ auto* material = static_cast(node->material()); ++ material->m_paddedX = m_cachedPaddedX; ++ material->m_paddedY = m_cachedPaddedY; ++ material->m_paddedW = m_cachedPaddedW; ++ material->m_paddedH = m_cachedPaddedH; ++ material->m_smoothFactor = static_cast(m_group->smoothing()); ++ material->m_myIndex = m_cachedMyIndex; ++ material->m_color = m_group->color(); ++ material->m_hasInverted = m_cachedHasInverted ? 1 : 0; ++ material->m_invertedRadius = m_cachedInvertedRadius; ++ memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); ++ memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); ++ ++ const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); ++ material->m_rectCount = count; ++ for (int i = 0; i < count; ++i) ++ material->m_rects[i] = m_cachedRects[i]; ++ ++ node->markDirty(QSGNode::DirtyMaterial); ++ ++ return node; ++} +diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp +new file mode 100644 +index 00000000..c05a40d8 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/blobshape.hpp +@@ -0,0 +1,79 @@ ++#pragma once ++ ++#include "blobmaterial.hpp" ++ ++#include ++#include ++#include ++ ++class BlobGroup; ++ ++class BlobShape : public QQuickItem { ++ Q_OBJECT ++ Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) ++ Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) ++ Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) ++ Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged) ++ ++ friend class BlobGroup; ++ ++public: ++ explicit BlobShape(QQuickItem* parent = nullptr); ++ ~BlobShape() override = default; ++ ++ BlobGroup* group() const { return m_group; } ++ ++ void setGroup(BlobGroup* g); ++ ++ qreal radius() const { return m_radius; } ++ ++ void setRadius(qreal r); ++ ++ QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } ++ ++ QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } ++ ++signals: ++ void groupChanged(); ++ void radiusChanged(); ++ void deformMatrixChanged(); ++ void rawDeformMatrixChanged(); ++ ++protected: ++ void componentComplete() override; ++ void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; ++ void updatePolish() override; ++ QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; ++ ++ virtual bool isInvertedRect() const { return false; } ++ ++ virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } ++ ++ virtual void cornerRadii(float out[4]) const; ++ ++ virtual void updatePhysics() {} ++ ++ virtual void registerWithGroup(); ++ virtual void unregisterFromGroup(); ++ void updateCenteredDeformMatrix(); ++ ++ BlobGroup* m_group = nullptr; ++ qreal m_radius = 0; ++ QMatrix4x4 m_deformMatrix; // identity by default ++ QMatrix4x4 m_centeredDeformMatrix; ++ ++ // Cached data from updatePolish ++ float m_cachedPaddedX = 0; ++ float m_cachedPaddedY = 0; ++ float m_cachedPaddedW = 0; ++ float m_cachedPaddedH = 0; ++ QRectF m_localPaddedRect; ++ QVector m_cachedRects; ++ int m_cachedMyIndex = -2; ++ float m_pendingDx = 0; ++ float m_pendingDy = 0; ++ bool m_cachedHasInverted = false; ++ float m_cachedInvertedRadius = 0; ++ float m_cachedInvertedOuter[4] = {}; ++ float m_cachedInvertedInner[4] = {}; ++}; +diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag +new file mode 100644 +index 00000000..c002fd25 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag +@@ -0,0 +1,251 @@ ++#version 440 ++ ++layout(location = 0) in vec2 qt_TexCoord0; ++layout(location = 0) out vec4 fragColor; ++ ++layout(std140, binding = 0) uniform buf { ++ mat4 qt_Matrix; ++ float qt_Opacity; ++ float paddedX; ++ float paddedY; ++ float paddedW; ++ float paddedH; ++ float smoothFactor; ++ int rectCount; ++ int myIndex; ++ vec4 color; ++ int hasInverted; ++ float invertedRadius; ++ vec4 invertedOuter; ++ vec4 invertedInner; ++ vec4 rectData[80]; ++}; ++ ++float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { ++ vec2 d = abs(p - center) - halfSize + vec2(radius); ++ return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius; ++} ++ ++float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { ++ // r = (topRight, bottomRight, bottomLeft, topLeft) ++ p -= center; ++ r.xy = (p.x > 0.0) ? r.xy : r.wz; ++ r.x = (p.y > 0.0) ? r.y : r.x; ++ vec2 q = abs(p) - halfSize + r.x; ++ return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; ++} ++ ++float sdBox(vec2 p, vec2 center, vec2 halfSize) { ++ vec2 d = abs(p - center) - halfSize; ++ return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); ++} ++ ++float smin(float a, float b, float k) { ++ // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) ++ float h = max(k - abs(a - b), 0.0) / k; ++ return min(a, b) - h * h * h * k * (1.0/6.0); ++} ++ ++float smax(float a, float b, float k) { ++ float h = max(k - abs(a - b), 0.0) / k; ++ return max(a, b) + h * h * h * k * (1.0/6.0); ++} ++ ++float smaxSharpA(float a, float b, float k) { ++ // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). ++ // Used for the frame outer edge so it always fills to the edges. ++ float h = max(k - abs(a - b), 0.0) / k; ++ float blend = h * h * h * k * (1.0/6.0); ++ blend *= smoothstep(0.0, k * 0.5, -a); ++ return max(a, b) + blend; ++} ++ ++void main() { ++ vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); ++ ++ // Phase 1: compute per-rect SDF, track owner. We can't smin yet because excluded ++ // pairs need to skip the smooth blend, which requires pairwise pass below. ++ float dArr[16]; ++ int owner = -2; ++ float minDist = 1e10; ++ ++ for (int i = 0; i < rectCount; i++) { ++ vec4 rect = rectData[i * 5]; // cx, cy, hw, hh ++ vec4 props = rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig ++ vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix ++ vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 ++ vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) ++ ++ // Offset center for asymmetric deformation ++ vec2 center = rect.xy + props.yz; ++ ++ // AABB early-out: skip rects far from this pixel ++ vec2 extent = sh.xy + vec2(smoothFactor * 1.5); ++ if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) { ++ dArr[i] = 1e10; ++ continue; ++ } ++ ++ // Apply pre-computed inverse deformation to the evaluation point ++ mat2 invDeform = mat2(invDm.xy, invDm.zw); ++ vec2 transformedPixel = center + invDeform * (pixel - center); ++ ++ // Use pre-computed effective per-corner radii ++ float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); ++ ++ // Use pre-computed minimum eigenvalue for SDF correction ++ d *= max(props.w, 0.01); ++ ++ // Scale SDF on the axis facing a nearby border to narrow the smin blend zone ++ // in that direction only, without reducing k (which would cause sharp edges). ++ if (hasInverted != 0) { ++ vec2 screenHalf = sh.xy; ++ ++ float distY0 = (center.y + screenHalf.y) - (invertedInner.y - invertedInner.w); ++ float distY1 = (invertedInner.y + invertedInner.w) - (center.y - screenHalf.y); ++ float distX0 = (center.x + screenHalf.x) - (invertedInner.x - invertedInner.z); ++ float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); ++ ++ // 0 = far from border, 1 = at border (max compression) ++ float yProx = 1.0 - min( ++ smoothstep(0.0, smoothFactor, distY0), ++ smoothstep(0.0, smoothFactor, distY1) ++ ); ++ float xProx = 1.0 - min( ++ smoothstep(0.0, smoothFactor, distX0), ++ smoothstep(0.0, smoothFactor, distX1) ++ ); ++ ++ // Smooth axis weights: gradient-based at corners, face-based inside. ++ vec2 q = abs(pixel - center) - screenHalf; ++ vec2 qp = max(q, vec2(0.0)); ++ float cornerLen = length(qp); ++ ++ // Gradient direction in corner region (smooth 90-degree rotation) ++ float gradX = qp.x / max(cornerLen, 0.001); ++ float gradY = qp.y / max(cornerLen, 0.001); ++ ++ // Smooth face weights for inside/edge (no hard branch) ++ float faceY = smoothstep(-4.0, 4.0, q.y - q.x); ++ float faceX = 1.0 - faceY; ++ ++ // Blend: gradient in corner region, face-based inside ++ float t = smoothstep(0.0, 2.0, cornerLen); ++ float xWeight = mix(faceX, gradX, t); ++ float yWeight = mix(faceY, gradY, t); ++ ++ float boost = 3.0; ++ float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost; ++ d *= scale; ++ } ++ ++ dArr[i] = d; ++ if (d < smoothFactor && d < minDist) { ++ minDist = d; ++ owner = i; ++ } ++ } ++ ++ // Phase 2: hard-min baseline over all rects. ++ float mergedSdf = 1e10; ++ for (int i = 0; i < rectCount; i++) { ++ mergedSdf = min(mergedSdf, dArr[i]); ++ } ++ ++ // Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin <= min, ++ // so taking the min over all non-excluded pair smins gives the smoothly-merged SDF. ++ for (int i = 0; i < rectCount; i++) { ++ if (dArr[i] >= 1e9) ++ continue; ++ int excludeMask = floatBitsToInt(rectData[i * 5 + 1].x); ++ for (int j = i + 1; j < rectCount; j++) { ++ if (dArr[j] >= 1e9) ++ continue; ++ if ((excludeMask & (1 << j)) != 0) ++ continue; ++ // smin only deviates from min within smoothFactor ++ if (abs(dArr[i] - dArr[j]) >= smoothFactor) ++ continue; ++ mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); ++ } ++ } ++ ++ if (hasInverted != 0) { ++ float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; ++ float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); ++ ++ // Border sinks: track the opposite rect edge, clamped to border thickness ++ float innerTop = invertedInner.y - invertedInner.w; ++ float innerBot = invertedInner.y + invertedInner.w; ++ float innerLeft = invertedInner.x - invertedInner.z; ++ float innerRight = invertedInner.x + invertedInner.z; ++ float outerTop = invertedOuter.y - invertedOuter.w; ++ float outerBot = invertedOuter.y + invertedOuter.w; ++ float outerLeft = invertedOuter.x - invertedOuter.z; ++ float outerRight = invertedOuter.x + invertedOuter.z; ++ ++ float sinkValue = 0.0; ++ for (int i = 0; i < rectCount; i++) { ++ vec4 rect = rectData[i * 5]; ++ vec4 sinkProps = rectData[i * 5 + 1]; ++ vec2 sinkSh = rectData[i * 5 + 3].xy; ++ ++ // Screen-space center (with offset) and pre-computed AABB half-extents ++ vec2 ctr = rect.xy + sinkProps.yz; ++ ++ // Delay sink to absorb smin blend depth (cubic smin max = k/6) ++ float preOff = smoothFactor * (1.0/6.0); ++ ++ // Top border: track rect's BOTTOM edge, only within border thickness ++ float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); ++ ++ // Bottom border: track rect's TOP edge ++ float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot); ++ ++ // Left border: track rect's RIGHT edge ++ float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft); ++ ++ // Right border: track rect's LEFT edge ++ float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight); ++ ++ // Lateral distance from pixel to rect's extent along each edge ++ float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); ++ float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); ++ ++ // Perpendicular proximity: full strength in border, fade inside inner area ++ float topZone = 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); ++ float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); ++ float leftZone = 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); ++ float rightZone = smoothstep(innerRight - smoothFactor, innerRight, pixel.x); ++ ++ float s = smoothFactor * 2.0; ++ float sink = max( ++ max(topPen * smoothstep(s, 0.0, hLat) * topZone, ++ botPen * smoothstep(s, 0.0, hLat) * botZone), ++ max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, ++ rightPen * smoothstep(s, 0.0, vLat) * rightZone) ++ ); ++ sinkValue = max(sinkValue, sink); ++ } ++ ++ dInner -= sinkValue; ++ ++ float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); ++ ++ mergedSdf = smin(mergedSdf, dFrame, smoothFactor); ++ if (dFrame < minDist) { ++ owner = -1; ++ } ++ } ++ ++ // Each renderer only outputs pixels it owns, but allow rendering ++ // blend zones to prevent gaps (mergedSdf < smoothFactor means in blend) ++ // myIndex == -1: inverted rect renders border-owned pixels ++ // myIndex >= 0: individual rect renders its owned pixels ++ if (owner != myIndex && mergedSdf > smoothFactor) ++ discard; ++ ++ float fw = fwidth(mergedSdf); ++ float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); ++ fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; ++} +diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.vert b/plugin/src/Caelestia/Blobs/shaders/blob.vert +new file mode 100644 +index 00000000..e71d8104 +--- /dev/null ++++ b/plugin/src/Caelestia/Blobs/shaders/blob.vert +@@ -0,0 +1,29 @@ ++#version 440 ++ ++layout(location = 0) in vec4 qt_VertexPosition; ++layout(location = 1) in vec2 qt_VertexTexCoord; ++ ++layout(location = 0) out vec2 qt_TexCoord0; ++ ++layout(std140, binding = 0) uniform buf { ++ mat4 qt_Matrix; ++ float qt_Opacity; ++ float paddedX; ++ float paddedY; ++ float paddedW; ++ float paddedH; ++ float smoothFactor; ++ int rectCount; ++ int myIndex; ++ vec4 color; ++ int hasInverted; ++ float invertedRadius; ++ vec4 invertedOuter; ++ vec4 invertedInner; ++ vec4 rectData[80]; ++}; ++ ++void main() { ++ gl_Position = qt_Matrix * qt_VertexPosition; ++ qt_TexCoord0 = qt_VertexTexCoord; ++} +diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt +index e4a02012..6e73f4fe 100644 +--- a/plugin/src/Caelestia/CMakeLists.txt ++++ b/plugin/src/Caelestia/CMakeLists.txt +@@ -1,20 +1,24 @@ +-find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) ++find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) + find_package(PkgConfig REQUIRED) + pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) + pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) + pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) +-pkg_check_modules(Cava IMPORTED_TARGET libcava REQUIRED) ++pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) ++if(NOT Cava_FOUND) ++ pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) ++endif() + + set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") + qt_standard_project_setup(REQUIRES 6.9) + + function(qml_module arg_TARGET) +- cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES") ++ cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;LIBRARIES") + + qt_add_qml_module(${arg_TARGET} + URI ${arg_URI} + VERSION ${VERSION} + SOURCES ${arg_SOURCES} ++ QML_FILES ${arg_QML_FILES} + ) + + qt_query_qml_module(${arg_TARGET} +@@ -34,9 +38,24 @@ function(qml_module arg_TARGET) + install(FILES "${module_qmldir}" DESTINATION "${module_dir}") + install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") + +- target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES}) ++ target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) + endfunction() + ++add_library(caelestia-pch INTERFACE) ++target_precompile_headers(caelestia-pch INTERFACE ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++ ++) ++ + qml_module(caelestia + URI Caelestia + SOURCES +@@ -54,6 +73,10 @@ qml_module(caelestia + PkgConfig::Qalculate + ) + ++add_subdirectory(Components) ++add_subdirectory(Config) + add_subdirectory(Internal) + add_subdirectory(Models) + add_subdirectory(Services) ++add_subdirectory(Blobs) ++add_subdirectory(Images) +diff --git a/plugin/src/Caelestia/Components/CMakeLists.txt b/plugin/src/Caelestia/Components/CMakeLists.txt +new file mode 100644 +index 00000000..f880d318 +--- /dev/null ++++ b/plugin/src/Caelestia/Components/CMakeLists.txt +@@ -0,0 +1,7 @@ ++qml_module(caelestia-components ++ URI Caelestia.Components ++ SOURCES ++ lazylistview.hpp lazylistview.cpp ++ LIBRARIES ++ Qt::Quick ++) +diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp +new file mode 100644 +index 00000000..36b49301 +--- /dev/null ++++ b/plugin/src/Caelestia/Components/lazylistview.cpp +@@ -0,0 +1,1108 @@ ++#include "lazylistview.hpp" ++ ++#include ++#include ++#include ++ ++namespace { ++ ++constexpr int ASYNC_BATCH_CREATE = 2; ++constexpr int ASYNC_BATCH_DESTROY = 4; ++ ++} // namespace ++ ++namespace caelestia::components { ++ ++// --- LazyListViewAttached --- ++ ++LazyListViewAttached::LazyListViewAttached(QObject* parent) ++ : QObject(parent) {} ++ ++qreal LazyListViewAttached::preferredHeight() const { ++ return m_preferredHeight; ++} ++ ++void LazyListViewAttached::setPreferredHeight(qreal height) { ++ if (qFuzzyCompare(m_preferredHeight + 1.0, height + 1.0)) ++ return; ++ m_preferredHeight = height; ++ emit preferredHeightChanged(); ++} ++ ++qreal LazyListViewAttached::visibleHeight() const { ++ return m_visibleHeight; ++} ++ ++void LazyListViewAttached::setVisibleHeight(qreal height) { ++ if (qFuzzyCompare(m_visibleHeight + 1.0, height + 1.0)) ++ return; ++ m_visibleHeight = height; ++ emit visibleHeightChanged(); ++} ++ ++bool LazyListViewAttached::ready() const { ++ return m_ready; ++} ++ ++void LazyListViewAttached::setReady(bool ready) { ++ if (m_ready == ready) ++ return; ++ m_ready = ready; ++ emit readyChanged(); ++} ++ ++bool LazyListViewAttached::adding() const { ++ return m_adding; ++} ++ ++void LazyListViewAttached::setAdding(bool adding) { ++ if (m_adding == adding) ++ return; ++ m_adding = adding; ++ emit addingChanged(); ++} ++ ++bool LazyListViewAttached::removing() const { ++ return m_removing; ++} ++ ++void LazyListViewAttached::setRemoving(bool removing) { ++ if (m_removing == removing) ++ return; ++ m_removing = removing; ++ emit removingChanged(); ++} ++ ++bool LazyListViewAttached::trackViewport() const { ++ return m_trackViewport; ++} ++ ++void LazyListViewAttached::setTrackViewport(bool track) { ++ if (m_trackViewport == track) ++ return; ++ m_trackViewport = track; ++ emit trackViewportChanged(); ++} ++ ++// --- LazyListView --- ++ ++LazyListView::LazyListView(QQuickItem* parent) ++ : QQuickItem(parent) { ++ setFlag(ItemHasContents, false); ++} ++ ++LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) { ++ return new LazyListViewAttached(object); ++} ++ ++LazyListView::~LazyListView() { ++ for (auto& entry : m_delegates) ++ destroyDelegate(entry); ++ for (auto& entry : m_dyingDelegates) ++ destroyDelegate(entry); ++} ++ ++// --- Model & Delegate --- ++ ++QAbstractItemModel* LazyListView::model() const { ++ return m_model; ++} ++ ++void LazyListView::setModel(QAbstractItemModel* model) { ++ if (m_model == model) ++ return; ++ ++ if (m_model) ++ disconnectModel(); ++ ++ m_model = model; ++ ++ if (m_model) ++ connectModel(); ++ ++ resetContent(); ++ emit modelChanged(); ++} ++ ++QQmlComponent* LazyListView::delegate() const { ++ return m_delegate; ++} ++ ++void LazyListView::setDelegate(QQmlComponent* delegate) { ++ if (m_delegate == delegate) ++ return; ++ ++ m_delegate = delegate; ++ resetContent(); ++ emit delegateChanged(); ++} ++ ++// --- Layout --- ++ ++qreal LazyListView::spacing() const { ++ return m_spacing; ++} ++ ++void LazyListView::setSpacing(qreal spacing) { ++ if (qFuzzyCompare(m_spacing, spacing)) ++ return; ++ m_spacing = spacing; ++ emit spacingChanged(); ++ polish(); ++} ++ ++qreal LazyListView::contentHeight() const { ++ return m_contentHeight; ++} ++ ++qreal LazyListView::layoutHeight() const { ++ return m_layoutHeight; ++} ++ ++qreal LazyListView::contentY() const { ++ return m_contentY; ++} ++ ++void LazyListView::setContentY(qreal contentY) { ++ if (qFuzzyCompare(m_contentY, contentY)) ++ return; ++ m_contentY = contentY; ++ emit contentYChanged(); ++ polish(); ++} ++ ++// --- Viewport --- ++ ++QRectF LazyListView::viewport() const { ++ return m_viewport; ++} ++ ++void LazyListView::setViewport(const QRectF& viewport) { ++ if (m_viewport == viewport) ++ return; ++ m_viewport = viewport; ++ emit viewportChanged(); ++ if (m_useCustomViewport) ++ polish(); ++} ++ ++bool LazyListView::useCustomViewport() const { ++ return m_useCustomViewport; ++} ++ ++void LazyListView::setUseCustomViewport(bool use) { ++ if (m_useCustomViewport == use) ++ return; ++ m_useCustomViewport = use; ++ emit useCustomViewportChanged(); ++ polish(); ++} ++ ++qreal LazyListView::cacheBuffer() const { ++ return m_cacheBuffer; ++} ++ ++void LazyListView::setCacheBuffer(qreal buffer) { ++ if (qFuzzyCompare(m_cacheBuffer, buffer)) ++ return; ++ m_cacheBuffer = buffer; ++ emit cacheBufferChanged(); ++ polish(); ++} ++ ++// --- Sizing --- ++ ++qreal LazyListView::estimatedHeight() const { ++ return m_estimatedHeight; ++} ++ ++void LazyListView::setEstimatedHeight(qreal height) { ++ if (qFuzzyCompare(m_estimatedHeight, height)) ++ return; ++ m_estimatedHeight = height; ++ emit estimatedHeightChanged(); ++ polish(); ++} ++ ++bool LazyListView::asynchronous() const { ++ return m_asynchronous; ++} ++ ++void LazyListView::setAsynchronous(bool async) { ++ if (m_asynchronous == async) ++ return; ++ m_asynchronous = async; ++ emit asynchronousChanged(); ++} ++ ++qreal LazyListView::effectiveEstimatedHeight() const { ++ if (m_estimatedHeight >= 0) ++ return m_estimatedHeight; ++ if (m_knownHeightCount > 0) ++ return m_knownHeightSum / m_knownHeightCount; ++ return 40; ++} ++ ++void LazyListView::trackHeight(qreal height) { ++ m_knownHeightSum += height; ++ ++m_knownHeightCount; ++} ++ ++void LazyListView::untrackHeight(qreal height) { ++ m_knownHeightSum -= height; ++ --m_knownHeightCount; ++} ++ ++qreal LazyListView::delegateHeight(QQuickItem* item) { ++ if (!item) ++ return 0; ++ ++ auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); ++ if (attached && attached->preferredHeight() >= 0) ++ return attached->preferredHeight(); ++ ++ return item->implicitHeight(); ++} ++ ++qreal LazyListView::delegateVisibleHeight(QQuickItem* item) { ++ if (!item) ++ return 0; ++ ++ auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); ++ if (attached) { ++ if (attached->visibleHeight() >= 0) ++ return attached->visibleHeight(); ++ if (attached->preferredHeight() >= 0) ++ return attached->preferredHeight(); ++ } ++ ++ return item->implicitHeight(); ++} ++ ++bool LazyListView::isDelegateReady(QQuickItem* item) { ++ if (!item) ++ return false; ++ auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); ++ return !att || att->ready(); ++} ++ ++// --- Animation Durations --- ++ ++int LazyListView::removeDuration() const { ++ return m_removeDuration; ++} ++ ++void LazyListView::setRemoveDuration(int duration) { ++ if (m_removeDuration == duration) ++ return; ++ m_removeDuration = duration; ++ emit removeDurationChanged(); ++} ++ ++int LazyListView::readyDelay() const { ++ return m_readyDelay; ++} ++ ++void LazyListView::setReadyDelay(int delay) { ++ if (m_readyDelay == delay) ++ return; ++ m_readyDelay = delay; ++ emit readyDelayChanged(); ++} ++ ++// --- State --- ++ ++int LazyListView::count() const { ++ return m_model ? m_model->rowCount() : 0; ++} ++ ++// --- QQuickItem Overrides --- ++ ++void LazyListView::componentComplete() { ++ QQuickItem::componentComplete(); ++ m_componentComplete = true; ++ resetContent(); ++} ++ ++void LazyListView::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { ++ QQuickItem::geometryChange(newGeometry, oldGeometry); ++ ++ if (!m_componentComplete) ++ return; ++ ++ if (!qFuzzyCompare(newGeometry.width(), oldGeometry.width())) { ++ for (auto& entry : m_delegates) { ++ if (entry.item) ++ entry.item->setWidth(newGeometry.width()); ++ } ++ } ++ ++ polish(); ++} ++ ++void LazyListView::updatePolish() { ++ if (!m_componentComplete || !m_model || !m_delegate) ++ return; ++ ++ // Flush pending inserts — make items visible and clear the adding flag ++ // so enter animations begin. When readyDelay > 0 the entire insert is ++ // deferred so delegates have time to lay out before appearing. ++ for (auto& entry : m_delegates) { ++ if (!entry.pendingInsert || !entry.item) ++ continue; ++ ++ if (m_readyDelay > 0) { ++ if (!entry.readyDelayStarted) { ++ entry.readyDelayStarted = true; ++ auto* item = entry.item; ++ QTimer::singleShot(m_readyDelay, this, [this, item] { ++ auto indexIt = m_itemToIndex.find(item); ++ if (indexIt == m_itemToIndex.end()) ++ return; ++ const int idx = indexIt.value(); ++ auto it = m_delegates.find(idx); ++ if (it == m_delegates.end() || it->item != item || !it->pendingInsert) ++ return; ++ ++ it->pendingInsert = false; ++ it->readyDelayStarted = false; ++ ++ // Set initial y to visual position (based on current visible heights) ++ if (idx >= 0 && idx < static_cast(m_layout.size())) { ++ qreal visualY = 0; ++ bool hasVisItem = false; ++ for (int i = 0; i < static_cast(m_layout.size()); ++i) { ++ qreal h; ++ auto dit = m_delegates.find(i); ++ if (dit != m_delegates.end() && dit->item) ++ h = delegateVisibleHeight(dit->item); ++ else ++ h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); ++ if (h > 0) { ++ if (hasVisItem) ++ visualY += m_spacing; ++ hasVisItem = true; ++ } ++ if (i == idx) ++ break; ++ if (h > 0) ++ visualY += h; ++ } ++ item->setY(visualY - m_contentY); ++ } ++ ++ item->setVisible(true); ++ auto* att = ++ qobject_cast(qmlAttachedPropertiesObject(item, false)); ++ if (att) { ++ att->setAdding(false); ++ att->setReady(true); ++ } ++ ++ // Animate from visual position to layout position ++ if (idx >= 0 && idx < static_cast(m_layout.size())) ++ item->setProperty("y", m_layout[idx].targetY - m_contentY); ++ ++ polish(); ++ }); ++ } ++ continue; ++ } ++ ++ entry.pendingInsert = false; ++ entry.item->setVisible(true); ++ auto* att = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); ++ if (att) { ++ att->setAdding(false); ++ att->setReady(true); ++ } ++ } ++ ++ relayout(); ++ syncDelegates(); ++ ++ // Clear isNew flags — the add animation only plays for items created ++ // during the same polish cycle as their model insertion, not for ++ // delegates created later when scrolling items into the viewport. ++ for (auto& record : m_layout) ++ record.isNew = false; ++ ++ // Position delegates — QML Behavior on y handles the animation ++ for (auto& entry : m_delegates) { ++ if (!entry.item || entry.pendingRemoval || entry.pendingInsert) ++ continue; ++ ++ const int idx = entry.modelIndex; ++ if (idx < 0 || idx >= static_cast(m_layout.size())) ++ continue; ++ ++ if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height)) ++ continue; ++ ++ // Use setProperty to go through the QML property system, ++ // which triggers Behaviors (setY bypasses them). ++ entry.item->setProperty("y", m_layout[idx].targetY - m_contentY); ++ } ++} ++ ++// --- Layout Engine --- ++ ++void LazyListView::relayout() { ++ // Layout positioning uses preferredHeight (final/non-animated). ++ // Only add spacing between items with non-zero height. ++ qreal y = 0; ++ bool hasLayoutItem = false; ++ for (auto& record : m_layout) { ++ const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); ++ if (layoutH > 0) { ++ if (hasLayoutItem) ++ y += m_spacing; ++ hasLayoutItem = true; ++ record.targetY = y; ++ y += layoutH; ++ } else { ++ record.targetY = y; ++ } ++ } ++ ++ if (!qFuzzyCompare(m_layoutHeight + 1.0, y + 1.0)) { ++ m_layoutHeight = y; ++ emit layoutHeightChanged(); ++ } ++ ++ // Content height tracks actual visible heights so scrolling follows animations. ++ // Only add spacing between items with non-zero visible height. ++ qreal visY = 0; ++ bool hasVisItem = false; ++ for (int i = 0; i < static_cast(m_layout.size()); ++i) { ++ qreal h; ++ auto dit = m_delegates.find(i); ++ if (dit != m_delegates.end() && dit->item) ++ h = delegateVisibleHeight(dit->item); ++ else ++ h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); ++ if (h > 0) { ++ if (hasVisItem) ++ visY += m_spacing; ++ hasVisItem = true; ++ visY += h; ++ } ++ } ++ ++ // Account for dying delegates still visually present ++ for (const auto& dying : std::as_const(m_dyingDelegates)) { ++ if (!dying.item) ++ continue; ++ const qreal dyingH = delegateVisibleHeight(dying.item); ++ if (dyingH > 0) ++ visY = std::max(visY, dying.item->y() + dyingH); ++ } ++ ++ if (!qFuzzyCompare(m_contentHeight + 1.0, visY + 1.0)) { ++ m_contentHeight = visY; ++ emit contentHeightChanged(); ++ } ++} ++ ++QRectF LazyListView::effectiveViewport() const { ++ QRectF vp; ++ if (m_useCustomViewport) ++ vp = m_viewport; ++ else ++ vp = QRectF(0, m_contentY, width(), height()); ++ ++ // During Flickable overshoot the viewport can extend entirely beyond content bounds, ++ // causing all delegates to be culled. Clamp so it always overlaps [0, layoutHeight]. ++ // Only needed for the built-in viewport — custom viewports represent the actual ++ // visible area and may legitimately lie entirely outside the content. ++ if (!m_useCustomViewport && m_layoutHeight > 0) { ++ const qreal top = std::min(vp.y(), m_layoutHeight); ++ const qreal bottom = std::max(vp.y() + vp.height(), 0.0); ++ if (bottom > top) ++ vp = QRectF(vp.x(), top, vp.width(), bottom - top); ++ } ++ ++ vp.adjust(0, -m_cacheBuffer, 0, m_cacheBuffer); ++ ++ // Trim the cache-buffered viewport to [0, layoutHeight]. No items exist outside ++ // those bounds, so extending past them wastes budget and can cause edge thrashing ++ // when a large cache buffer reaches the opposite end of the content. ++ if (m_layoutHeight > 0) { ++ const qreal top = std::max(vp.y(), 0.0); ++ const qreal bottom = std::min(vp.y() + vp.height(), m_layoutHeight); ++ if (top < bottom) ++ vp = QRectF(vp.x(), top, vp.width(), bottom - top); ++ else ++ return {}; ++ } ++ ++ return vp; ++} ++ ++std::pair LazyListView::computeVisibleRange() const { ++ if (m_layout.isEmpty()) ++ return { -1, -1 }; ++ ++ const auto vp = effectiveViewport(); ++ if (vp.isEmpty()) ++ return { -1, -1 }; ++ ++ const qreal vpTop = vp.y(); ++ const qreal vpBottom = vp.y() + vp.height(); ++ ++ // Binary search for first visible item ++ int lo = 0; ++ int hi = static_cast(m_layout.size()) - 1; ++ int first = static_cast(m_layout.size()); ++ ++ while (lo <= hi) { ++ const int mid = lo + (hi - lo) / 2; ++ const auto& record = m_layout[mid]; ++ const qreal itemBottom = record.targetY + (record.heightKnown ? record.height : effectiveEstimatedHeight()); ++ ++ if (itemBottom >= vpTop) { ++ first = mid; ++ hi = mid - 1; ++ } else { ++ lo = mid + 1; ++ } ++ } ++ ++ if (first >= static_cast(m_layout.size())) ++ return { -1, -1 }; ++ ++ // Linear scan for last visible item ++ int last = first; ++ for (int i = first; i < static_cast(m_layout.size()); ++i) { ++ if (m_layout[i].targetY > vpBottom) ++ break; ++ last = i; ++ } ++ ++ return { first, last }; ++} ++ ++// --- Delegate Lifecycle --- ++ ++void LazyListView::syncDelegates() { ++ const auto [first, last] = computeVisibleRange(); ++ ++ // Collect indices that should be alive ++ QSet visibleIndices; ++ if (first >= 0) { ++ for (int i = first; i <= last; ++i) ++ visibleIndices.insert(i); ++ } ++ ++ // Collect delegates to destroy — only if visually outside the viewport ++ const auto vp = effectiveViewport(); ++ QList toRemove; ++ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { ++ if (visibleIndices.contains(it.key())) ++ continue; ++ if (!it->item || vp.isEmpty()) { ++ toRemove.append(it.key()); ++ continue; ++ } ++ const qreal itemTop = it->item->y(); ++ const qreal itemBottom = itemTop + delegateVisibleHeight(it->item); ++ if (itemBottom < vp.top() || itemTop > vp.bottom()) ++ toRemove.append(it.key()); ++ } ++ ++ // Batch destroy ++ const int destroyBudget = m_asynchronous ? ASYNC_BATCH_DESTROY : static_cast(toRemove.size()); ++ QVector removedEntries; ++ removedEntries.reserve(std::min(destroyBudget, static_cast(toRemove.size()))); ++ int destroyed = 0; ++ for (int idx : toRemove) { ++ if (destroyed >= destroyBudget) ++ break; ++ auto entry = m_delegates.take(idx); ++ if (entry.item) ++ m_itemToIndex.remove(entry.item); ++ removedEntries.append(std::move(entry)); ++ ++destroyed; ++ } ++ for (auto& entry : removedEntries) ++ destroyDelegate(entry); ++ ++ // Collect indices to create ++ QList toCreate; ++ if (first >= 0) { ++ for (int i = first; i <= last; ++i) { ++ if (!m_delegates.contains(i)) ++ toCreate.append(i); ++ } ++ } ++ ++ // Batch create ++ const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast(toCreate.size()); ++ int created = 0; ++ for (int i : toCreate) { ++ if (created >= createBudget) ++ break; ++ ++ auto entry = createDelegate(i); ++ if (entry.item) { ++ // Height tracking and viewport compensation are deferred ++ // until the delegate signals ready via readyChanged. ++ entry.pendingInsert = true; ++ entry.item->setY(m_layout[i].targetY - m_contentY); ++ m_itemToIndex.insert(entry.item, i); ++ m_delegates.insert(i, std::move(entry)); ++ ++created; ++ } ++ } ++ ++ // Pending inserts need to become visible on the next frame, and ++ // async mode may have remaining create/destroy work. ++ if (created > 0 || (m_asynchronous && (destroyed < static_cast(toRemove.size()) || ++ created < static_cast(toCreate.size())))) ++ polish(); ++} ++ ++LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { ++ DelegateEntry entry; ++ entry.modelIndex = modelIndex; ++ ++ if (!m_delegate || !m_model) ++ return entry; ++ ++ const auto roleNames = m_model->roleNames(); ++ ++ // Use the delegate component's creation context for beginCreate ++ // so bound components (pragma ComponentBehavior: Bound) are accepted. ++ auto* compContext = m_delegate->creationContext(); ++ if (!compContext) ++ compContext = qmlContext(this); ++ if (!compContext) ++ return entry; ++ ++ auto* obj = m_delegate->beginCreate(compContext); ++ entry.item = qobject_cast(obj); ++ ++ if (!entry.item) { ++ if (obj) ++ m_delegate->completeCreate(); ++ delete obj; ++ return entry; ++ } ++ ++ // Build initial properties from model data ++ const auto index = m_model->index(modelIndex, 0); ++ QVariantMap initialProps; ++ bool hasModelData = false; ++ ++ for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { ++ const auto name = QString::fromUtf8(it.value()); ++ initialProps.insert(name, m_model->data(index, it.key())); ++ if (name == QStringLiteral("modelData")) ++ hasModelData = true; ++ } ++ initialProps.insert(QStringLiteral("index"), modelIndex); ++ ++ if (!hasModelData) { ++ const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); ++ initialProps.insert(QStringLiteral("modelData"), m_model->data(index, role)); ++ } ++ ++ m_delegate->setInitialProperties(entry.item, initialProps); ++ ++ entry.item->setParentItem(this); ++ entry.item->setWidth(width()); ++ ++ // Only set adding = true for genuinely new model items (not viewport entries). ++ // Cleared on the next frame in updatePolish when the item becomes visible. ++ if (modelIndex < static_cast(m_layout.size()) && m_layout[modelIndex].isNew) { ++ auto* addingAttached = ++ qobject_cast(qmlAttachedPropertiesObject(entry.item, true)); ++ if (addingAttached) ++ addingAttached->setAdding(true); ++ } ++ ++ m_delegate->completeCreate(); ++ ++ // Keep adding=true and hide — flushed on the next frame in updatePolish ++ entry.item->setVisible(false); ++ ++ // Height-change handler — uses m_itemToIndex for O(1) lookup. ++ // Ignored while the delegate is not yet ready. ++ auto onHeightChanged = [this, item = entry.item] { ++ if (!isDelegateReady(item)) ++ return; ++ auto indexIt = m_itemToIndex.find(item); ++ if (indexIt == m_itemToIndex.end()) ++ return; ++ const int idx = indexIt.value(); ++ auto delegateIt = m_delegates.find(idx); ++ if (delegateIt == m_delegates.end() || delegateIt->item != item) ++ return; ++ const qreal h = delegateHeight(item); ++ if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) { ++ const qreal oldH = m_layout[idx].height; ++ const bool wasKnown = m_layout[idx].heightKnown; ++ m_layout[idx].height = h; ++ m_layout[idx].heightKnown = true; ++ if (wasKnown) ++ untrackHeight(oldH); ++ trackHeight(h); ++ ++ // If this tracked item is above the viewport, emit a ++ // compensation delta so the consumer can adjust scroll. ++ if (wasKnown) { ++ auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); ++ if (att && att->trackViewport()) { ++ const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; ++ if (m_layout[idx].targetY < vpTop) ++ emit viewportAdjustNeeded(h - oldH); ++ } ++ } ++ ++ if (!m_relayoutPending) { ++ m_relayoutPending = true; ++ QTimer::singleShot(0, this, [this] { ++ m_relayoutPending = false; ++ relayout(); ++ polish(); ++ }); ++ } ++ } ++ }; ++ ++ // Watch implicitHeight as fallback ++ connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); ++ ++ // Watch attached properties if the delegate uses them ++ auto* attached = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); ++ if (attached) { ++ connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); ++ connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] { ++ polish(); ++ }); ++ connect(attached, &LazyListViewAttached::readyChanged, this, [this, item = entry.item] { ++ auto indexIt = m_itemToIndex.find(item); ++ if (indexIt == m_itemToIndex.end()) ++ return; ++ const int idx = indexIt.value(); ++ if (idx >= static_cast(m_layout.size())) ++ return; ++ auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); ++ if (!att || !att->ready()) ++ return; ++ ++ const qreal h = delegateHeight(item); ++ const qreal oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight(); ++ if (m_layout[idx].heightKnown) ++ untrackHeight(m_layout[idx].height); ++ m_layout[idx].height = h; ++ m_layout[idx].heightKnown = true; ++ trackHeight(h); ++ ++ if (att->trackViewport() && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) { ++ const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; ++ if (m_layout[idx].targetY < vpTop) ++ emit viewportAdjustNeeded(h - oldLayoutH); ++ } ++ ++ polish(); ++ }); ++ } ++ ++ return entry; ++} ++ ++void LazyListView::destroyDelegate(DelegateEntry& entry) { ++ if (entry.item) { ++ entry.item->setParentItem(nullptr); ++ entry.item->setVisible(false); ++ entry.item->deleteLater(); ++ entry.item = nullptr; ++ } ++} ++ ++void LazyListView::updateDelegateData(DelegateEntry& entry) { ++ if (!m_model || !entry.item) ++ return; ++ ++ const auto roleNames = m_model->roleNames(); ++ const auto index = m_model->index(entry.modelIndex, 0); ++ bool hasModelData = false; ++ ++ for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { ++ const auto name = QString::fromUtf8(it.value()); ++ entry.item->setProperty(name.toUtf8().constData(), m_model->data(index, it.key())); ++ if (name == QStringLiteral("modelData")) ++ hasModelData = true; ++ } ++ ++ entry.item->setProperty("index", entry.modelIndex); ++ ++ if (!hasModelData) { ++ const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); ++ entry.item->setProperty("modelData", m_model->data(index, role)); ++ } ++} ++ ++// --- Model Connection --- ++ ++void LazyListView::connectModel() { ++ if (!m_model) ++ return; ++ ++ m_modelConnections = { ++ connect(m_model, &QAbstractItemModel::rowsInserted, this, &LazyListView::onRowsInserted), ++ connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &LazyListView::onRowsAboutToBeRemoved), ++ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LazyListView::onRowsRemoved), ++ connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), ++ connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), ++ connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), ++ connect(m_model, &QAbstractItemModel::layoutChanged, this, ++ [this] { ++ for (auto& entry : m_delegates) ++ updateDelegateData(entry); ++ polish(); ++ }), ++ connect(m_model, &QObject::destroyed, this, ++ [this] { ++ m_model = nullptr; ++ resetContent(); ++ emit modelChanged(); ++ }), ++ }; ++} ++ ++void LazyListView::disconnectModel() { ++ for (auto& conn : m_modelConnections) ++ disconnect(conn); ++ m_modelConnections.clear(); ++} ++ ++void LazyListView::resetContent() { ++ // Stop all animations and destroy all delegates ++ for (auto& entry : m_delegates) ++ destroyDelegate(entry); ++ m_delegates.clear(); ++ m_itemToIndex.clear(); ++ ++ for (auto& entry : m_dyingDelegates) ++ destroyDelegate(entry); ++ m_dyingDelegates.clear(); ++ ++ // Reset pending state ++ m_knownHeightSum = 0; ++ m_knownHeightCount = 0; ++ ++ // Rebuild layout from model ++ m_layout.clear(); ++ if (m_model && m_componentComplete) { ++ const int rows = m_model->rowCount(); ++ m_layout.resize(rows); ++ for (int i = 0; i < rows; ++i) { ++ m_layout[i].height = 0; ++ m_layout[i].heightKnown = false; ++ } ++ emit countChanged(); ++ } ++ ++ polish(); ++} ++ ++void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last) { ++ if (parent.isValid()) ++ return; ++ ++ const int insertCount = last - first + 1; ++ // Insert new layout records ++ m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false, true }); ++ ++ // Shift existing delegate indices ++ QHash shifted; ++ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { ++ int newIdx = it.key() >= first ? it.key() + insertCount : it.key(); ++ auto entry = std::move(it.value()); ++ entry.modelIndex = newIdx; ++ if (entry.item) { ++ entry.item->setProperty("index", newIdx); ++ m_itemToIndex[entry.item] = newIdx; ++ } ++ shifted.insert(newIdx, std::move(entry)); ++ } ++ m_delegates = std::move(shifted); ++ ++ emit countChanged(); ++ polish(); ++} ++ ++void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { ++ if (parent.isValid()) ++ return; ++ ++ for (int i = first; i <= last; ++i) { ++ if (!m_delegates.contains(i)) ++ continue; ++ ++ auto entry = m_delegates.take(i); ++ if (entry.item) ++ m_itemToIndex.remove(entry.item); ++ entry.pendingRemoval = true; ++ ++ // Never made visible — skip remove animation ++ if (entry.pendingInsert) { ++ destroyDelegate(entry); ++ continue; ++ } ++ ++ if (m_removeDuration > 0 && entry.item) { ++ auto* attached = ++ qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); ++ if (attached) ++ attached->setRemoving(true); ++ ++ // Schedule destruction after the remove animation duration ++ auto* item = entry.item; ++ QTimer::singleShot(m_removeDuration, this, [this, item] { ++ for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) { ++ if (it->item == item) { ++ destroyDelegate(*it); ++ m_dyingDelegates.erase(it); ++ return; ++ } ++ } ++ }); ++ m_dyingDelegates.append(std::move(entry)); ++ } else { ++ destroyDelegate(entry); ++ } ++ } ++} ++ ++void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) { ++ if (parent.isValid()) ++ return; ++ ++ const int removeCount = last - first + 1; ++ ++ // Untrack known heights being removed ++ for (int i = first; i <= last; ++i) { ++ if (m_layout[i].heightKnown) ++ untrackHeight(m_layout[i].height); ++ } ++ ++ // Remove layout records ++ m_layout.remove(first, removeCount); ++ ++ // Shift remaining delegate indices down ++ QHash shifted; ++ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { ++ int newIdx = it.key() > last ? it.key() - removeCount : it.key(); ++ auto entry = std::move(it.value()); ++ entry.modelIndex = newIdx; ++ if (entry.item) { ++ entry.item->setProperty("index", newIdx); ++ m_itemToIndex[entry.item] = newIdx; ++ } ++ shifted.insert(newIdx, std::move(entry)); ++ } ++ m_delegates = std::move(shifted); ++ ++ emit countChanged(); ++ polish(); ++} ++ ++void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { ++ if (parent.isValid() || destination.isValid()) ++ return; ++ ++ const int count = end - start + 1; ++ const int dest = row > start ? row - count : row; ++ ++ // Reorder layout records ++ QVector moved; ++ moved.reserve(count); ++ for (int i = start; i <= end; ++i) ++ moved.append(m_layout[i]); ++ m_layout.remove(start, count); ++ for (int i = 0; i < count; ++i) ++ m_layout.insert(dest + i, moved[i]); ++ ++ // Remap delegate indices to match new model order ++ QHash remapped; ++ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { ++ int oldIdx = it.key(); ++ int newIdx = oldIdx; ++ ++ if (oldIdx >= start && oldIdx <= end) { ++ newIdx = dest + (oldIdx - start); ++ } else { ++ if (oldIdx > end) ++ newIdx -= count; ++ if (newIdx >= dest) ++ newIdx += count; ++ } ++ ++ auto entry = std::move(it.value()); ++ entry.modelIndex = newIdx; ++ if (entry.item) { ++ entry.item->setProperty("index", newIdx); ++ m_itemToIndex[entry.item] = newIdx; ++ } ++ remapped.insert(newIdx, std::move(entry)); ++ } ++ m_delegates = std::move(remapped); ++ ++ polish(); ++} ++ ++void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) { ++ Q_UNUSED(roles) ++ ++ if (topLeft.parent().isValid()) ++ return; ++ ++ for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { ++ if (m_delegates.contains(i)) ++ updateDelegateData(m_delegates[i]); ++ } ++} ++ ++void LazyListView::onModelReset() { ++ if (!m_model) { ++ resetContent(); ++ return; ++ } ++ ++ const int newRows = m_model->rowCount(); ++ const int oldRows = static_cast(m_layout.size()); ++ ++ // Check if the model data actually changed ++ if (newRows == oldRows) { ++ const auto roleNames = m_model->roleNames(); ++ const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); ++ bool changed = false; ++ ++ for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) { ++ if (!it->item || it.key() >= newRows) { ++ changed = true; ++ break; ++ } ++ const auto newData = m_model->data(m_model->index(it.key(), 0), role); ++ const auto oldData = it->item->property("modelData"); ++ if (newData != oldData) { ++ changed = true; ++ break; ++ } ++ } ++ ++ if (!changed) { ++ // Model content unchanged, just refresh delegate data ++ for (auto& entry : m_delegates) ++ updateDelegateData(entry); ++ return; ++ } ++ } ++ ++ resetContent(); ++} ++ ++} // namespace caelestia::components +diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp +new file mode 100644 +index 00000000..e0746db2 +--- /dev/null ++++ b/plugin/src/Caelestia/Components/lazylistview.hpp +@@ -0,0 +1,243 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace caelestia::components { ++ ++class LazyListViewAttached : public QObject { ++ Q_OBJECT ++ ++ Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) ++ Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged) ++ Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) ++ Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) ++ Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) ++ Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged) ++ ++public: ++ explicit LazyListViewAttached(QObject* parent = nullptr); ++ ++ [[nodiscard]] qreal preferredHeight() const; ++ void setPreferredHeight(qreal height); ++ ++ [[nodiscard]] qreal visibleHeight() const; ++ void setVisibleHeight(qreal height); ++ ++ [[nodiscard]] bool ready() const; ++ void setReady(bool ready); ++ ++ [[nodiscard]] bool adding() const; ++ void setAdding(bool adding); ++ ++ [[nodiscard]] bool removing() const; ++ void setRemoving(bool removing); ++ ++ [[nodiscard]] bool trackViewport() const; ++ void setTrackViewport(bool track); ++ ++signals: ++ void preferredHeightChanged(); ++ void visibleHeightChanged(); ++ void readyChanged(); ++ void addingChanged(); ++ void removingChanged(); ++ void trackViewportChanged(); ++ ++private: ++ qreal m_preferredHeight = -1; ++ qreal m_visibleHeight = -1; ++ bool m_ready = false; ++ bool m_adding = false; ++ bool m_removing = false; ++ bool m_trackViewport = false; ++}; ++ ++class LazyListView : public QQuickItem { ++ Q_OBJECT ++ QML_ELEMENT ++ QML_ATTACHED(LazyListViewAttached) ++ ++ // Model & Delegate ++ Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged) ++ Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) ++ ++ // Layout ++ Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) ++ Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged) ++ Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged) ++ Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) ++ ++ // Viewport & Lazy Loading ++ Q_PROPERTY(QRectF viewport READ viewport WRITE setViewport NOTIFY viewportChanged) ++ Q_PROPERTY(bool useCustomViewport READ useCustomViewport WRITE setUseCustomViewport NOTIFY useCustomViewportChanged) ++ Q_PROPERTY(qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged) ++ ++ // Sizing ++ Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged) ++ ++ // Async ++ Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged) ++ ++ // Animation Durations ++ Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged) ++ Q_PROPERTY(int readyDelay READ readyDelay WRITE setReadyDelay NOTIFY readyDelayChanged) ++ ++ // State ++ Q_PROPERTY(int count READ count NOTIFY countChanged) ++ ++public: ++ explicit LazyListView(QQuickItem* parent = nullptr); ++ ~LazyListView() override; ++ ++ static LazyListViewAttached* qmlAttachedProperties(QObject* object); ++ ++ // Model & Delegate ++ [[nodiscard]] QAbstractItemModel* model() const; ++ void setModel(QAbstractItemModel* model); ++ ++ [[nodiscard]] QQmlComponent* delegate() const; ++ void setDelegate(QQmlComponent* delegate); ++ ++ // Layout ++ [[nodiscard]] qreal spacing() const; ++ void setSpacing(qreal spacing); ++ ++ [[nodiscard]] qreal contentHeight() const; ++ [[nodiscard]] qreal layoutHeight() const; ++ ++ [[nodiscard]] qreal contentY() const; ++ void setContentY(qreal contentY); ++ ++ // Viewport ++ [[nodiscard]] QRectF viewport() const; ++ void setViewport(const QRectF& viewport); ++ ++ [[nodiscard]] bool useCustomViewport() const; ++ void setUseCustomViewport(bool use); ++ ++ [[nodiscard]] qreal cacheBuffer() const; ++ void setCacheBuffer(qreal buffer); ++ ++ // Sizing ++ [[nodiscard]] qreal estimatedHeight() const; ++ void setEstimatedHeight(qreal height); ++ ++ // Async ++ [[nodiscard]] bool asynchronous() const; ++ void setAsynchronous(bool async); ++ ++ // Animation Durations ++ [[nodiscard]] int removeDuration() const; ++ void setRemoveDuration(int duration); ++ ++ [[nodiscard]] int readyDelay() const; ++ void setReadyDelay(int delay); ++ ++ // State ++ [[nodiscard]] int count() const; ++signals: ++ void modelChanged(); ++ void delegateChanged(); ++ void spacingChanged(); ++ void contentHeightChanged(); ++ void layoutHeightChanged(); ++ void contentYChanged(); ++ void viewportChanged(); ++ void useCustomViewportChanged(); ++ void cacheBufferChanged(); ++ void estimatedHeightChanged(); ++ void asynchronousChanged(); ++ void removeDurationChanged(); ++ void readyDelayChanged(); ++ void countChanged(); ++ void viewportAdjustNeeded(qreal delta); ++ ++protected: ++ void componentComplete() override; ++ void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; ++ void updatePolish() override; ++ ++private: ++ struct ItemRecord { ++ qreal targetY = 0; ++ qreal height = 0; ++ bool heightKnown = false; ++ bool isNew = false; ++ }; ++ ++ struct DelegateEntry { ++ int modelIndex = -1; ++ QQuickItem* item = nullptr; ++ bool pendingRemoval = false; ++ bool pendingInsert = false; ++ bool readyDelayStarted = false; ++ }; ++ ++ // Layout ++ void relayout(); ++ [[nodiscard]] std::pair computeVisibleRange() const; ++ [[nodiscard]] QRectF effectiveViewport() const; ++ [[nodiscard]] qreal effectiveEstimatedHeight() const; ++ [[nodiscard]] static qreal delegateHeight(QQuickItem* item); ++ [[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item); ++ [[nodiscard]] static bool isDelegateReady(QQuickItem* item); ++ void trackHeight(qreal height); ++ void untrackHeight(qreal height); ++ ++ // Delegate lifecycle ++ void syncDelegates(); ++ DelegateEntry createDelegate(int modelIndex); ++ void destroyDelegate(DelegateEntry& entry); ++ void updateDelegateData(DelegateEntry& entry); ++ ++ // Model connection ++ void connectModel(); ++ void disconnectModel(); ++ void resetContent(); ++ void onRowsInserted(const QModelIndex& parent, int first, int last); ++ void onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); ++ void onRowsRemoved(const QModelIndex& parent, int first, int last); ++ void onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row); ++ void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles); ++ void onModelReset(); ++ ++ // Members ++ QAbstractItemModel* m_model = nullptr; ++ QQmlComponent* m_delegate = nullptr; ++ ++ qreal m_spacing = 0; ++ qreal m_contentHeight = 0; ++ qreal m_layoutHeight = 0; ++ qreal m_contentY = 0; ++ ++ QRectF m_viewport; ++ bool m_useCustomViewport = false; ++ qreal m_cacheBuffer = 0; ++ ++ qreal m_estimatedHeight = -1; ++ qreal m_knownHeightSum = 0; ++ int m_knownHeightCount = 0; ++ bool m_asynchronous = false; ++ ++ int m_removeDuration = 300; ++ int m_readyDelay = 0; ++ ++ QVector m_layout; ++ QHash m_delegates; ++ QHash m_itemToIndex; ++ QVector m_dyingDelegates; ++ ++ bool m_componentComplete = false; ++ bool m_relayoutPending = false; ++ ++ QList m_modelConnections; ++}; ++ ++} // namespace caelestia::components +diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt +new file mode 100644 +index 00000000..4b26ea02 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/CMakeLists.txt +@@ -0,0 +1,32 @@ ++qml_module(caelestia-config ++ URI Caelestia.Config ++ SOURCES ++ config.cpp ++ configattached.cpp ++ configobject.cpp ++ rootconfig.cpp ++ appearanceconfig.cpp ++ tokens.cpp ++ tokensattached.cpp ++ anim.cpp ++ monitorconfigmanager.cpp ++ backgroundconfig.hpp ++ barconfig.hpp ++ borderconfig.hpp ++ controlcenterconfig.hpp ++ dashboardconfig.hpp ++ generalconfig.hpp ++ launcherconfig.hpp ++ lockconfig.hpp ++ notifsconfig.hpp ++ osdconfig.hpp ++ serviceconfig.hpp ++ sessionconfig.hpp ++ sidebarconfig.hpp ++ userpaths.hpp ++ utilitiesconfig.hpp ++ winfoconfig.hpp ++ LIBRARIES ++ Qt::Quick ++ Qt::QuickControls2 ++) +diff --git a/plugin/src/Caelestia/Config/anim.cpp b/plugin/src/Caelestia/Config/anim.cpp +new file mode 100644 +index 00000000..e307e2f9 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/anim.cpp +@@ -0,0 +1,117 @@ ++#include "anim.hpp" ++#include "appearanceconfig.hpp" ++#include "tokens.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++AnimTokens::AnimTokens(QObject* parent) ++ : QObject(parent) {} ++ ++QEasingCurve AnimTokens::emphasized() const { ++ return m_emphasized; ++} ++ ++QEasingCurve AnimTokens::emphasizedAccel() const { ++ return m_emphasizedAccel; ++} ++ ++QEasingCurve AnimTokens::emphasizedDecel() const { ++ return m_emphasizedDecel; ++} ++ ++QEasingCurve AnimTokens::standard() const { ++ return m_standard; ++} ++ ++QEasingCurve AnimTokens::standardAccel() const { ++ return m_standardAccel; ++} ++ ++QEasingCurve AnimTokens::standardDecel() const { ++ return m_standardDecel; ++} ++ ++QEasingCurve AnimTokens::expressiveFastSpatial() const { ++ return m_expressiveFastSpatial; ++} ++ ++QEasingCurve AnimTokens::expressiveDefaultSpatial() const { ++ return m_expressiveDefaultSpatial; ++} ++ ++QEasingCurve AnimTokens::expressiveSlowSpatial() const { ++ return m_expressiveSlowSpatial; ++} ++ ++QEasingCurve AnimTokens::expressiveFastEffects() const { ++ return m_expressiveFastEffects; ++} ++ ++QEasingCurve AnimTokens::expressiveDefaultEffects() const { ++ return m_expressiveDefaultEffects; ++} ++ ++QEasingCurve AnimTokens::expressiveSlowEffects() const { ++ return m_expressiveSlowEffects; ++} ++ ++AnimDurations* AnimTokens::durations() const { ++ return m_durations; ++} ++ ++QEasingCurve AnimTokens::buildCurve(const QList& points) { ++ QEasingCurve curve(QEasingCurve::BezierSpline); ++ ++ // Points come in pairs of (x, y) forming cubic bezier segments. ++ // Each segment needs 3 control points: c1, c2, endPoint. ++ // So 6 values per segment: c1x, c1y, c2x, c2y, endX, endY. ++ for (int i = 0; i + 5 < points.size(); i += 6) { ++ QPointF c1(points[i], points[i + 1]); ++ QPointF c2(points[i + 2], points[i + 3]); ++ QPointF end(points[i + 4], points[i + 5]); ++ curve.addCubicBezierSegment(c1, c2, end); ++ } ++ ++ return curve; ++} ++ ++void AnimTokens::rebuildCurves() { ++ if (!m_curves) ++ return; ++ ++ m_emphasized = buildCurve(m_curves->emphasized()); ++ m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel()); ++ m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel()); ++ m_standard = buildCurve(m_curves->standard()); ++ m_standardAccel = buildCurve(m_curves->standardAccel()); ++ m_standardDecel = buildCurve(m_curves->standardDecel()); ++ m_expressiveFastSpatial = buildCurve(m_curves->expressiveFastSpatial()); ++ m_expressiveDefaultSpatial = buildCurve(m_curves->expressiveDefaultSpatial()); ++ m_expressiveSlowSpatial = buildCurve(m_curves->expressiveSlowSpatial()); ++ m_expressiveFastEffects = buildCurve(m_curves->expressiveFastEffects()); ++ m_expressiveDefaultEffects = buildCurve(m_curves->expressiveDefaultEffects()); ++ m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects()); ++ ++ emit curvesChanged(); ++} ++ ++void AnimTokens::bindCurves(AnimCurves* curves) { ++ m_curves = curves; ++ ++ // Rebuild when any curve control points change ++ connect(curves, &AnimCurves::propertiesChanged, this, &AnimTokens::rebuildCurves); ++ ++ rebuildCurves(); ++} ++ ++void AnimTokens::bindDurations(AnimDurations* durations) { ++ if (m_durations == durations) ++ return; ++ ++ m_durations = durations; ++ emit durationsChanged(); ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/anim.hpp b/plugin/src/Caelestia/Config/anim.hpp +new file mode 100644 +index 00000000..895b8e06 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/anim.hpp +@@ -0,0 +1,78 @@ ++#pragma once ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++class AnimCurves; ++class AnimDurations; ++ ++class AnimTokens : public QObject { ++ Q_OBJECT ++ Q_MOC_INCLUDE("tokens.hpp") // AnimCurves ++ Q_MOC_INCLUDE("appearanceconfig.hpp") // AnimDurations ++ QML_ANONYMOUS ++ ++ Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve emphasizedAccel READ emphasizedAccel NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve emphasizedDecel READ emphasizedDecel NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve standard READ standard NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve standardAccel READ standardAccel NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve standardDecel READ standardDecel NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve expressiveFastSpatial READ expressiveFastSpatial NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve expressiveFastEffects READ expressiveFastEffects NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY curvesChanged) ++ Q_PROPERTY(QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY curvesChanged) ++ ++ Q_PROPERTY(caelestia::config::AnimDurations* durations READ durations NOTIFY durationsChanged) ++ ++public: ++ explicit AnimTokens(QObject* parent = nullptr); ++ ++ void bindCurves(AnimCurves* curves); ++ void bindDurations(AnimDurations* durations); ++ ++ [[nodiscard]] QEasingCurve emphasized() const; ++ [[nodiscard]] QEasingCurve emphasizedAccel() const; ++ [[nodiscard]] QEasingCurve emphasizedDecel() const; ++ [[nodiscard]] QEasingCurve standard() const; ++ [[nodiscard]] QEasingCurve standardAccel() const; ++ [[nodiscard]] QEasingCurve standardDecel() const; ++ [[nodiscard]] QEasingCurve expressiveFastSpatial() const; ++ [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const; ++ [[nodiscard]] QEasingCurve expressiveSlowSpatial() const; ++ [[nodiscard]] QEasingCurve expressiveFastEffects() const; ++ [[nodiscard]] QEasingCurve expressiveDefaultEffects() const; ++ [[nodiscard]] QEasingCurve expressiveSlowEffects() const; ++ [[nodiscard]] AnimDurations* durations() const; ++ ++signals: ++ void curvesChanged(); ++ void durationsChanged(); ++ ++private: ++ void rebuildCurves(); ++ static QEasingCurve buildCurve(const QList& points); ++ ++ AnimCurves* m_curves = nullptr; ++ AnimDurations* m_durations = nullptr; ++ ++ QEasingCurve m_emphasized; ++ QEasingCurve m_emphasizedAccel; ++ QEasingCurve m_emphasizedDecel; ++ QEasingCurve m_standard; ++ QEasingCurve m_standardAccel; ++ QEasingCurve m_standardDecel; ++ QEasingCurve m_expressiveFastSpatial; ++ QEasingCurve m_expressiveDefaultSpatial; ++ QEasingCurve m_expressiveSlowSpatial; ++ QEasingCurve m_expressiveFastEffects; ++ QEasingCurve m_expressiveDefaultEffects; ++ QEasingCurve m_expressiveSlowEffects; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp +new file mode 100644 +index 00000000..59228c67 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp +@@ -0,0 +1,183 @@ ++#include "appearanceconfig.hpp" ++#include "tokens.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++// Helper: connect all changed signals from a token object to a single valuesChanged signal, ++// plus connect the local scaleChanged signal. ++template static void connectTokenSignals(Source* source, Target* target) { ++ const auto* meta = source->metaObject(); ++ ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ auto prop = meta->property(i); ++ ++ if (prop.hasNotifySignal()) ++ QObject::connect(source, prop.notifySignal(), target, ++ target->metaObject()->method(target->metaObject()->indexOfSignal("valuesChanged()"))); ++ } ++ ++ QObject::connect(target, &Target::scaleChanged, target, &Target::valuesChanged); ++} ++ ++// AppearanceRounding ++ ++void AppearanceRounding::bindTokens(RoundingTokens* tokens) { ++ m_tokens = tokens; ++ connectTokenSignals(tokens, this); ++} ++ ++int AppearanceRounding::extraSmall() const { ++ return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; ++} ++ ++int AppearanceRounding::small() const { ++ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; ++} ++ ++int AppearanceRounding::normal() const { ++ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; ++} ++ ++int AppearanceRounding::large() const { ++ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; ++} ++ ++int AppearanceRounding::full() const { ++ return m_tokens ? static_cast(m_tokens->full() * m_scale) : 0; ++} ++ ++// AppearanceSpacing ++ ++void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { ++ m_tokens = tokens; ++ connectTokenSignals(tokens, this); ++} ++ ++int AppearanceSpacing::small() const { ++ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; ++} ++ ++int AppearanceSpacing::smaller() const { ++ return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; ++} ++ ++int AppearanceSpacing::normal() const { ++ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; ++} ++ ++int AppearanceSpacing::larger() const { ++ return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; ++} ++ ++int AppearanceSpacing::large() const { ++ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; ++} ++ ++// AppearancePadding ++ ++void AppearancePadding::bindTokens(PaddingTokens* tokens) { ++ m_tokens = tokens; ++ connectTokenSignals(tokens, this); ++} ++ ++int AppearancePadding::small() const { ++ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; ++} ++ ++int AppearancePadding::smaller() const { ++ return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; ++} ++ ++int AppearancePadding::normal() const { ++ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; ++} ++ ++int AppearancePadding::larger() const { ++ return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; ++} ++ ++int AppearancePadding::large() const { ++ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; ++} ++ ++// FontSize ++ ++void FontSize::bindTokens(FontSizeTokens* tokens) { ++ m_tokens = tokens; ++ connectTokenSignals(tokens, this); ++} ++ ++int FontSize::small() const { ++ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; ++} ++ ++int FontSize::smaller() const { ++ return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; ++} ++ ++int FontSize::normal() const { ++ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; ++} ++ ++int FontSize::larger() const { ++ return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; ++} ++ ++int FontSize::large() const { ++ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; ++} ++ ++int FontSize::extraLarge() const { ++ return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; ++} ++ ++// AnimDurations ++ ++void AnimDurations::bindTokens(AnimDurationTokens* tokens) { ++ m_tokens = tokens; ++ connectTokenSignals(tokens, this); ++} ++ ++int AnimDurations::small() const { ++ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; ++} ++ ++int AnimDurations::normal() const { ++ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; ++} ++ ++int AnimDurations::large() const { ++ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; ++} ++ ++int AnimDurations::extraLarge() const { ++ return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; ++} ++ ++int AnimDurations::expressiveFastSpatial() const { ++ return m_tokens ? static_cast(m_tokens->expressiveFastSpatial() * m_scale) : 0; ++} ++ ++int AnimDurations::expressiveDefaultSpatial() const { ++ return m_tokens ? static_cast(m_tokens->expressiveDefaultSpatial() * m_scale) : 0; ++} ++ ++int AnimDurations::expressiveSlowSpatial() const { ++ return m_tokens ? static_cast(m_tokens->expressiveSlowSpatial() * m_scale) : 0; ++} ++ ++int AnimDurations::expressiveFastEffects() const { ++ return m_tokens ? static_cast(m_tokens->expressiveFastEffects() * m_scale) : 0; ++} ++ ++int AnimDurations::expressiveDefaultEffects() const { ++ return m_tokens ? static_cast(m_tokens->expressiveDefaultEffects() * m_scale) : 0; ++} ++ ++int AnimDurations::expressiveSlowEffects() const { ++ return m_tokens ? static_cast(m_tokens->expressiveSlowEffects() * m_scale) : 0; ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp +new file mode 100644 +index 00000000..3446ea72 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp +@@ -0,0 +1,259 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++// Forward declare token types from advancedconfig.hpp ++class RoundingTokens; ++class SpacingTokens; ++class PaddingTokens; ++class FontSizeTokens; ++class AnimDurationTokens; ++ ++class AppearanceRounding : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, scale, 1) ++ ++ Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) ++ Q_PROPERTY(int small READ small NOTIFY valuesChanged) ++ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) ++ Q_PROPERTY(int large READ large NOTIFY valuesChanged) ++ Q_PROPERTY(int full READ full NOTIFY valuesChanged) ++ ++public: ++ explicit AppearanceRounding(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++ ++ void bindTokens(RoundingTokens* tokens); ++ ++ [[nodiscard]] int extraSmall() const; ++ [[nodiscard]] int small() const; ++ [[nodiscard]] int normal() const; ++ [[nodiscard]] int large() const; ++ [[nodiscard]] int full() const; ++ ++signals: ++ void valuesChanged(); ++ ++private: ++ RoundingTokens* m_tokens = nullptr; ++}; ++ ++class AppearanceSpacing : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, scale, 1) ++ ++ Q_PROPERTY(int small READ small NOTIFY valuesChanged) ++ Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) ++ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) ++ Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) ++ Q_PROPERTY(int large READ large NOTIFY valuesChanged) ++ ++public: ++ explicit AppearanceSpacing(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++ ++ void bindTokens(SpacingTokens* tokens); ++ ++ [[nodiscard]] int small() const; ++ [[nodiscard]] int smaller() const; ++ [[nodiscard]] int normal() const; ++ [[nodiscard]] int larger() const; ++ [[nodiscard]] int large() const; ++ ++signals: ++ void valuesChanged(); ++ ++private: ++ SpacingTokens* m_tokens = nullptr; ++}; ++ ++class AppearancePadding : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, scale, 1) ++ ++ Q_PROPERTY(int small READ small NOTIFY valuesChanged) ++ Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) ++ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) ++ Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) ++ Q_PROPERTY(int large READ large NOTIFY valuesChanged) ++ ++public: ++ explicit AppearancePadding(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++ ++ void bindTokens(PaddingTokens* tokens); ++ ++ [[nodiscard]] int small() const; ++ [[nodiscard]] int smaller() const; ++ [[nodiscard]] int normal() const; ++ [[nodiscard]] int larger() const; ++ [[nodiscard]] int large() const; ++ ++signals: ++ void valuesChanged(); ++ ++private: ++ PaddingTokens* m_tokens = nullptr; ++}; ++ ++class FontFamily : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(QString, sans, QStringLiteral("Rubik")) ++ CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF")) ++ CONFIG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) ++ CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) ++ ++public: ++ explicit FontFamily(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class FontSize : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, scale, 1) ++ ++ Q_PROPERTY(int small READ small NOTIFY valuesChanged) ++ Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) ++ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) ++ Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) ++ Q_PROPERTY(int large READ large NOTIFY valuesChanged) ++ Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) ++ ++public: ++ explicit FontSize(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++ ++ void bindTokens(FontSizeTokens* tokens); ++ ++ [[nodiscard]] int small() const; ++ [[nodiscard]] int smaller() const; ++ [[nodiscard]] int normal() const; ++ [[nodiscard]] int larger() const; ++ [[nodiscard]] int large() const; ++ [[nodiscard]] int extraLarge() const; ++ ++signals: ++ void valuesChanged(); ++ ++private: ++ FontSizeTokens* m_tokens = nullptr; ++}; ++ ++class AppearanceFont : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_SUBOBJECT(FontFamily, family) ++ CONFIG_SUBOBJECT(FontSize, size) ++ ++public: ++ explicit AppearanceFont(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_family(new FontFamily(this)) ++ , m_size(new FontSize(this)) {} ++}; ++ ++class AnimDurations : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(qreal, scale, 1) ++ ++ Q_PROPERTY(int small READ small NOTIFY valuesChanged) ++ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) ++ Q_PROPERTY(int large READ large NOTIFY valuesChanged) ++ Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) ++ Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY valuesChanged) ++ Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY valuesChanged) ++ Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY valuesChanged) ++ Q_PROPERTY(int expressiveFastEffects READ expressiveFastEffects NOTIFY valuesChanged) ++ Q_PROPERTY(int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY valuesChanged) ++ Q_PROPERTY(int expressiveSlowEffects READ expressiveSlowEffects NOTIFY valuesChanged) ++ ++public: ++ explicit AnimDurations(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++ ++ void bindTokens(AnimDurationTokens* tokens); ++ ++ [[nodiscard]] int small() const; ++ [[nodiscard]] int normal() const; ++ [[nodiscard]] int large() const; ++ [[nodiscard]] int extraLarge() const; ++ [[nodiscard]] int expressiveFastSpatial() const; ++ [[nodiscard]] int expressiveDefaultSpatial() const; ++ [[nodiscard]] int expressiveSlowSpatial() const; ++ [[nodiscard]] int expressiveFastEffects() const; ++ [[nodiscard]] int expressiveDefaultEffects() const; ++ [[nodiscard]] int expressiveSlowEffects() const; ++ ++signals: ++ void valuesChanged(); ++ ++private: ++ AnimDurationTokens* m_tokens = nullptr; ++}; ++ ++class AppearanceAnim : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_SUBOBJECT(AnimDurations, durations) ++ ++public: ++ explicit AppearanceAnim(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_durations(new AnimDurations(this)) {} ++}; ++ ++class AppearanceTransparency : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(bool, enabled, false) ++ CONFIG_GLOBAL_PROPERTY(qreal, base, 0.85) ++ CONFIG_GLOBAL_PROPERTY(qreal, layers, 0.4) ++ ++public: ++ explicit AppearanceTransparency(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class AppearanceConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, deformScale, 1) ++ CONFIG_SUBOBJECT(AppearanceRounding, rounding) ++ CONFIG_SUBOBJECT(AppearanceSpacing, spacing) ++ CONFIG_SUBOBJECT(AppearancePadding, padding) ++ CONFIG_SUBOBJECT(AppearanceFont, font) ++ CONFIG_SUBOBJECT(AppearanceAnim, anim) ++ CONFIG_SUBOBJECT(AppearanceTransparency, transparency) ++ ++public: ++ explicit AppearanceConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_rounding(new AppearanceRounding(this)) ++ , m_spacing(new AppearanceSpacing(this)) ++ , m_padding(new AppearancePadding(this)) ++ , m_font(new AppearanceFont(this)) ++ , m_anim(new AppearanceAnim(this)) ++ , m_transparency(new AppearanceTransparency(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/backgroundconfig.hpp b/plugin/src/Caelestia/Config/backgroundconfig.hpp +new file mode 100644 +index 00000000..743cd787 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/backgroundconfig.hpp +@@ -0,0 +1,84 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++class DesktopClockBackground : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, false) ++ CONFIG_PROPERTY(qreal, opacity, 0.7) ++ CONFIG_PROPERTY(bool, blur, true) ++ ++public: ++ explicit DesktopClockBackground(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class DesktopClockShadow : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(qreal, opacity, 0.7) ++ CONFIG_PROPERTY(qreal, blur, 0.4) ++ ++public: ++ explicit DesktopClockShadow(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class DesktopClock : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, false) ++ CONFIG_PROPERTY(qreal, scale, 1.0) ++ CONFIG_PROPERTY(QString, position, QStringLiteral("bottom-right")) ++ CONFIG_PROPERTY(bool, invertColors, false) ++ CONFIG_SUBOBJECT(DesktopClockBackground, background) ++ CONFIG_SUBOBJECT(DesktopClockShadow, shadow) ++ ++public: ++ explicit DesktopClock(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_background(new DesktopClockBackground(this)) ++ , m_shadow(new DesktopClockShadow(this)) {} ++}; ++ ++class BackgroundVisualiser : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, false) ++ CONFIG_PROPERTY(bool, autoHide, true) ++ CONFIG_PROPERTY(bool, blur, false) ++ CONFIG_PROPERTY(qreal, rounding, 1) ++ CONFIG_PROPERTY(qreal, spacing, 1) ++ ++public: ++ explicit BackgroundVisualiser(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BackgroundConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(bool, wallpaperEnabled, true) ++ CONFIG_SUBOBJECT(DesktopClock, desktopClock) ++ CONFIG_SUBOBJECT(BackgroundVisualiser, visualiser) ++ ++public: ++ explicit BackgroundConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_desktopClock(new DesktopClock(this)) ++ , m_visualiser(new BackgroundVisualiser(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp +new file mode 100644 +index 00000000..43d43205 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/barconfig.hpp +@@ -0,0 +1,166 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++using Qt::StringLiterals::operator""_s; ++ ++class BarScrollActions : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, workspaces, true) ++ CONFIG_PROPERTY(bool, volume, true) ++ CONFIG_PROPERTY(bool, brightness, true) ++ ++public: ++ explicit BarScrollActions(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BarPopouts : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, activeWindow, true) ++ CONFIG_PROPERTY(bool, tray, true) ++ CONFIG_PROPERTY(bool, statusIcons, true) ++ ++public: ++ explicit BarPopouts(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BarWorkspaces : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, shown, 5) ++ CONFIG_PROPERTY(bool, activeIndicator, true) ++ CONFIG_PROPERTY(bool, occupiedBg, false) ++ CONFIG_PROPERTY(bool, showWindows, true) ++ CONFIG_PROPERTY(bool, showWindowsOnSpecialWorkspaces, true) ++ CONFIG_PROPERTY(int, maxWindowIcons, 5) ++ CONFIG_PROPERTY(bool, activeTrail, false) ++ CONFIG_GLOBAL_PROPERTY(bool, perMonitorWorkspaces, true) ++ CONFIG_PROPERTY(QString, label, u" "_s) ++ CONFIG_PROPERTY(QString, occupiedLabel, u"󰮯"_s) ++ CONFIG_PROPERTY(QString, activeLabel, u"󰮯"_s) ++ CONFIG_PROPERTY(QString, capitalisation, u"preserve"_s) ++ CONFIG_GLOBAL_PROPERTY(QVariantList, specialWorkspaceIcons) ++ CONFIG_GLOBAL_PROPERTY(QVariantList, windowIcons, ++ { vmap({ ++ { u"regex"_s, u"steam(_app_(default|[0-9]+))?"_s }, ++ { u"icon"_s, u"sports_esports"_s }, ++ }) }) ++ ++public: ++ explicit BarWorkspaces(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BarActiveWindow : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, compact, false) ++ CONFIG_PROPERTY(bool, inverted, false) ++ CONFIG_PROPERTY(bool, showOnHover, true) ++ ++public: ++ explicit BarActiveWindow(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BarTray : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, background, false) ++ CONFIG_PROPERTY(bool, recolour, false) ++ CONFIG_PROPERTY(bool, compact, false) ++ CONFIG_GLOBAL_PROPERTY(QVariantList, iconSubs) ++ CONFIG_GLOBAL_PROPERTY(QStringList, hiddenIcons) ++ ++public: ++ explicit BarTray(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BarStatus : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, showAudio, false) ++ CONFIG_PROPERTY(bool, showMicrophone, false) ++ CONFIG_PROPERTY(bool, showKbLayout, false) ++ CONFIG_PROPERTY(bool, showNetwork, true) ++ CONFIG_PROPERTY(bool, showWifi, true) ++ CONFIG_PROPERTY(bool, showBluetooth, true) ++ CONFIG_PROPERTY(bool, showBattery, true) ++ CONFIG_PROPERTY(bool, showLockStatus, true) ++ ++public: ++ explicit BarStatus(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BarClock : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, background, false) ++ CONFIG_PROPERTY(bool, showDate, false) ++ CONFIG_PROPERTY(bool, showIcon, true) ++ ++public: ++ explicit BarClock(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class BarConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, persistent, true) ++ CONFIG_PROPERTY(bool, showOnHover, true) ++ CONFIG_PROPERTY(int, dragThreshold, 20) ++ CONFIG_SUBOBJECT(BarScrollActions, scrollActions) ++ CONFIG_SUBOBJECT(BarPopouts, popouts) ++ CONFIG_SUBOBJECT(BarWorkspaces, workspaces) ++ CONFIG_SUBOBJECT(BarActiveWindow, activeWindow) ++ CONFIG_SUBOBJECT(BarTray, tray) ++ CONFIG_SUBOBJECT(BarStatus, status) ++ CONFIG_SUBOBJECT(BarClock, clock) ++ CONFIG_PROPERTY(QVariantList, entries, ++ { ++ vmap({ { u"id"_s, u"logo"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"workspaces"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"activeWindow"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"tray"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"clock"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"statusIcons"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"power"_s }, { u"enabled"_s, true } }), ++ }) ++ CONFIG_PROPERTY(QStringList, excludedScreens) ++ ++public: ++ explicit BarConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_scrollActions(new BarScrollActions(this)) ++ , m_popouts(new BarPopouts(this)) ++ , m_workspaces(new BarWorkspaces(this)) ++ , m_activeWindow(new BarActiveWindow(this)) ++ , m_tray(new BarTray(this)) ++ , m_status(new BarStatus(this)) ++ , m_clock(new BarClock(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/borderconfig.hpp b/plugin/src/Caelestia/Config/borderconfig.hpp +new file mode 100644 +index 00000000..9de604e8 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/borderconfig.hpp +@@ -0,0 +1,29 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++class BorderConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, thickness, 10) ++ CONFIG_PROPERTY(int, rounding, 25) ++ CONFIG_PROPERTY(int, smoothing, 32) ++ ++ Q_PROPERTY(int minThickness READ minThickness CONSTANT) ++ Q_PROPERTY(int clampedThickness READ clampedThickness NOTIFY thicknessChanged) ++ ++public: ++ explicit BorderConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++ ++ [[nodiscard]] static int minThickness() { return 2; } ++ ++ [[nodiscard]] int clampedThickness() const { return std::max(minThickness(), m_thickness); } ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp +new file mode 100644 +index 00000000..ff939f99 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/config.cpp +@@ -0,0 +1,120 @@ ++#include "config.hpp" ++#include "appearanceconfig.hpp" ++#include "backgroundconfig.hpp" ++#include "barconfig.hpp" ++#include "borderconfig.hpp" ++#include "controlcenterconfig.hpp" ++#include "dashboardconfig.hpp" ++#include "generalconfig.hpp" ++#include "launcherconfig.hpp" ++#include "lockconfig.hpp" ++#include "monitorconfigmanager.hpp" ++#include "notifsconfig.hpp" ++#include "osdconfig.hpp" ++#include "serviceconfig.hpp" ++#include "sessionconfig.hpp" ++#include "sidebarconfig.hpp" ++#include "tokens.hpp" ++#include "userpaths.hpp" ++#include "utilitiesconfig.hpp" ++#include "winfoconfig.hpp" ++ ++#include ++#include ++ ++namespace caelestia::config { ++ ++namespace { ++ ++QString configDir() { ++ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); ++} ++ ++} // namespace ++ ++GlobalConfig::GlobalConfig(QObject* parent) ++ : RootConfig(parent) ++ , m_appearance(new AppearanceConfig(this)) ++ , m_general(new GeneralConfig(this)) ++ , m_background(new BackgroundConfig(this)) ++ , m_bar(new BarConfig(this)) ++ , m_border(new BorderConfig(this)) ++ , m_dashboard(new DashboardConfig(this)) ++ , m_controlCenter(new ControlCenterConfig(this)) ++ , m_launcher(new LauncherConfig(this)) ++ , m_notifs(new NotifsConfig(this)) ++ , m_osd(new OsdConfig(this)) ++ , m_session(new SessionConfig(this)) ++ , m_winfo(new WInfoConfig(this)) ++ , m_lock(new LockConfig(this)) ++ , m_utilities(new UtilitiesConfig(this)) ++ , m_sidebar(new SidebarConfig(this)) ++ , m_services(new ServiceConfig(this)) ++ , m_paths(new UserPaths(this)) { ++ setupFileBackend(configDir() + QStringLiteral("shell.json")); ++} ++ ++GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) ++ : RootConfig(parent) ++ , m_appearance(new AppearanceConfig(this)) ++ , m_general(new GeneralConfig(this)) ++ , m_background(new BackgroundConfig(this)) ++ , m_bar(new BarConfig(this)) ++ , m_border(new BorderConfig(this)) ++ , m_dashboard(new DashboardConfig(this)) ++ , m_controlCenter(new ControlCenterConfig(this)) ++ , m_launcher(new LauncherConfig(this)) ++ , m_notifs(new NotifsConfig(this)) ++ , m_osd(new OsdConfig(this)) ++ , m_session(new SessionConfig(this)) ++ , m_winfo(new WInfoConfig(this)) ++ , m_lock(new LockConfig(this)) ++ , m_utilities(new UtilitiesConfig(this)) ++ , m_sidebar(new SidebarConfig(this)) ++ , m_services(new ServiceConfig(this)) ++ , m_paths(new UserPaths(this)) { ++ if (!filePath.isEmpty()) ++ setupFileBackend(filePath, screen); ++ if (fallback) ++ syncFromGlobal(fallback); ++ ++ // Bind appearance computed properties to token base values ++ bindAppearanceTokens(); ++} ++ ++GlobalConfig* GlobalConfig::instance() { ++ static GlobalConfig instance; ++ instance.bindAppearanceTokens(); ++ return &instance; ++} ++ ++GlobalConfig* GlobalConfig::defaults() { ++ if (!m_defaults) ++ m_defaults = new GlobalConfig(nullptr, QString(), QString(), this); ++ return m_defaults; ++} ++ ++void GlobalConfig::bindAppearanceTokens() { ++ if (m_tokensBound) ++ return; ++ ++ qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: binding appearance to token values"; ++ auto* const tokenAppearance = TokenConfig::instance()->appearance(); ++ m_appearance->rounding()->bindTokens(tokenAppearance->rounding()); ++ m_appearance->spacing()->bindTokens(tokenAppearance->spacing()); ++ m_appearance->padding()->bindTokens(tokenAppearance->padding()); ++ m_appearance->font()->size()->bindTokens(tokenAppearance->fontSize()); ++ m_appearance->anim()->durations()->bindTokens(tokenAppearance->animDurations()); ++ m_tokensBound = true; ++} ++ ++GlobalConfig* GlobalConfig::forScreen(const QString& screen) { ++ return MonitorConfigManager::instance()->configForScreen(screen); ++} ++ ++GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { ++ QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); ++ return instance(); ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp +new file mode 100644 +index 00000000..248a3f22 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/config.hpp +@@ -0,0 +1,86 @@ ++#pragma once ++ ++#include "rootconfig.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++class AppearanceConfig; ++class BackgroundConfig; ++class BarConfig; ++class BorderConfig; ++class ControlCenterConfig; ++class DashboardConfig; ++class GeneralConfig; ++class LauncherConfig; ++class LockConfig; ++class NotifsConfig; ++class OsdConfig; ++class ServiceConfig; ++class SessionConfig; ++class SidebarConfig; ++class UserPaths; ++class UtilitiesConfig; ++class WInfoConfig; ++ ++class GlobalConfig : public RootConfig { ++ Q_OBJECT ++ QML_ELEMENT ++ QML_SINGLETON ++ Q_MOC_INCLUDE("appearanceconfig.hpp") ++ Q_MOC_INCLUDE("backgroundconfig.hpp") ++ Q_MOC_INCLUDE("barconfig.hpp") ++ Q_MOC_INCLUDE("borderconfig.hpp") ++ Q_MOC_INCLUDE("controlcenterconfig.hpp") ++ Q_MOC_INCLUDE("dashboardconfig.hpp") ++ Q_MOC_INCLUDE("generalconfig.hpp") ++ Q_MOC_INCLUDE("launcherconfig.hpp") ++ Q_MOC_INCLUDE("lockconfig.hpp") ++ Q_MOC_INCLUDE("notifsconfig.hpp") ++ Q_MOC_INCLUDE("osdconfig.hpp") ++ Q_MOC_INCLUDE("serviceconfig.hpp") ++ Q_MOC_INCLUDE("sessionconfig.hpp") ++ Q_MOC_INCLUDE("sidebarconfig.hpp") ++ Q_MOC_INCLUDE("userpaths.hpp") ++ Q_MOC_INCLUDE("utilitiesconfig.hpp") ++ Q_MOC_INCLUDE("winfoconfig.hpp") ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_SUBOBJECT(AppearanceConfig, appearance) ++ CONFIG_SUBOBJECT(GeneralConfig, general) ++ CONFIG_SUBOBJECT(BackgroundConfig, background) ++ CONFIG_SUBOBJECT(BarConfig, bar) ++ CONFIG_SUBOBJECT(BorderConfig, border) ++ CONFIG_SUBOBJECT(DashboardConfig, dashboard) ++ CONFIG_SUBOBJECT(ControlCenterConfig, controlCenter) ++ CONFIG_SUBOBJECT(LauncherConfig, launcher) ++ CONFIG_SUBOBJECT(NotifsConfig, notifs) ++ CONFIG_SUBOBJECT(OsdConfig, osd) ++ CONFIG_SUBOBJECT(SessionConfig, session) ++ CONFIG_SUBOBJECT(WInfoConfig, winfo) ++ CONFIG_SUBOBJECT(LockConfig, lock) ++ CONFIG_SUBOBJECT(UtilitiesConfig, utilities) ++ CONFIG_SUBOBJECT(SidebarConfig, sidebar) ++ CONFIG_SUBOBJECT(ServiceConfig, services) ++ CONFIG_SUBOBJECT(UserPaths, paths) ++ ++public: ++ static GlobalConfig* instance(); ++ [[nodiscard]] Q_INVOKABLE GlobalConfig* defaults(); ++ [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); ++ static GlobalConfig* create(QQmlEngine*, QJSEngine*); ++ ++ void bindAppearanceTokens(); ++ ++private: ++ friend class MonitorConfigManager; ++ explicit GlobalConfig(QObject* parent = nullptr); ++ explicit GlobalConfig( ++ GlobalConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); ++ ++ GlobalConfig* m_defaults = nullptr; ++ bool m_tokensBound = false; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp +new file mode 100644 +index 00000000..8d229049 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/configattached.cpp +@@ -0,0 +1,96 @@ ++#include "configattached.hpp" ++#include "config.hpp" ++#include "monitorconfigmanager.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++Config::Config(QObject* parent) ++ : QQuickAttachedPropertyPropagator(parent) { ++ initialize(); ++} ++ ++void Config::classBegin() {} ++ ++void Config::componentComplete() { ++ m_complete = true; ++} ++ ++QString Config::screen() const { ++ return m_screen; ++} ++ ++void Config::inheritScreen(const QString& screen) { ++ if (screen == m_screen) ++ return; ++ ++ m_screen = screen; ++ ++ if (m_screen.isEmpty()) ++ m_config = nullptr; ++ else ++ m_config = MonitorConfigManager::instance()->configForScreen(m_screen); ++ ++ propagateScreen(); ++ emit sourceChanged(); ++} ++ ++void Config::propagateScreen() { ++ const auto children = attachedChildren(); ++ for (auto* const child : children) { ++ auto* const config = qobject_cast(child); ++ if (config) ++ config->inheritScreen(m_screen); ++ } ++} ++ ++void Config::attachedParentChange( ++ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { ++ Q_UNUSED(oldParent); ++ auto* const config = qobject_cast(newParent); ++ if (config) ++ inheritScreen(config->screen()); ++} ++ ++#define CONFIG_ATTACHED_GETTER(Type, name) \ ++ const Type* Config::name() const { \ ++ if (m_config) \ ++ return m_config->name(); \ ++ /* Suppress warnings before component is complete if attached to a QQuickItem. */ \ ++ /* Raw QObjects are unable to inherit the screen (only QQuickItems can). */ \ ++ if ((m_complete || !qobject_cast(parent())) && parent()) \ ++ qCWarning(lcConfig, "Config.%s accessed without a screen set on %s", #name, \ ++ parent()->metaObject()->className()); \ ++ return GlobalConfig::instance()->name(); \ ++ } ++ ++CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) ++CONFIG_ATTACHED_GETTER(GeneralConfig, general) ++CONFIG_ATTACHED_GETTER(BackgroundConfig, background) ++CONFIG_ATTACHED_GETTER(BarConfig, bar) ++CONFIG_ATTACHED_GETTER(BorderConfig, border) ++CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard) ++CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter) ++CONFIG_ATTACHED_GETTER(LauncherConfig, launcher) ++CONFIG_ATTACHED_GETTER(NotifsConfig, notifs) ++CONFIG_ATTACHED_GETTER(OsdConfig, osd) ++CONFIG_ATTACHED_GETTER(SessionConfig, session) ++CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) ++CONFIG_ATTACHED_GETTER(LockConfig, lock) ++CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) ++CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) ++CONFIG_ATTACHED_GETTER(ServiceConfig, services) ++CONFIG_ATTACHED_GETTER(UserPaths, paths) ++ ++#undef CONFIG_ATTACHED_GETTER ++ ++GlobalConfig* Config::forScreen(const QString& screen) { ++ return GlobalConfig::forScreen(screen); ++} ++ ++Config* Config::qmlAttachedProperties(QObject* object) { ++ return new Config(object); ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp +new file mode 100644 +index 00000000..815b1080 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/configattached.hpp +@@ -0,0 +1,98 @@ ++#pragma once ++ ++#include "config.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { ++ Q_OBJECT ++ Q_INTERFACES(QQmlParserStatus) ++ QML_ELEMENT ++ QML_UNCREATABLE("") ++ QML_ATTACHED(Config) ++ Q_MOC_INCLUDE("appearanceconfig.hpp") ++ Q_MOC_INCLUDE("backgroundconfig.hpp") ++ Q_MOC_INCLUDE("barconfig.hpp") ++ Q_MOC_INCLUDE("borderconfig.hpp") ++ Q_MOC_INCLUDE("controlcenterconfig.hpp") ++ Q_MOC_INCLUDE("dashboardconfig.hpp") ++ Q_MOC_INCLUDE("generalconfig.hpp") ++ Q_MOC_INCLUDE("launcherconfig.hpp") ++ Q_MOC_INCLUDE("lockconfig.hpp") ++ Q_MOC_INCLUDE("notifsconfig.hpp") ++ Q_MOC_INCLUDE("osdconfig.hpp") ++ Q_MOC_INCLUDE("serviceconfig.hpp") ++ Q_MOC_INCLUDE("sessionconfig.hpp") ++ Q_MOC_INCLUDE("sidebarconfig.hpp") ++ Q_MOC_INCLUDE("userpaths.hpp") ++ Q_MOC_INCLUDE("utilitiesconfig.hpp") ++ Q_MOC_INCLUDE("winfoconfig.hpp") ++ ++ Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::BarConfig* bar READ bar NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::BorderConfig* border READ border NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) ++ ++public: ++ explicit Config(QObject* parent = nullptr); ++ ++ [[nodiscard]] QString screen() const; ++ void inheritScreen(const QString& screen); ++ ++ [[nodiscard]] const AppearanceConfig* appearance() const; ++ [[nodiscard]] const GeneralConfig* general() const; ++ [[nodiscard]] const BackgroundConfig* background() const; ++ [[nodiscard]] const BarConfig* bar() const; ++ [[nodiscard]] const BorderConfig* border() const; ++ [[nodiscard]] const DashboardConfig* dashboard() const; ++ [[nodiscard]] const ControlCenterConfig* controlCenter() const; ++ [[nodiscard]] const LauncherConfig* launcher() const; ++ [[nodiscard]] const NotifsConfig* notifs() const; ++ [[nodiscard]] const OsdConfig* osd() const; ++ [[nodiscard]] const SessionConfig* session() const; ++ [[nodiscard]] const WInfoConfig* winfo() const; ++ [[nodiscard]] const LockConfig* lock() const; ++ [[nodiscard]] const UtilitiesConfig* utilities() const; ++ [[nodiscard]] const SidebarConfig* sidebar() const; ++ [[nodiscard]] const ServiceConfig* services() const; ++ [[nodiscard]] const UserPaths* paths() const; ++ ++ [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); ++ ++ static Config* qmlAttachedProperties(QObject* object); ++ ++signals: ++ void sourceChanged(); ++ ++protected: ++ void attachedParentChange( ++ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; ++ ++private: ++ void classBegin() override; ++ void componentComplete() override; ++ ++ void propagateScreen(); ++ ++ bool m_complete = false; ++ QString m_screen; ++ GlobalConfig* m_config = nullptr; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp +new file mode 100644 +index 00000000..030ecabe +--- /dev/null ++++ b/plugin/src/Caelestia/Config/configobject.cpp +@@ -0,0 +1,316 @@ ++#include "configobject.hpp" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg) ++ ++// ConfigObject ++ ++ConfigObject::ConfigObject(QObject* parent) ++ : QObject(parent) {} ++ ++void ConfigObject::loadFromJson(const QJsonObject& obj) { ++ const auto* meta = metaObject(); ++ ++ qCDebug(lcConfig) << "Loading JSON into" << meta->className() << "with" << obj.keys().size() ++ << "keys:" << obj.keys(); ++ ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ auto prop = meta->property(i); ++ const auto key = QString::fromUtf8(prop.name()); ++ ++ if (!obj.contains(key)) ++ continue; ++ ++ if (isGlobalOnly(key)) ++ qCWarning(lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", ++ qUtf8Printable(propertyPath(key))); ++ ++ const auto jsonVal = obj.value(key); ++ ++ // Recurse into sub-objects ++ auto current = prop.read(this); ++ auto* subObj = current.value(); ++ ++ if (subObj) { ++ qCDebug(lcConfig) << " Recursing into sub-object" << key; ++ subObj->loadFromJson(jsonVal.toObject()); ++ continue; ++ } ++ ++ // Skip read-only properties ++ if (!prop.isWritable()) ++ continue; ++ ++ // Handle QStringList explicitly (QJsonArray → QStringList needs manual conversion) ++ if (prop.metaType().id() == QMetaType::QStringList) { ++ QStringList list; ++ const auto jsonArr = jsonVal.toArray(); ++ for (const auto& v : jsonArr) ++ list.append(v.toString()); ++ prop.write(this, QVariant::fromValue(list)); ++ m_loadedKeys.insert(key); ++ qCDebug(lcConfig) << " Loaded" << key << "=" << list; ++ continue; ++ } ++ ++ // For all other types, let Qt's variant conversion handle it ++ prop.write(this, jsonVal.toVariant()); ++ m_loadedKeys.insert(key); ++ qCDebug(lcConfig) << " Loaded" << key << "=" << jsonVal.toVariant(); ++ } ++} ++ ++QJsonObject ConfigObject::toJsonObject() const { ++ QJsonObject obj; ++ const auto* meta = metaObject(); ++ ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ const auto prop = meta->property(i); ++ ++ if (!prop.isReadable()) ++ continue; ++ ++ const auto key = QString::fromUtf8(prop.name()); ++ ++ if (isGlobalOnly(key)) ++ continue; ++ ++ const auto value = prop.read(this); ++ ++ // Recurse into sub-objects — include only if they have loaded keys ++ if (value.canView()) { ++ auto* const subObj = value.value(); ++ if (subObj) { ++ auto subJson = subObj->toJsonObject(); ++ if (!subJson.isEmpty()) ++ obj.insert(key, subJson); ++ } ++ continue; ++ } ++ ++ // Only include properties that were explicitly loaded ++ if (!m_loadedKeys.contains(key)) ++ continue; ++ ++ if (!prop.isWritable()) ++ continue; ++ ++ if (prop.metaType().id() == QMetaType::QStringList) { ++ QJsonArray arr; ++ const auto strList = value.toStringList(); ++ for (const auto& s : strList) ++ arr.append(s); ++ obj.insert(key, arr); ++ continue; ++ } ++ ++ if (prop.metaType().id() == QMetaType::QVariantList) { ++ obj.insert(key, QJsonArray::fromVariantList(value.toList())); ++ continue; ++ } ++ ++ obj.insert(key, QJsonValue::fromVariant(value)); ++ } ++ ++ return obj; ++} ++ ++void ConfigObject::clearLoadedKeys() { ++ m_loadedKeys.clear(); ++ ++ const auto* meta = metaObject(); ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ auto prop = meta->property(i); ++ if (isGlobalOnly(QString::fromUtf8(prop.name()))) ++ continue; ++ auto value = prop.read(this); ++ auto* subObj = value.value(); ++ if (subObj) ++ subObj->clearLoadedKeys(); ++ } ++} ++ ++void ConfigObject::syncFromGlobal(ConfigObject* global) { ++ m_global = global; ++ ++ const auto* meta = metaObject(); ++ qCDebug(lcConfig) << "Syncing" << meta->className() << "from global, loaded keys:" << m_loadedKeys; ++ ++ // Connect batched change signal (single connection per ConfigObject pair) ++ connect(global, &ConfigObject::propertiesChanged, this, &ConfigObject::onGlobalPropertiesChanged); ++ ++ // Initial sync: copy all non-loaded property values from global ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ auto prop = meta->property(i); ++ const auto key = QString::fromUtf8(prop.name()); ++ ++ if (isGlobalOnly(key)) ++ continue; ++ ++ auto current = prop.read(this); ++ auto* subObj = current.value(); ++ ++ if (subObj) { ++ auto globalVal = prop.read(global); ++ auto* globalSub = globalVal.value(); ++ if (globalSub) ++ subObj->syncFromGlobal(globalSub); ++ continue; ++ } ++ ++ if (!prop.isWritable()) ++ continue; ++ ++ if (!m_loadedKeys.contains(key)) { ++ auto val = prop.read(global); ++ prop.write(this, val); ++ m_loadedKeys.remove(key); // setter added it — remove since this is a synced value ++ qCDebug(lcConfig) << " Synced" << key << "=" << val << "from global"; ++ } else { ++ qCDebug(lcConfig) << " Keeping loaded" << key << "=" << prop.read(this); ++ } ++ } ++} ++ ++void ConfigObject::resyncFromGlobal() { ++ if (!m_global) ++ return; ++ ++ const auto* meta = metaObject(); ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ auto prop = meta->property(i); ++ const auto key = QString::fromUtf8(prop.name()); ++ ++ if (isGlobalOnly(key)) ++ continue; ++ ++ auto current = prop.read(this); ++ auto* subObj = current.value(); ++ ++ if (subObj) { ++ subObj->resyncFromGlobal(); ++ continue; ++ } ++ ++ if (!prop.isWritable()) ++ continue; ++ ++ if (!m_loadedKeys.contains(key)) { ++ prop.write(this, prop.read(m_global)); ++ m_loadedKeys.remove(key); // setter added it — remove since this is a synced value ++ } ++ } ++} ++ ++QString ConfigObject::propertyPath(const QString& name) const { ++ QStringList parts; ++ parts.append(name); ++ ++ const QObject* obj = this; ++ while (auto* parentObj = obj->parent()) { ++ auto* parentConfig = qobject_cast(parentObj); ++ if (!parentConfig) ++ break; ++ ++ // Find which property name this child is on the parent ++ const auto* meta = parentConfig->metaObject(); ++ bool found = false; ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ auto prop = meta->property(i); ++ auto val = prop.read(parentObj); ++ if (val.value() == obj) { ++ parts.prepend(QString::fromUtf8(prop.name())); ++ found = true; ++ break; ++ } ++ } ++ ++ if (!found) ++ break; ++ ++ obj = parentObj; ++ } ++ ++ return parts.join(QLatin1Char('.')); ++} ++ ++bool ConfigObject::isPropertyLoaded(const QString& name) const { ++ return m_loadedKeys.contains(name); ++} ++ ++bool ConfigObject::isOverlay() const { ++ return m_global != nullptr; ++} ++ ++bool ConfigObject::isGlobalOnly(const QString& name) const { ++ return isOverlay() && m_globalOnlyKeys.contains(name); ++} ++ ++void ConfigObject::markPropertyLoaded(const QString& name) { ++ m_loadedKeys.insert(name); ++} ++ ++void ConfigObject::resetOption(const QString& name) { ++ m_loadedKeys.remove(name); ++ ++ // If synced from global, re-copy the global value ++ if (m_global) { ++ int idx = metaObject()->indexOfProperty(name.toUtf8().constData()); ++ if (idx >= 0) { ++ auto prop = metaObject()->property(idx); ++ if (prop.isWritable()) ++ prop.write(this, prop.read(m_global)); ++ } ++ } ++} ++ ++void ConfigObject::onGlobalPropertiesChanged(const QMap& changed) { ++ for (auto it = changed.begin(); it != changed.end(); ++it) { ++ if (m_loadedKeys.contains(it.key()) || isGlobalOnly(it.key())) ++ continue; ++ ++ int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); ++ if (idx >= 0) { ++ metaObject()->property(idx).write(this, it.value()); ++ m_loadedKeys.remove(it.key()); // setter added it — remove since this is a synced value ++ qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value() ++ << "from global change"; ++ } ++ } ++} ++ ++void ConfigObject::markGlobalOnly(const QString& name) { ++ m_globalOnlyKeys.insert(name); ++} ++ ++void ConfigObject::notifyPropertyChanged(const QString& name, const QVariant& value) { ++ m_pendingChanges.insert(name, value); ++ ++ if (!m_batchTimer) { ++ m_batchTimer = new QTimer(this); ++ m_batchTimer->setSingleShot(true); ++ m_batchTimer->setInterval(0); ++ connect(m_batchTimer, &QTimer::timeout, this, &ConfigObject::emitBatchedChanges); ++ } ++ ++ m_batchTimer->start(); ++} ++ ++void ConfigObject::emitBatchedChanges() { ++ if (m_pendingChanges.isEmpty()) ++ return; ++ ++ auto changes = std::move(m_pendingChanges); ++ m_pendingChanges.clear(); ++ emit propertiesChanged(changes); ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp +new file mode 100644 +index 00000000..c4f4428b +--- /dev/null ++++ b/plugin/src/Caelestia/Config/configobject.hpp +@@ -0,0 +1,143 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++inline QVariantMap vmap(std::initializer_list> entries) { ++ QVariantMap map; ++ for (const auto& [key, value] : entries) ++ map.insert(std::move(key), std::move(value)); ++ return map; ++} ++ ++} // namespace caelestia::config ++ ++// Declares a serialized config property with getter, setter (change-detected), signal, and member. ++#define CONFIG_PROPERTY(Type, name, ...) \ ++ Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ ++ \ ++public: \ ++ [[nodiscard]] Type name() const { \ ++ return m_##name; \ ++ } \ ++ void set_##name(const Type& val) { \ ++ if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ ++ markPropertyLoaded(QStringLiteral(#name)); \ ++ Q_EMIT name##Changed(); \ ++ notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ ++ } \ ++ } \ ++ Q_SIGNAL void name##Changed(); \ ++ \ ++private: \ ++ Type m_##name __VA_OPT__(= __VA_ARGS__); ++ ++// Declares a CONSTANT sub-object property. Initialize the member in the constructor. ++#define CONFIG_SUBOBJECT(Type, name) \ ++ Q_PROPERTY(caelestia::config::Type* name READ name CONSTANT) \ ++ \ ++public: \ ++ [[nodiscard]] Type* name() const { \ ++ return m_##name; \ ++ } \ ++ \ ++private: \ ++ Type* m_##name = nullptr; ++ ++// Like CONFIG_PROPERTY but warns on read/write when accessed on a per-monitor overlay. ++#define CONFIG_GLOBAL_PROPERTY(Type, name, ...) \ ++ Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ ++ \ ++public: \ ++ [[nodiscard]] Type name() const { \ ++ if (isOverlay()) \ ++ qCWarning(caelestia::config::lcConfig, "Reading global-only option '%s' on per-monitor overlay", \ ++ qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ ++ return m_##name; \ ++ } \ ++ void set_##name(const Type& val) { \ ++ if (isOverlay()) \ ++ qCWarning(caelestia::config::lcConfig, "Writing global-only option '%s' on per-monitor overlay", \ ++ qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ ++ if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ ++ markPropertyLoaded(QStringLiteral(#name)); \ ++ Q_EMIT name##Changed(); \ ++ notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ ++ } \ ++ } \ ++ Q_SIGNAL void name##Changed(); \ ++ \ ++private: \ ++ Type m_##name __VA_OPT__(= __VA_ARGS__); \ ++ const bool m_##name##_go = [this] { \ ++ markGlobalOnly(QStringLiteral(#name)); \ ++ return true; \ ++ }(); ++ ++namespace caelestia::config { ++ ++Q_DECLARE_LOGGING_CATEGORY(lcConfig) ++ ++class ConfigObject : public QObject { ++ Q_OBJECT ++ ++public: ++ explicit ConfigObject(QObject* parent = nullptr); ++ ++ void loadFromJson(const QJsonObject& obj); ++ [[nodiscard]] QJsonObject toJsonObject() const; ++ ++ // Per-monitor overlay support (Qt Resolve Mask pattern). ++ void syncFromGlobal(ConfigObject* global); ++ void resyncFromGlobal(); ++ void clearLoadedKeys(); ++ ++ [[nodiscard]] bool isPropertyLoaded(const QString& name) const; ++ [[nodiscard]] QString propertyPath(const QString& name) const; ++ [[nodiscard]] bool isOverlay() const; ++ // Returns true only on overlays — global singleton always returns false. ++ [[nodiscard]] bool isGlobalOnly(const QString& name) const; ++ ++ Q_INVOKABLE void resetOption(const QString& name); ++ ++ template static bool updateMember(T& member, const T& value) { ++ if constexpr (std::is_floating_point_v) { ++ if (qFuzzyCompare(member + 1.0, value + 1.0)) ++ return false; ++ } else { ++ if (member == value) ++ return false; ++ } ++ member = value; ++ return true; ++ } ++ ++signals: ++ void propertiesChanged(const QMap& changed); ++ ++protected: ++ void markPropertyLoaded(const QString& name); ++ void markGlobalOnly(const QString& name); ++ void notifyPropertyChanged(const QString& name, const QVariant& value); ++ ++private: ++ void onGlobalPropertiesChanged(const QMap& changed); ++ void emitBatchedChanges(); ++ ++ // Per-monitor overlay state ++ ConfigObject* m_global = nullptr; ++ QSet m_loadedKeys; ++ QSet m_globalOnlyKeys; ++ QMap m_pendingChanges; ++ QTimer* m_batchTimer = nullptr; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/controlcenterconfig.hpp b/plugin/src/Caelestia/Config/controlcenterconfig.hpp +new file mode 100644 +index 00000000..80b40b87 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/controlcenterconfig.hpp +@@ -0,0 +1,18 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++namespace caelestia::config { ++ ++// ControlCenterConfig has no serialized properties (serializer returns {}) ++// All properties are in AdvancedConfig.controlCenter ++class ControlCenterConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++public: ++ explicit ControlCenterConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/dashboardconfig.hpp b/plugin/src/Caelestia/Config/dashboardconfig.hpp +new file mode 100644 +index 00000000..cb872d1d +--- /dev/null ++++ b/plugin/src/Caelestia/Config/dashboardconfig.hpp +@@ -0,0 +1,44 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++namespace caelestia::config { ++ ++class DashboardPerformance : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, showBattery, true) ++ CONFIG_PROPERTY(bool, showGpu, true) ++ CONFIG_PROPERTY(bool, showCpu, true) ++ CONFIG_PROPERTY(bool, showMemory, true) ++ CONFIG_PROPERTY(bool, showStorage, true) ++ CONFIG_PROPERTY(bool, showNetwork, true) ++ ++public: ++ explicit DashboardPerformance(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class DashboardConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(bool, showOnHover, true) ++ CONFIG_PROPERTY(bool, showDashboard, true) ++ CONFIG_PROPERTY(bool, showMedia, true) ++ CONFIG_PROPERTY(bool, showPerformance, true) ++ CONFIG_PROPERTY(bool, showWeather, true) ++ CONFIG_GLOBAL_PROPERTY(int, mediaUpdateInterval, 500) ++ CONFIG_GLOBAL_PROPERTY(int, resourceUpdateInterval, 1000) ++ CONFIG_PROPERTY(int, dragThreshold, 50) ++ CONFIG_SUBOBJECT(DashboardPerformance, performance) ++ ++public: ++ explicit DashboardConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_performance(new DashboardPerformance(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp +new file mode 100644 +index 00000000..859302fe +--- /dev/null ++++ b/plugin/src/Caelestia/Config/generalconfig.hpp +@@ -0,0 +1,108 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++using Qt::StringLiterals::operator""_s; ++ ++class GeneralApps : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(QStringList, terminal, { u"foot"_s }) ++ CONFIG_GLOBAL_PROPERTY(QStringList, audio, { u"pavucontrol"_s }) ++ CONFIG_GLOBAL_PROPERTY(QStringList, playback, { u"mpv"_s }) ++ CONFIG_GLOBAL_PROPERTY(QStringList, explorer, { u"thunar"_s }) ++ ++public: ++ explicit GeneralApps(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class GeneralIdle : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(bool, lockBeforeSleep, true) ++ CONFIG_GLOBAL_PROPERTY(bool, inhibitWhenAudio, true) ++ CONFIG_GLOBAL_PROPERTY(QVariantList, timeouts, ++ { ++ vmap({ ++ { u"timeout"_s, 180 }, ++ { u"idleAction"_s, u"lock"_s }, ++ }), ++ vmap({ ++ { u"timeout"_s, 300 }, ++ { u"idleAction"_s, u"dpms off"_s }, ++ { u"returnAction"_s, u"dpms on"_s }, ++ }), ++ vmap({ ++ { u"timeout"_s, 600 }, ++ { u"idleAction"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, ++ }), ++ }) ++ ++public: ++ explicit GeneralIdle(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class GeneralBattery : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(QVariantList, warnLevels, ++ { ++ vmap({ ++ { u"level"_s, 20 }, ++ { u"title"_s, u"Low battery"_s }, ++ { u"message"_s, u"You might want to plug in a charger"_s }, ++ { u"icon"_s, u"battery_android_frame_2"_s }, ++ }), ++ vmap({ ++ { u"level"_s, 10 }, ++ { u"title"_s, u"Did you see the previous message?"_s }, ++ { u"message"_s, u"You should probably plug in a charger now"_s }, ++ { u"icon"_s, u"battery_android_frame_1"_s }, ++ }), ++ vmap({ ++ { u"level"_s, 5 }, ++ { u"title"_s, u"Critical battery level"_s }, ++ { u"message"_s, u"PLUG THE CHARGER RIGHT NOW!!"_s }, ++ { u"icon"_s, u"battery_android_alert"_s }, ++ { u"critical"_s, true }, ++ }), ++ }) ++ CONFIG_GLOBAL_PROPERTY(int, criticalLevel, 3) ++ ++public: ++ explicit GeneralBattery(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class GeneralConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(QString, logo) ++ CONFIG_PROPERTY(bool, showOverFullscreen, false) ++ CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) ++ CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) ++ CONFIG_SUBOBJECT(GeneralApps, apps) ++ CONFIG_SUBOBJECT(GeneralIdle, idle) ++ CONFIG_SUBOBJECT(GeneralBattery, battery) ++ ++public: ++ explicit GeneralConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_apps(new GeneralApps(this)) ++ , m_idle(new GeneralIdle(this)) ++ , m_battery(new GeneralBattery(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp +new file mode 100644 +index 00000000..049e213a +--- /dev/null ++++ b/plugin/src/Caelestia/Config/launcherconfig.hpp +@@ -0,0 +1,135 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++using Qt::StringLiterals::operator""_s; ++ ++class LauncherUseFuzzy : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(bool, apps, false) ++ CONFIG_GLOBAL_PROPERTY(bool, actions, false) ++ CONFIG_GLOBAL_PROPERTY(bool, schemes, false) ++ CONFIG_GLOBAL_PROPERTY(bool, variants, false) ++ CONFIG_GLOBAL_PROPERTY(bool, wallpapers, false) ++ ++public: ++ explicit LauncherUseFuzzy(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class LauncherConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(bool, showOnHover, false) ++ CONFIG_PROPERTY(int, maxShown, 7) ++ CONFIG_PROPERTY(int, maxWallpapers, 9) ++ CONFIG_GLOBAL_PROPERTY(QString, specialPrefix, u"@"_s) ++ CONFIG_GLOBAL_PROPERTY(QString, actionPrefix, u">"_s) ++ CONFIG_GLOBAL_PROPERTY(bool, enableDangerousActions, false) ++ CONFIG_PROPERTY(int, dragThreshold, 50) ++ CONFIG_GLOBAL_PROPERTY(bool, vimKeybinds, false) ++ CONFIG_GLOBAL_PROPERTY(QStringList, favouriteApps) ++ CONFIG_GLOBAL_PROPERTY(QStringList, hiddenApps) ++ CONFIG_SUBOBJECT(LauncherUseFuzzy, useFuzzy) ++ CONFIG_GLOBAL_PROPERTY(QVariantList, actions, ++ { ++ vmap({ ++ { u"name"_s, u"Calculator"_s }, ++ { u"icon"_s, u"calculate"_s }, ++ { u"description"_s, u"Do simple math equations (powered by Qalc)"_s }, ++ { u"command"_s, QStringList{ u"autocomplete"_s, u"calc"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Scheme"_s }, ++ { u"icon"_s, u"palette"_s }, ++ { u"description"_s, u"Change the current colour scheme"_s }, ++ { u"command"_s, QStringList{ u"autocomplete"_s, u"scheme"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Wallpaper"_s }, ++ { u"icon"_s, u"image"_s }, ++ { u"description"_s, u"Change the current wallpaper"_s }, ++ { u"command"_s, QStringList{ u"autocomplete"_s, u"wallpaper"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Variant"_s }, ++ { u"icon"_s, u"colors"_s }, ++ { u"description"_s, u"Change the current scheme variant"_s }, ++ { u"command"_s, QStringList{ u"autocomplete"_s, u"variant"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Random"_s }, ++ { u"icon"_s, u"casino"_s }, ++ { u"description"_s, u"Switch to a random wallpaper"_s }, ++ { u"command"_s, QStringList{ u"caelestia"_s, u"wallpaper"_s, u"-r"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Light"_s }, ++ { u"icon"_s, u"light_mode"_s }, ++ { u"description"_s, u"Change the scheme to light mode"_s }, ++ { u"command"_s, QStringList{ u"setMode"_s, u"light"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Dark"_s }, ++ { u"icon"_s, u"dark_mode"_s }, ++ { u"description"_s, u"Change the scheme to dark mode"_s }, ++ { u"command"_s, QStringList{ u"setMode"_s, u"dark"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Shutdown"_s }, ++ { u"icon"_s, u"power_settings_new"_s }, ++ { u"description"_s, u"Shutdown the system"_s }, ++ { u"command"_s, QStringList{ u"systemctl"_s, u"poweroff"_s } }, ++ { u"dangerous"_s, true }, ++ }), ++ vmap({ ++ { u"name"_s, u"Reboot"_s }, ++ { u"icon"_s, u"cached"_s }, ++ { u"description"_s, u"Reboot the system"_s }, ++ { u"command"_s, QStringList{ u"systemctl"_s, u"reboot"_s } }, ++ { u"dangerous"_s, true }, ++ }), ++ vmap({ ++ { u"name"_s, u"Logout"_s }, ++ { u"icon"_s, u"exit_to_app"_s }, ++ { u"description"_s, u"Log out of the current session"_s }, ++ { u"command"_s, QStringList{ u"loginctl"_s, u"terminate-user"_s, u""_s } }, ++ { u"dangerous"_s, true }, ++ }), ++ vmap({ ++ { u"name"_s, u"Lock"_s }, ++ { u"icon"_s, u"lock"_s }, ++ { u"description"_s, u"Lock the current session"_s }, ++ { u"command"_s, QStringList{ u"loginctl"_s, u"lock-session"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Sleep"_s }, ++ { u"icon"_s, u"bedtime"_s }, ++ { u"description"_s, u"Suspend then hibernate"_s }, ++ { u"command"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, ++ }), ++ vmap({ ++ { u"name"_s, u"Settings"_s }, ++ { u"icon"_s, u"settings"_s }, ++ { u"description"_s, u"Configure the shell"_s }, ++ { u"command"_s, QStringList{ u"caelestia"_s, u"shell"_s, u"controlCenter"_s, u"open"_s } }, ++ }), ++ }) ++ ++public: ++ explicit LauncherConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_useFuzzy(new LauncherUseFuzzy(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp +new file mode 100644 +index 00000000..0d8aa6ba +--- /dev/null ++++ b/plugin/src/Caelestia/Config/lockconfig.hpp +@@ -0,0 +1,21 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++namespace caelestia::config { ++ ++class LockConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, recolourLogo, false) ++ CONFIG_GLOBAL_PROPERTY(bool, enableFprint, true) ++ CONFIG_GLOBAL_PROPERTY(int, maxFprintTries, 3) ++ CONFIG_PROPERTY(bool, hideNotifs, false) ++ ++public: ++ explicit LockConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp +new file mode 100644 +index 00000000..fb1745ec +--- /dev/null ++++ b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp +@@ -0,0 +1,64 @@ ++#include "monitorconfigmanager.hpp" ++#include "config.hpp" ++#include "tokens.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++namespace { ++ ++QString monitorConfigDir(const QString& screen) { ++ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + ++ QStringLiteral("/caelestia/monitors/") + screen + QStringLiteral("/"); ++} ++ ++} // namespace ++ ++MonitorConfigManager::MonitorConfigManager(QObject* parent) ++ : QObject(parent) {} ++ ++MonitorConfigManager* MonitorConfigManager::instance() { ++ static MonitorConfigManager instance; ++ return &instance; ++} ++ ++MonitorConfigManager* MonitorConfigManager::create(QQmlEngine*, QJSEngine*) { ++ QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); ++ return instance(); ++} ++ ++GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) { ++ auto& overlay = m_overlays[screen]; ++ if (!overlay.config) { ++ auto dir = monitorConfigDir(screen); ++ overlay.config = new GlobalConfig(GlobalConfig::instance(), dir + QStringLiteral("shell.json"), screen, this); ++ ++ auto* const global = GlobalConfig::instance(); ++ connect(overlay.config, &GlobalConfig::loaded, global, &GlobalConfig::loaded); ++ connect(overlay.config, &GlobalConfig::saved, global, &GlobalConfig::saved); ++ connect(overlay.config, &GlobalConfig::loadFailed, global, &GlobalConfig::loadFailed); ++ connect(overlay.config, &GlobalConfig::saveFailed, global, &GlobalConfig::saveFailed); ++ connect(overlay.config, &GlobalConfig::unknownOption, global, &GlobalConfig::unknownOption); ++ } ++ return overlay.config; ++} ++ ++TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) { ++ auto& overlay = m_overlays[screen]; ++ if (!overlay.tokens) { ++ auto dir = monitorConfigDir(screen); ++ overlay.tokens = ++ new TokenConfig(TokenConfig::instance(), dir + QStringLiteral("shell-tokens.json"), screen, this); ++ ++ auto* const global = TokenConfig::instance(); ++ connect(overlay.tokens, &TokenConfig::loaded, global, &TokenConfig::loaded); ++ connect(overlay.tokens, &TokenConfig::saved, global, &TokenConfig::saved); ++ connect(overlay.tokens, &TokenConfig::loadFailed, global, &TokenConfig::loadFailed); ++ connect(overlay.tokens, &TokenConfig::saveFailed, global, &TokenConfig::saveFailed); ++ connect(overlay.tokens, &TokenConfig::unknownOption, global, &TokenConfig::unknownOption); ++ } ++ return overlay.tokens; ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.hpp b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp +new file mode 100644 +index 00000000..70bbdc0d +--- /dev/null ++++ b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp +@@ -0,0 +1,35 @@ ++#pragma once ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++class GlobalConfig; ++class TokenConfig; ++ ++class MonitorConfigManager : public QObject { ++ Q_OBJECT ++ QML_ELEMENT ++ QML_SINGLETON ++ ++public: ++ static MonitorConfigManager* instance(); ++ static MonitorConfigManager* create(QQmlEngine*, QJSEngine*); ++ ++ [[nodiscard]] Q_INVOKABLE GlobalConfig* configForScreen(const QString& screen); ++ [[nodiscard]] Q_INVOKABLE TokenConfig* tokensForScreen(const QString& screen); ++ ++private: ++ explicit MonitorConfigManager(QObject* parent = nullptr); ++ ++ struct ScreenOverlay { ++ GlobalConfig* config = nullptr; ++ TokenConfig* tokens = nullptr; ++ }; ++ ++ QHash m_overlays; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/notifsconfig.hpp b/plugin/src/Caelestia/Config/notifsconfig.hpp +new file mode 100644 +index 00000000..cdac94bd +--- /dev/null ++++ b/plugin/src/Caelestia/Config/notifsconfig.hpp +@@ -0,0 +1,28 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++class NotifsConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(bool, expire, true) ++ CONFIG_GLOBAL_PROPERTY(QString, fullscreen, QStringLiteral("on")) ++ CONFIG_GLOBAL_PROPERTY(int, defaultExpireTimeout, 5000) ++ CONFIG_GLOBAL_PROPERTY(int, fullscreenExpireTimeout, 2000) ++ CONFIG_PROPERTY(qreal, clearThreshold, 0.3) ++ CONFIG_PROPERTY(int, expandThreshold, 20) ++ CONFIG_GLOBAL_PROPERTY(bool, actionOnClick, false) ++ CONFIG_PROPERTY(int, groupPreviewNum, 3) ++ CONFIG_PROPERTY(bool, openExpanded, false) ++ ++public: ++ explicit NotifsConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/osdconfig.hpp b/plugin/src/Caelestia/Config/osdconfig.hpp +new file mode 100644 +index 00000000..18770294 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/osdconfig.hpp +@@ -0,0 +1,21 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++namespace caelestia::config { ++ ++class OsdConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(int, hideDelay, 2000) ++ CONFIG_PROPERTY(bool, enableBrightness, true) ++ CONFIG_PROPERTY(bool, enableMicrophone, false) ++ ++public: ++ explicit OsdConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp +new file mode 100644 +index 00000000..9e976ab2 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/rootconfig.cpp +@@ -0,0 +1,258 @@ ++#include "rootconfig.hpp" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++namespace { ++ ++QString watchRoot() { ++ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); ++} ++ ++} // namespace ++ ++RootConfig::RootConfig(QObject* parent) ++ : ConfigObject(parent) {} ++ ++bool RootConfig::recentlySaved() const { ++ return m_recentlySaved; ++} ++ ++QStringList RootConfig::collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json) { ++ QStringList unknown; ++ const auto* meta = obj->metaObject(); ++ ++ QSet known; ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) ++ known.insert(QString::fromUtf8(meta->property(i).name())); ++ ++ for (auto it = json.begin(); it != json.end(); ++it) { ++ if (!known.contains(it.key())) { ++ unknown.append(it.key()); ++ } else if (it.value().isObject()) { ++ int idx = meta->indexOfProperty(it.key().toUtf8().constData()); ++ if (idx >= 0) { ++ auto prop = meta->property(idx); ++ auto value = prop.read(obj); ++ auto* subObj = value.value(); ++ if (subObj) { ++ const auto subUnknown = collectUnknownKeys(subObj, it.value().toObject()); ++ for (const auto& subKey : subUnknown) ++ unknown.append(it.key() + QStringLiteral(".") + subKey); ++ } ++ } ++ } ++ } ++ ++ return unknown; ++} ++ ++void RootConfig::setupFileBackend(const QString& path, const QString& screen) { ++ m_filePath = path; ++ m_screen = screen; ++ ++ m_watcher = new QFileSystemWatcher(this); ++ m_saveTimer = new QTimer(this); ++ m_cooldownTimer = new QTimer(this); ++ m_retryTimer = new QTimer(this); ++ ++ m_retryTimer->setSingleShot(true); ++ m_retryTimer->setInterval(50); ++ connect(m_retryTimer, &QTimer::timeout, this, &RootConfig::reload); ++ ++ m_saveTimer->setSingleShot(true); ++ m_saveTimer->setInterval(500); ++ connect(m_saveTimer, &QTimer::timeout, this, [this] { ++ QDir().mkpath(QFileInfo(m_filePath).absolutePath()); ++ ++ QFile file(m_filePath); ++ if (!file.open(QIODevice::WriteOnly)) { ++ auto err = QStringLiteral("Failed to write %1: %2").arg(m_filePath, file.errorString()); ++ qCWarning(lcConfig, "%s", qUtf8Printable(err)); ++ emit saveFailed(err, m_screen); ++ return; ++ } ++ ++ auto json = toJsonObject(); ++ file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); ++ file.close(); ++ ++ // Update watches — save may have created directories ++ updateWatch(); ++ ++ emit saved(m_screen); ++ }); ++ ++ m_cooldownTimer->setSingleShot(true); ++ m_cooldownTimer->setInterval(2000); ++ connect(m_cooldownTimer, &QTimer::timeout, this, [this] { ++ m_recentlySaved = false; ++ }); ++ ++ m_reloadDebounce = new QTimer(this); ++ m_reloadDebounce->setSingleShot(true); ++ m_reloadDebounce->setInterval(50); ++ connect(m_reloadDebounce, &QTimer::timeout, this, &RootConfig::reload); ++ ++ // Auto-save when any property changes (debounced by the save timer) ++ connectAutoSave(this); ++ ++ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RootConfig::onWatcherEvent); ++ connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onWatcherEvent); ++ ++ qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; ++ ++ updateWatch(); ++ ++ // Load immediately so values are available during construction. ++ // Defer signal emissions to next event loop tick so QML has time to connect. ++ auto result = reloadFromFile(); ++ QTimer::singleShot(0, this, [this, result] { ++ emitLoadSignals(result, false); ++ }); ++} ++ ++void RootConfig::connectAutoSave(ConfigObject* obj) { ++ connect(obj, &ConfigObject::propertiesChanged, this, [this] { ++ if (!m_loading) ++ saveToFile(); ++ }); ++ ++ // Recurse into sub-objects ++ const auto* meta = obj->metaObject(); ++ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { ++ auto prop = meta->property(i); ++ auto value = prop.read(obj); ++ auto* subObj = value.value(); ++ if (subObj) ++ connectAutoSave(subObj); ++ } ++} ++ ++void RootConfig::updateWatch() { ++ auto targetDir = QFileInfo(m_filePath).absolutePath(); ++ ++ // Find the nearest existing directory, walking up toward the watch root ++ auto dir = targetDir; ++ while (!QFile::exists(dir) && dir != watchRoot() && !dir.isEmpty()) { ++ auto parent = QFileInfo(dir).absolutePath(); ++ if (parent == dir) ++ break; // reached filesystem root ++ dir = parent; ++ } ++ ++ // Update directory watch if it changed ++ if (dir != m_watchedDir) { ++ if (!m_watchedDir.isEmpty()) ++ m_watcher->removePath(m_watchedDir); ++ ++ m_watchedDir = dir; ++ ++ if (QFile::exists(dir)) ++ m_watcher->addPath(dir); ++ } ++ ++ // Watch the file itself if it exists (for in-place modifications) ++ if (QFile::exists(m_filePath)) { ++ if (!m_watcher->files().contains(m_filePath)) ++ m_watcher->addPath(m_filePath); ++ } ++} ++ ++void RootConfig::onWatcherEvent() { ++ // Re-evaluate what to watch — directories may have been created or deleted ++ updateWatch(); ++ ++ if (!m_recentlySaved) ++ m_reloadDebounce->start(); ++} ++ ++void RootConfig::saveToFile() { ++ if (!m_saveTimer) ++ return; ++ m_saveTimer->start(); ++ m_recentlySaved = true; ++ m_cooldownTimer->start(); ++} ++ ++std::optional RootConfig::reloadFromFile() { ++ QFile file(m_filePath); ++ ++ if (!file.exists()) { ++ qCDebug(lcConfig) << "File does not exist:" << m_filePath; ++ return std::nullopt; ++ } ++ ++ if (!file.open(QIODevice::ReadOnly)) { ++ auto err = QStringLiteral("Failed to open %1: %2").arg(m_filePath, file.errorString()); ++ qCDebug(lcConfig, "%s", qUtf8Printable(err)); ++ return err; ++ } ++ ++ QJsonParseError error{}; ++ auto doc = QJsonDocument::fromJson(file.readAll(), &error); ++ ++ if (error.error != QJsonParseError::NoError) { ++ if (m_retryTimer && m_parseRetries < 3) { ++ m_parseRetries++; ++ qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath), ++ qUtf8Printable(error.errorString()), m_parseRetries); ++ m_retryTimer->start(); ++ return std::nullopt; // pending retry — no signal ++ } ++ ++ qCWarning(lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); ++ m_parseRetries = 0; ++ return QStringLiteral("JSON parse error: %1").arg(error.errorString()); ++ } ++ ++ m_parseRetries = 0; ++ ++ qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; ++ ++ m_loading = true; ++ ++ clearLoadedKeys(); ++ ++ auto jsonObj = doc.object(); ++ loadFromJson(jsonObj); ++ ++ m_loading = false; ++ ++ // Collect unknown keys — caller is responsible for emitting signals ++ m_lastUnknownKeys = collectUnknownKeys(this, jsonObj); ++ ++ return QString(); // success ++} ++ ++void RootConfig::save() { ++ saveToFile(); ++} ++ ++void RootConfig::emitLoadSignals(const std::optional& result, bool emitLoaded) { ++ if (!result.has_value()) ++ return; ++ ++ for (const auto& key : std::as_const(m_lastUnknownKeys)) ++ emit unknownOption(key, m_screen); ++ m_lastUnknownKeys.clear(); ++ ++ if (result->isEmpty()) { ++ if (emitLoaded) ++ emit loaded(m_screen); ++ } else { ++ emit loadFailed(*result, m_screen); ++ } ++} ++ ++void RootConfig::reload() { ++ emitLoadSignals(reloadFromFile()); ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp +new file mode 100644 +index 00000000..bf9b8e2c +--- /dev/null ++++ b/plugin/src/Caelestia/Config/rootconfig.hpp +@@ -0,0 +1,59 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++// Intermediate base for singleton config roots (GlobalConfig, TokenConfig). ++// Provides file-backed persistence, save/reload, and lifecycle signals. ++class RootConfig : public ConfigObject { ++ Q_OBJECT ++ ++public: ++ explicit RootConfig(QObject* parent = nullptr); ++ ++ void setupFileBackend(const QString& path, const QString& screen = {}); ++ void saveToFile(); ++ // Returns nullopt if retrying, empty string on success, error message on failure. ++ [[nodiscard]] std::optional reloadFromFile(); ++ ++ [[nodiscard]] bool recentlySaved() const; ++ ++ Q_INVOKABLE void save(); ++ Q_INVOKABLE void reload(); ++ ++signals: ++ void loaded(const QString& screen); ++ void loadFailed(const QString& error, const QString& screen); ++ void saved(const QString& screen); ++ void saveFailed(const QString& error, const QString& screen); ++ void unknownOption(const QString& key, const QString& screen); ++ ++private: ++ static QStringList collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json); ++ void emitLoadSignals(const std::optional& result, bool emitLoaded = true); ++ void updateWatch(); ++ void onWatcherEvent(); ++ ++ void connectAutoSave(ConfigObject* obj); ++ ++ QString m_filePath; ++ QString m_screen; ++ QString m_watchedDir; ++ bool m_recentlySaved = false; ++ bool m_loading = false; ++ ++ QFileSystemWatcher* m_watcher = nullptr; ++ QTimer* m_saveTimer = nullptr; ++ QTimer* m_cooldownTimer = nullptr; ++ QTimer* m_retryTimer = nullptr; ++ QTimer* m_reloadDebounce = nullptr; ++ int m_parseRetries = 0; ++ QStringList m_lastUnknownKeys; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp +new file mode 100644 +index 00000000..b97c69c1 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/serviceconfig.hpp +@@ -0,0 +1,43 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++ ++namespace caelestia::config { ++ ++using Qt::StringLiterals::operator""_s; ++ ++class ServiceConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(QString, weatherLocation) ++ // Guess based on locale ++ CONFIG_GLOBAL_PROPERTY(bool, useFahrenheit, ++ QLocale().measurementSystem() == QLocale::ImperialUSSystem || ++ QLocale().measurementSystem() == QLocale::ImperialUKSystem) ++ // This is always false by default cause apparently even imperial system users don't use it for perf temps? ++ CONFIG_GLOBAL_PROPERTY(bool, useFahrenheitPerformance, false) ++ // Attempt to guess based on locale ++ CONFIG_GLOBAL_PROPERTY( ++ bool, useTwelveHourClock, QLocale().timeFormat(QLocale::ShortFormat).toLower().contains(u"a"_s)) ++ CONFIG_GLOBAL_PROPERTY(QString, gpuType) ++ CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 45) ++ CONFIG_GLOBAL_PROPERTY(qreal, audioIncrement, 0.1) ++ CONFIG_GLOBAL_PROPERTY(qreal, brightnessIncrement, 0.1) ++ CONFIG_GLOBAL_PROPERTY(qreal, maxVolume, 1.0) ++ CONFIG_GLOBAL_PROPERTY(bool, smartScheme, true) ++ CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, u"Spotify"_s) ++ CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases, ++ { vmap({ { u"from"_s, u"com.github.th_ch.youtube_music"_s }, { u"to"_s, u"YT Music"_s } }) }) ++ CONFIG_GLOBAL_PROPERTY(bool, showLyrics, false) ++ CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, u"Auto"_s) ++ ++public: ++ explicit ServiceConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/sessionconfig.hpp b/plugin/src/Caelestia/Config/sessionconfig.hpp +new file mode 100644 +index 00000000..7e7548f1 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/sessionconfig.hpp +@@ -0,0 +1,57 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++ ++namespace caelestia::config { ++ ++using Qt::StringLiterals::operator""_s; ++ ++class SessionIcons : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(QString, logout, u"logout"_s) ++ CONFIG_PROPERTY(QString, shutdown, u"power_settings_new"_s) ++ CONFIG_PROPERTY(QString, hibernate, u"downloading"_s) ++ CONFIG_PROPERTY(QString, reboot, u"cached"_s) ++ ++public: ++ explicit SessionIcons(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class SessionCommands : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(QStringList, logout, { u"loginctl"_s, u"terminate-user"_s, u""_s }) ++ CONFIG_PROPERTY(QStringList, shutdown, { u"systemctl"_s, u"poweroff"_s }) ++ CONFIG_PROPERTY(QStringList, hibernate, { u"systemctl"_s, u"hibernate"_s }) ++ CONFIG_PROPERTY(QStringList, reboot, { u"systemctl"_s, u"reboot"_s }) ++ ++public: ++ explicit SessionCommands(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class SessionConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(int, dragThreshold, 30) ++ CONFIG_PROPERTY(bool, vimKeybinds, false) ++ CONFIG_SUBOBJECT(SessionIcons, icons) ++ CONFIG_SUBOBJECT(SessionCommands, commands) ++ ++public: ++ explicit SessionConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_icons(new SessionIcons(this)) ++ , m_commands(new SessionCommands(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/sidebarconfig.hpp b/plugin/src/Caelestia/Config/sidebarconfig.hpp +new file mode 100644 +index 00000000..4460872e +--- /dev/null ++++ b/plugin/src/Caelestia/Config/sidebarconfig.hpp +@@ -0,0 +1,19 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++namespace caelestia::config { ++ ++class SidebarConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(int, dragThreshold, 80) ++ ++public: ++ explicit SidebarConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp +new file mode 100644 +index 00000000..6d171cb3 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/tokens.cpp +@@ -0,0 +1,54 @@ ++#include "tokens.hpp" ++#include "monitorconfigmanager.hpp" ++ ++#include ++#include ++ ++namespace caelestia::config { ++ ++namespace { ++ ++QString configDir() { ++ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); ++} ++ ++} // namespace ++ ++TokenConfig::TokenConfig(QObject* parent) ++ : RootConfig(parent) ++ , m_appearance(new AppearanceTokens(this)) ++ , m_sizes(new SizeTokens(this)) { ++ setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); ++} ++ ++TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) ++ : RootConfig(parent) ++ , m_appearance(new AppearanceTokens(this)) ++ , m_sizes(new SizeTokens(this)) { ++ if (!filePath.isEmpty()) ++ setupFileBackend(filePath, screen); ++ if (fallback) ++ syncFromGlobal(fallback); ++} ++ ++TokenConfig* TokenConfig::instance() { ++ static TokenConfig instance; ++ return &instance; ++} ++ ++TokenConfig* TokenConfig::defaults() { ++ if (!m_defaults) ++ m_defaults = new TokenConfig(nullptr, QString(), QString(), this); ++ return m_defaults; ++} ++ ++TokenConfig* TokenConfig::forScreen(const QString& screen) { ++ return MonitorConfigManager::instance()->tokensForScreen(screen); ++} ++ ++TokenConfig* TokenConfig::create(QQmlEngine*, QJSEngine*) { ++ QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); ++ return instance(); ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp +new file mode 100644 +index 00000000..28bac1f1 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/tokens.hpp +@@ -0,0 +1,351 @@ ++#pragma once ++ ++#include "rootconfig.hpp" ++ ++#include ++#include ++ ++namespace caelestia::config { ++ ++class AnimCurves : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(QList, emphasized) ++ CONFIG_GLOBAL_PROPERTY(QList, emphasizedAccel) ++ CONFIG_GLOBAL_PROPERTY(QList, emphasizedDecel) ++ CONFIG_GLOBAL_PROPERTY(QList, standard) ++ CONFIG_GLOBAL_PROPERTY(QList, standardAccel) ++ CONFIG_GLOBAL_PROPERTY(QList, standardDecel) ++ CONFIG_GLOBAL_PROPERTY(QList, expressiveFastSpatial) ++ CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultSpatial) ++ CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowSpatial) ++ CONFIG_GLOBAL_PROPERTY(QList, expressiveFastEffects) ++ CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultEffects) ++ CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowEffects) ++ ++public: ++ explicit AnimCurves(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_emphasized({ 0.05, 0, 2.0 / 15.0, 0.06, 1.0 / 6.0, 0.4, 5.0 / 24.0, 0.82, 0.25, 1, 1, 1 }) ++ , m_emphasizedAccel({ 0.3, 0, 0.8, 0.15, 1, 1 }) ++ , m_emphasizedDecel({ 0.05, 0.7, 0.1, 1, 1, 1 }) ++ , m_standard({ 0.2, 0, 0, 1, 1, 1 }) ++ , m_standardAccel({ 0.3, 0, 1, 1, 1, 1 }) ++ , m_standardDecel({ 0, 0, 0, 1, 1, 1 }) ++ , m_expressiveFastSpatial({ 0.42, 1.67, 0.21, 0.9, 1, 1 }) ++ , m_expressiveDefaultSpatial({ 0.38, 1.21, 0.22, 1, 1, 1 }) ++ , m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) ++ , m_expressiveFastEffects({ 0.31, 0.94, 0.34, 1, 1, 1 }) ++ , m_expressiveDefaultEffects({ 0.34, 0.8, 0.34, 1, 1, 1 }) ++ , m_expressiveSlowEffects({ 0.34, 0.88, 0.34, 1, 1, 1 }) {} ++}; ++ ++class RoundingTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, extraSmall, 4) ++ CONFIG_PROPERTY(int, small, 12) ++ CONFIG_PROPERTY(int, normal, 17) ++ CONFIG_PROPERTY(int, large, 25) ++ CONFIG_PROPERTY(int, full, 1000) ++ ++public: ++ explicit RoundingTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class SpacingTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, small, 7) ++ CONFIG_PROPERTY(int, smaller, 10) ++ CONFIG_PROPERTY(int, normal, 12) ++ CONFIG_PROPERTY(int, larger, 15) ++ CONFIG_PROPERTY(int, large, 20) ++ ++public: ++ explicit SpacingTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class PaddingTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, small, 5) ++ CONFIG_PROPERTY(int, smaller, 7) ++ CONFIG_PROPERTY(int, normal, 10) ++ CONFIG_PROPERTY(int, larger, 12) ++ CONFIG_PROPERTY(int, large, 15) ++ ++public: ++ explicit PaddingTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class FontSizeTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, small, 11) ++ CONFIG_PROPERTY(int, smaller, 12) ++ CONFIG_PROPERTY(int, normal, 13) ++ CONFIG_PROPERTY(int, larger, 15) ++ CONFIG_PROPERTY(int, large, 18) ++ CONFIG_PROPERTY(int, extraLarge, 28) ++ ++public: ++ explicit FontSizeTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class AnimDurationTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(int, small, 200) ++ CONFIG_GLOBAL_PROPERTY(int, normal, 400) ++ CONFIG_GLOBAL_PROPERTY(int, large, 600) ++ CONFIG_GLOBAL_PROPERTY(int, extraLarge, 1000) ++ CONFIG_GLOBAL_PROPERTY(int, expressiveFastSpatial, 350) ++ CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultSpatial, 500) ++ CONFIG_GLOBAL_PROPERTY(int, expressiveSlowSpatial, 650) ++ CONFIG_GLOBAL_PROPERTY(int, expressiveFastEffects, 150) ++ CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultEffects, 200) ++ CONFIG_GLOBAL_PROPERTY(int, expressiveSlowEffects, 300) ++ ++public: ++ explicit AnimDurationTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class AppearanceTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_SUBOBJECT(AnimCurves, curves) ++ CONFIG_SUBOBJECT(RoundingTokens, rounding) ++ CONFIG_SUBOBJECT(SpacingTokens, spacing) ++ CONFIG_SUBOBJECT(PaddingTokens, padding) ++ CONFIG_SUBOBJECT(FontSizeTokens, fontSize) ++ CONFIG_SUBOBJECT(AnimDurationTokens, animDurations) ++ ++public: ++ explicit AppearanceTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_curves(new AnimCurves(this)) ++ , m_rounding(new RoundingTokens(this)) ++ , m_spacing(new SpacingTokens(this)) ++ , m_padding(new PaddingTokens(this)) ++ , m_fontSize(new FontSizeTokens(this)) ++ , m_animDurations(new AnimDurationTokens(this)) {} ++}; ++ ++class BarTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, innerWidth, 40) ++ CONFIG_PROPERTY(int, windowPreviewSize, 400) ++ CONFIG_PROPERTY(int, trayMenuWidth, 300) ++ CONFIG_PROPERTY(int, batteryWidth, 250) ++ CONFIG_PROPERTY(int, networkWidth, 320) ++ CONFIG_PROPERTY(int, kbLayoutWidth, 320) ++ ++public: ++ explicit BarTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class DashboardTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, tabIndicatorHeight, 3) ++ CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) ++ CONFIG_PROPERTY(int, infoWidth, 200) ++ CONFIG_PROPERTY(int, infoIconSize, 25) ++ CONFIG_PROPERTY(int, dateTimeWidth, 110) ++ CONFIG_PROPERTY(int, mediaWidth, 200) ++ CONFIG_PROPERTY(int, mediaProgressSweep, 180) ++ CONFIG_PROPERTY(int, mediaProgressThickness, 8) ++ CONFIG_PROPERTY(int, resourceProgressThickness, 10) ++ CONFIG_PROPERTY(int, weatherWidth, 250) ++ CONFIG_PROPERTY(int, mediaCoverArtSize, 150) ++ CONFIG_PROPERTY(int, mediaVisualiserSize, 80) ++ CONFIG_PROPERTY(int, resourceSize, 200) ++ ++public: ++ explicit DashboardTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class LauncherTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, itemWidth, 600) ++ CONFIG_PROPERTY(int, itemHeight, 57) ++ CONFIG_PROPERTY(int, wallpaperWidth, 280) ++ CONFIG_PROPERTY(int, wallpaperHeight, 200) ++ ++public: ++ explicit LauncherTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class NotifsTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, width, 400) ++ CONFIG_GLOBAL_PROPERTY(int, image, 41) ++ CONFIG_PROPERTY(int, badge, 20) ++ ++public: ++ explicit NotifsTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class OsdTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, sliderWidth, 30) ++ CONFIG_PROPERTY(int, sliderHeight, 150) ++ ++public: ++ explicit OsdTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class SessionTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, button, 80) ++ ++public: ++ explicit SessionTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class SidebarTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, width, 430) ++ ++public: ++ explicit SidebarTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class UtilitiesTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(int, width, 430) ++ CONFIG_PROPERTY(int, toastWidth, 430) ++ ++public: ++ explicit UtilitiesTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class LockTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, heightMult, 0.7) ++ CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) ++ CONFIG_PROPERTY(int, centerWidth, 600) ++ ++public: ++ explicit LockTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class WInfoTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, heightMult, 0.7) ++ CONFIG_PROPERTY(qreal, detailsWidth, 500) ++ ++public: ++ explicit WInfoTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class ControlCenterTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(qreal, heightMult, 0.7) ++ CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) ++ ++public: ++ explicit ControlCenterTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class SizeTokens : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_SUBOBJECT(BarTokens, bar) ++ CONFIG_SUBOBJECT(DashboardTokens, dashboard) ++ CONFIG_SUBOBJECT(LauncherTokens, launcher) ++ CONFIG_SUBOBJECT(NotifsTokens, notifs) ++ CONFIG_SUBOBJECT(OsdTokens, osd) ++ CONFIG_SUBOBJECT(SessionTokens, session) ++ CONFIG_SUBOBJECT(SidebarTokens, sidebar) ++ CONFIG_SUBOBJECT(UtilitiesTokens, utilities) ++ CONFIG_SUBOBJECT(LockTokens, lock) ++ CONFIG_SUBOBJECT(WInfoTokens, winfo) ++ CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter) ++ ++public: ++ explicit SizeTokens(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_bar(new BarTokens(this)) ++ , m_dashboard(new DashboardTokens(this)) ++ , m_launcher(new LauncherTokens(this)) ++ , m_notifs(new NotifsTokens(this)) ++ , m_osd(new OsdTokens(this)) ++ , m_session(new SessionTokens(this)) ++ , m_sidebar(new SidebarTokens(this)) ++ , m_utilities(new UtilitiesTokens(this)) ++ , m_lock(new LockTokens(this)) ++ , m_winfo(new WInfoTokens(this)) ++ , m_controlCenter(new ControlCenterTokens(this)) {} ++}; ++ ++class TokenConfig : public RootConfig { ++ Q_OBJECT ++ QML_ELEMENT ++ QML_SINGLETON ++ ++ CONFIG_SUBOBJECT(AppearanceTokens, appearance) ++ CONFIG_SUBOBJECT(SizeTokens, sizes) ++ ++public: ++ static TokenConfig* instance(); ++ [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); ++ [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); ++ static TokenConfig* create(QQmlEngine*, QJSEngine*); ++ ++private: ++ friend class MonitorConfigManager; ++ explicit TokenConfig(QObject* parent = nullptr); ++ explicit TokenConfig( ++ TokenConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); ++ ++ TokenConfig* m_defaults = nullptr; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp +new file mode 100644 +index 00000000..16f863b8 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/tokensattached.cpp +@@ -0,0 +1,118 @@ ++#include "tokensattached.hpp" ++#include "anim.hpp" ++#include "appearanceconfig.hpp" ++#include "config.hpp" ++#include "monitorconfigmanager.hpp" ++#include "tokens.hpp" ++ ++#include ++ ++namespace caelestia::config { ++ ++namespace { ++ ++const AppearanceConfig* resolveAppearance(GlobalConfig* config, bool complete, const char* prop, QObject* parent) { ++ if (config) ++ return config->appearance(); ++ if ((complete || !qobject_cast(parent)) && parent) ++ qCWarning(lcConfig, "Tokens.%s accessed without a screen set on %s", prop, parent->metaObject()->className()); ++ return GlobalConfig::instance()->appearance(); ++} ++ ++} // namespace ++ ++Tokens::Tokens(QObject* parent) ++ : QQuickAttachedPropertyPropagator(parent) ++ , m_anim(new AnimTokens(this)) { ++ bindAnim(); ++ initialize(); ++} ++ ++void Tokens::classBegin() {} ++ ++void Tokens::componentComplete() { ++ m_complete = true; ++} ++ ++QString Tokens::screen() const { ++ return m_screen; ++} ++ ++void Tokens::inheritScreen(const QString& screen) { ++ if (screen == m_screen) ++ return; ++ ++ m_screen = screen; ++ ++ if (m_screen.isEmpty()) { ++ m_config = nullptr; ++ m_tokens = nullptr; ++ } else { ++ m_config = MonitorConfigManager::instance()->configForScreen(m_screen); ++ m_tokens = MonitorConfigManager::instance()->tokensForScreen(m_screen); ++ } ++ ++ propagateScreen(); ++ emit sourceChanged(); ++} ++ ++void Tokens::propagateScreen() { ++ const auto children = attachedChildren(); ++ for (auto* const child : children) { ++ auto* const tokens = qobject_cast(child); ++ if (tokens) ++ tokens->inheritScreen(m_screen); ++ } ++} ++ ++void Tokens::attachedParentChange( ++ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { ++ Q_UNUSED(oldParent); ++ auto* const tokens = qobject_cast(newParent); ++ if (tokens) ++ inheritScreen(tokens->screen()); ++} ++ ++void Tokens::bindAnim() { ++ m_anim->bindDurations(GlobalConfig::instance()->appearance()->anim()->durations()); ++ m_anim->bindCurves(TokenConfig::instance()->appearance()->curves()); ++} ++ ++#define TOKENS_ATTACHED_GETTER(Type, name) \ ++ const Type* Tokens::name() const { \ ++ auto* a = resolveAppearance(m_config, m_complete, #name, parent()); \ ++ return a ? a->name() : nullptr; \ ++ } ++ ++TOKENS_ATTACHED_GETTER(AppearanceRounding, rounding) ++TOKENS_ATTACHED_GETTER(AppearanceSpacing, spacing) ++TOKENS_ATTACHED_GETTER(AppearancePadding, padding) ++TOKENS_ATTACHED_GETTER(AppearanceFont, font) ++ ++#undef TOKENS_ATTACHED_GETTER ++ ++const AppearanceTransparency* Tokens::transparency() const { ++ return GlobalConfig::instance()->appearance()->transparency(); // Transparency is always global ++} ++ ++const SizeTokens* Tokens::sizes() const { ++ if (m_tokens) ++ return m_tokens->sizes(); ++ if ((m_complete || !qobject_cast(parent())) && parent()) ++ qCWarning(lcConfig, "Tokens.sizes accessed without a screen set on %s", parent()->metaObject()->className()); ++ return TokenConfig::instance()->sizes(); ++} ++ ++const AnimTokens* Tokens::anim() const { ++ return m_anim; ++} ++ ++TokenConfig* Tokens::forScreen(const QString& screen) { ++ return TokenConfig::forScreen(screen); ++} ++ ++Tokens* Tokens::qmlAttachedProperties(QObject* object) { ++ return new Tokens(object); ++} ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp +new file mode 100644 +index 00000000..810b0c0d +--- /dev/null ++++ b/plugin/src/Caelestia/Config/tokensattached.hpp +@@ -0,0 +1,78 @@ ++#pragma once ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++class AnimTokens; ++class AppearanceFont; ++class AppearancePadding; ++class AppearanceRounding; ++class AppearanceSpacing; ++class AppearanceTransparency; ++class GlobalConfig; ++class SizeTokens; ++class TokenConfig; ++ ++class Tokens : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { ++ Q_OBJECT ++ Q_INTERFACES(QQmlParserStatus) ++ QML_ELEMENT ++ QML_UNCREATABLE("") ++ QML_ATTACHED(Tokens) ++ Q_MOC_INCLUDE("anim.hpp") ++ Q_MOC_INCLUDE("appearanceconfig.hpp") ++ Q_MOC_INCLUDE("tokens.hpp") ++ ++ Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) ++ Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) ++ ++public: ++ explicit Tokens(QObject* parent = nullptr); ++ ++ [[nodiscard]] QString screen() const; ++ void inheritScreen(const QString& screen); ++ ++ [[nodiscard]] const AppearanceRounding* rounding() const; ++ [[nodiscard]] const AppearanceSpacing* spacing() const; ++ [[nodiscard]] const AppearancePadding* padding() const; ++ [[nodiscard]] const AppearanceFont* font() const; ++ [[nodiscard]] const AppearanceTransparency* transparency() const; ++ ++ [[nodiscard]] const SizeTokens* sizes() const; ++ [[nodiscard]] const AnimTokens* anim() const; ++ ++ [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); ++ ++ static Tokens* qmlAttachedProperties(QObject* object); ++ ++signals: ++ void sourceChanged(); ++ ++protected: ++ void attachedParentChange( ++ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; ++ ++private: ++ void classBegin() override; ++ void componentComplete() override; ++ ++ void propagateScreen(); ++ void bindAnim(); ++ ++ bool m_complete = false; ++ QString m_screen; ++ GlobalConfig* m_config = nullptr; ++ TokenConfig* m_tokens = nullptr; ++ AnimTokens* m_anim = nullptr; ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/userpaths.hpp b/plugin/src/Caelestia/Config/userpaths.hpp +new file mode 100644 +index 00000000..da6973a7 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/userpaths.hpp +@@ -0,0 +1,30 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++#include ++ ++namespace caelestia::config { ++ ++using Qt::StringLiterals::operator""_s; ++ ++class UserPaths : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY( ++ QString, wallpaperDir, QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + u"/Wallpapers"_s) ++ CONFIG_GLOBAL_PROPERTY(QString, lyricsDir, QDir::homePath() + u"/Music/lyrics/"_s) ++ CONFIG_PROPERTY(QString, sessionGif, u"root:/assets/kurukuru.gif"_s) ++ CONFIG_PROPERTY(QString, mediaGif, u"root:/assets/bongocat.gif"_s) ++ CONFIG_PROPERTY(QString, noNotifsPic, u"root:/assets/dino.png"_s) ++ CONFIG_PROPERTY(QString, lockNoNotifsPic, u"root:/assets/dino.png"_s) ++ ++public: ++ explicit UserPaths(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/utilitiesconfig.hpp b/plugin/src/Caelestia/Config/utilitiesconfig.hpp +new file mode 100644 +index 00000000..153f21d5 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/utilitiesconfig.hpp +@@ -0,0 +1,88 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++#include ++#include ++ ++namespace caelestia::config { ++ ++using Qt::StringLiterals::operator""_s; ++ ++class UtilitiesToasts : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(QString, fullscreen, u"off"_s) ++ CONFIG_GLOBAL_PROPERTY(bool, configLoaded, true) ++ CONFIG_GLOBAL_PROPERTY(bool, chargingChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, gameModeChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, dndChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, audioOutputChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, audioInputChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, capsLockChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, numLockChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, kbLayoutChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, kbLimit, true) ++ CONFIG_GLOBAL_PROPERTY(bool, vpnChanged, true) ++ CONFIG_GLOBAL_PROPERTY(bool, nowPlaying, false) ++ ++public: ++ explicit UtilitiesToasts(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class UtilitiesVpn : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_GLOBAL_PROPERTY(bool, enabled, false) ++ CONFIG_GLOBAL_PROPERTY(QVariantList, provider) ++ ++public: ++ explicit UtilitiesVpn(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class UtilitiesRecording : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(QString, videoMode, u"fullscreen"_s) ++ CONFIG_PROPERTY(bool, recordSystem, true) ++ CONFIG_PROPERTY(bool, recordMicrophone, false) ++ ++public: ++ explicit UtilitiesRecording(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++class UtilitiesConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++ CONFIG_PROPERTY(bool, enabled, true) ++ CONFIG_PROPERTY(int, maxToasts, 4) ++ CONFIG_SUBOBJECT(UtilitiesToasts, toasts) ++ CONFIG_SUBOBJECT(UtilitiesVpn, vpn) ++ CONFIG_SUBOBJECT(UtilitiesRecording, recording) ++ CONFIG_PROPERTY(QVariantList, quickToggles, ++ { ++ vmap({ { u"id"_s, u"wifi"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"bluetooth"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"mic"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"settings"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"gameMode"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"dnd"_s }, { u"enabled"_s, true } }), ++ vmap({ { u"id"_s, u"vpn"_s }, { u"enabled"_s, false } }), ++ }) ++ ++public: ++ explicit UtilitiesConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) ++ , m_toasts(new UtilitiesToasts(this)) ++ , m_vpn(new UtilitiesVpn(this)) ++ , m_recording(new UtilitiesRecording(this)) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Config/winfoconfig.hpp b/plugin/src/Caelestia/Config/winfoconfig.hpp +new file mode 100644 +index 00000000..30d4f5e6 +--- /dev/null ++++ b/plugin/src/Caelestia/Config/winfoconfig.hpp +@@ -0,0 +1,18 @@ ++#pragma once ++ ++#include "configobject.hpp" ++ ++namespace caelestia::config { ++ ++// WInfoConfig has no serialized properties (serializer returns {}) ++// All properties are in AdvancedConfig.winfo ++class WInfoConfig : public ConfigObject { ++ Q_OBJECT ++ QML_ANONYMOUS ++ ++public: ++ explicit WInfoConfig(QObject* parent = nullptr) ++ : ConfigObject(parent) {} ++}; ++ ++} // namespace caelestia::config +diff --git a/plugin/src/Caelestia/Images/CMakeLists.txt b/plugin/src/Caelestia/Images/CMakeLists.txt +new file mode 100644 +index 00000000..d869667d +--- /dev/null ++++ b/plugin/src/Caelestia/Images/CMakeLists.txt +@@ -0,0 +1,11 @@ ++qml_module(caelestia-images ++ URI Caelestia.Images ++ SOURCES ++ cachingimageprovider.cpp ++ imagecacher.cpp ++ iutils.cpp ++ LIBRARIES ++ Qt::Gui ++ Qt::Quick ++ Qt::Concurrent ++) +diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp +new file mode 100644 +index 00000000..768e05da +--- /dev/null ++++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp +@@ -0,0 +1,120 @@ ++#include "cachingimageprovider.hpp" ++ ++#include "imagecacher.hpp" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg) ++ ++namespace caelestia::images { ++ ++namespace { ++ ++class CachingImageResponse final : public QQuickImageResponse, public QRunnable { ++public: ++ CachingImageResponse(const QString& id, const QSize& requestedSize, ImageCacher::FillMode fillMode) ++ : m_id(id) ++ , m_requestedSize(requestedSize) ++ , m_fillMode(fillMode) { ++ setAutoDelete(false); ++ } ++ ++ [[nodiscard]] QQuickTextureFactory* textureFactory() const override { ++ return QQuickTextureFactory::textureFactoryForImage(m_image); ++ } ++ ++ [[nodiscard]] QString errorString() const override { return m_error; } ++ ++ void run() override { ++ process(); ++ emit finished(); ++ } ++ ++private: ++ void process() { ++ QString path = QString::fromUtf8(m_id.toUtf8().percentDecoded()); ++ if (!path.startsWith(QLatin1Char('/'))) ++ path.prepend(QLatin1Char('/')); ++ ++ if (!QFileInfo::exists(path)) { ++ m_error = QStringLiteral("Source file does not exist: ") + path; ++ qCWarning(lcCProv).noquote() << m_error; ++ return; ++ } ++ ++ QSize size = m_requestedSize; ++ const bool needsW = size.width() <= 0; ++ const bool needsH = size.height() <= 0; ++ ++ // If both dimensions are missing, return the original directly ++ if (needsW && needsH) { ++ qCDebug(lcCProv).noquote() << "Given source size is invalid, returning original:" << path; ++ m_image = QImage(path); ++ if (m_image.isNull()) { ++ m_error = QStringLiteral("Failed to decode source: ") + path; ++ qCWarning(lcCProv).noquote() << m_error; ++ } ++ return; ++ } ++ ++ // If one dimension is missing, derive it from the source aspect ratio ++ if (needsW || needsH) { ++ const QImageReader sourceReader(path); ++ const QSize sourceSize = sourceReader.size(); ++ if (!sourceSize.isValid() || sourceSize.isEmpty()) { ++ m_error = QStringLiteral("Could not determine source size for: ") + path; ++ qCWarning(lcCProv).noquote() << m_error; ++ return; ++ } ++ ++ if (needsW) ++ size.setWidth(qRound(size.height() * sourceSize.width() / static_cast(sourceSize.height()))); ++ else ++ size.setHeight(qRound(size.width() * sourceSize.height() / static_cast(sourceSize.width()))); ++ } ++ ++ // Try to use cached image ++ const auto cachePath = ImageCacher::cachePathFor(path, size, m_fillMode); ++ if (!cachePath.isEmpty()) { ++ QImageReader cacheReader(cachePath); ++ if (cacheReader.canRead()) { ++ m_image = cacheReader.read(); ++ if (!m_image.isNull()) ++ return; ++ } ++ } ++ ++ // Schedule cache job (this call will return the original image, but later ones will use cache) ++ ImageCacher::instance()->schedule(path, cachePath, size, m_fillMode); ++ ++ m_image = QImage(path); ++ if (m_image.isNull()) { ++ m_error = QStringLiteral("Failed to decode source: ") + path; ++ qCWarning(lcCProv).noquote() << m_error; ++ } ++ } ++ ++ QString m_id; ++ QSize m_requestedSize; ++ ImageCacher::FillMode m_fillMode; ++ QImage m_image; ++ QString m_error; ++}; ++ ++} // namespace ++ ++CachingImageProvider::CachingImageProvider(FillMode fillMode) ++ : m_fillMode(fillMode) {} ++ ++QQuickImageResponse* CachingImageProvider::requestImageResponse(const QString& id, const QSize& requestedSize) { ++ auto* const response = new CachingImageResponse(id, requestedSize, m_fillMode); ++ QThreadPool::globalInstance()->start(response); ++ return response; ++} ++ ++} // namespace caelestia::images +diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.hpp b/plugin/src/Caelestia/Images/cachingimageprovider.hpp +new file mode 100644 +index 00000000..97082c76 +--- /dev/null ++++ b/plugin/src/Caelestia/Images/cachingimageprovider.hpp +@@ -0,0 +1,21 @@ ++#pragma once ++ ++#include "imagecacher.hpp" ++ ++#include ++ ++namespace caelestia::images { ++ ++class CachingImageProvider : public QQuickAsyncImageProvider { ++public: ++ using FillMode = ImageCacher::FillMode; ++ ++ explicit CachingImageProvider(FillMode fillMode); ++ ++ QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override; ++ ++private: ++ FillMode m_fillMode; ++}; ++ ++} // namespace caelestia::images +diff --git a/plugin/src/Caelestia/Images/imagecacher.cpp b/plugin/src/Caelestia/Images/imagecacher.cpp +new file mode 100644 +index 00000000..dfef8f0b +--- /dev/null ++++ b/plugin/src/Caelestia/Images/imagecacher.cpp +@@ -0,0 +1,159 @@ ++#include "imagecacher.hpp" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++Q_LOGGING_CATEGORY(lcCacher, "caelestia.images.cacher", QtInfoMsg) ++ ++namespace caelestia::images { ++ ++namespace { ++ ++QString sha256sum(const QString& path) { ++ QFile file(path); ++ if (!file.open(QIODevice::ReadOnly)) { ++ qCWarning(lcCacher).noquote() << "sha256sum: failed to open" << path; ++ return {}; ++ } ++ ++ QCryptographicHash hash(QCryptographicHash::Sha256); ++ hash.addData(&file); ++ file.close(); ++ ++ return hash.result().toHex(); ++} ++ ++QString fillSuffix(ImageCacher::FillMode fillMode) { ++ switch (fillMode) { ++ case ImageCacher::FillMode::Crop: ++ return QStringLiteral("crop"); ++ case ImageCacher::FillMode::Fit: ++ return QStringLiteral("fit"); ++ default: ++ return QStringLiteral("stretch"); ++ } ++} ++ ++} // namespace ++ ++const QString& ImageCacher::cacheDir() { ++ static const QString s_dir = [] { ++ QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); ++ if (cache.isEmpty()) ++ cache = QDir::homePath() + QStringLiteral("/.cache"); ++ return cache + QStringLiteral("/caelestia/imagecache"); ++ }(); ++ return s_dir; ++} ++ ++QString ImageCacher::cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode) { ++ const QString sha = sha256sum(sourcePath); ++ if (sha.isEmpty()) ++ return {}; ++ ++ const QString filename = ++ QStringLiteral("%1@%2x%3-%4.png") ++ .arg(sha, QString::number(size.width()), QString::number(size.height()), fillSuffix(fillMode)); ++ ++ return cacheDir() + QLatin1Char('/') + filename; ++} ++ ++ImageCacher* ImageCacher::instance() { ++ static ImageCacher s_instance; ++ return &s_instance; ++} ++ ++ImageCacher::ImageCacher(QObject* parent) ++ : QObject(parent) {} ++ ++void ImageCacher::schedule(const QString& sourcePath, const QSize& size, FillMode fillMode) { ++ schedule(sourcePath, cachePathFor(sourcePath, size, fillMode), size, fillMode); ++} ++ ++void ImageCacher::schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { ++ if (cachePath.isEmpty()) ++ return; ++ ++ { ++ QMutexLocker locker(&m_mutex); ++ if (m_inflight.contains(cachePath)) ++ return; ++ m_inflight.insert(cachePath); ++ } ++ ++ QThreadPool::globalInstance()->start([this, sourcePath, cachePath, size, fillMode]() { ++ runJob(sourcePath, cachePath, size, fillMode); ++ QMutexLocker locker(&m_mutex); ++ m_inflight.remove(cachePath); ++ }); ++} ++ ++void ImageCacher::runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { ++ if (QFile::exists(cachePath)) { ++ return; ++ } ++ ++ QImage image(sourcePath); ++ if (image.isNull()) { ++ qCWarning(lcCacher).noquote() << "Failed to decode source" << sourcePath; ++ return; ++ } ++ ++ Qt::AspectRatioMode scaleMode; ++ switch (fillMode) { ++ case FillMode::Crop: ++ scaleMode = Qt::KeepAspectRatioByExpanding; ++ break; ++ case FillMode::Fit: ++ scaleMode = Qt::KeepAspectRatio; ++ break; ++ case FillMode::Stretch: ++ scaleMode = Qt::IgnoreAspectRatio; ++ break; ++ } ++ ++ image.convertTo(QImage::Format_ARGB32); ++ image = image.scaled(size, scaleMode, Qt::SmoothTransformation); ++ ++ if (image.isNull()) { ++ qCWarning(lcCacher).noquote() << "Failed to scale" << sourcePath; ++ return; ++ } ++ ++ QImage canvas; ++ if (fillMode == FillMode::Stretch) { ++ canvas = image; ++ } else { ++ canvas = QImage(size, QImage::Format_ARGB32); ++ canvas.fill(Qt::transparent); ++ ++ QPainter painter(&canvas); ++ painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); ++ painter.end(); ++ } ++ ++ const QString parent = QFileInfo(cachePath).absolutePath(); ++ if (!QDir().mkpath(parent)) { ++ qCWarning(lcCacher).noquote() << "Failed to create cache dir" << parent; ++ return; ++ } ++ ++ QSaveFile saveFile(cachePath); ++ if (!saveFile.open(QIODevice::WriteOnly) || !canvas.save(&saveFile, "PNG") || !saveFile.commit()) { ++ qCWarning( ++ lcCacher, "Failed to save to %s: %s", qUtf8Printable(cachePath), qUtf8Printable(saveFile.errorString())); ++ return; ++ } ++ ++ qCDebug(lcCacher).noquote() << "Saved to" << cachePath; ++} ++ ++} // namespace caelestia::images +diff --git a/plugin/src/Caelestia/Images/imagecacher.hpp b/plugin/src/Caelestia/Images/imagecacher.hpp +new file mode 100644 +index 00000000..79608419 +--- /dev/null ++++ b/plugin/src/Caelestia/Images/imagecacher.hpp +@@ -0,0 +1,38 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace caelestia::images { ++ ++class ImageCacher : public QObject { ++ Q_OBJECT ++ ++public: ++ enum class FillMode { ++ Crop, ++ Fit, ++ Stretch, ++ }; ++ ++ static ImageCacher* instance(); ++ ++ static const QString& cacheDir(); ++ static QString cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode); ++ ++ void schedule(const QString& sourcePath, const QSize& size, FillMode fillMode); ++ void schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); ++ ++private: ++ explicit ImageCacher(QObject* parent = nullptr); ++ ++ static void runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); ++ ++ QMutex m_mutex; ++ QSet m_inflight; ++}; ++ ++} // namespace caelestia::images +diff --git a/plugin/src/Caelestia/Images/iutils.cpp b/plugin/src/Caelestia/Images/iutils.cpp +new file mode 100644 +index 00000000..996f93dd +--- /dev/null ++++ b/plugin/src/Caelestia/Images/iutils.cpp +@@ -0,0 +1,42 @@ ++#include "iutils.hpp" ++ ++#include "cachingimageprovider.hpp" ++ ++namespace caelestia::images { ++ ++IUtils* IUtils::create(QQmlEngine* engine, QJSEngine* jsEngine) { ++ Q_UNUSED(jsEngine); ++ ++ engine->addImageProvider(QStringLiteral("ccache"), new CachingImageProvider(CachingImageProvider::FillMode::Crop)); ++ engine->addImageProvider(QStringLiteral("fcache"), new CachingImageProvider(CachingImageProvider::FillMode::Fit)); ++ engine->addImageProvider( ++ QStringLiteral("scache"), new CachingImageProvider(CachingImageProvider::FillMode::Stretch)); ++ ++ return new IUtils(engine); ++} ++ ++QUrl IUtils::urlForPath(const QString& path, int fillMode) { ++ if (path.isEmpty()) ++ return QUrl(); ++ ++ QString prefix; ++ switch (fillMode) { ++ case 1: // Image.PreserveAspectFit ++ prefix = QStringLiteral("fcache"); ++ break; ++ case 2: // Image.PreserveAspectCrop ++ prefix = QStringLiteral("ccache"); ++ break; ++ default: // Image.Stretch or any other ones ++ prefix = QStringLiteral("scache"); ++ break; ++ } ++ ++ QUrl url; ++ url.setScheme(QStringLiteral("image")); ++ url.setHost(prefix); ++ url.setPath(path.startsWith(QLatin1Char('/')) ? path : QLatin1Char('/') + path); ++ return url; ++} ++ ++} // namespace caelestia::images +diff --git a/plugin/src/Caelestia/Images/iutils.hpp b/plugin/src/Caelestia/Images/iutils.hpp +new file mode 100644 +index 00000000..2187c8d0 +--- /dev/null ++++ b/plugin/src/Caelestia/Images/iutils.hpp +@@ -0,0 +1,24 @@ ++#pragma once ++ ++#include ++#include ++#include ++ ++namespace caelestia::images { ++ ++class IUtils : public QObject { ++ Q_OBJECT ++ QML_ELEMENT ++ QML_SINGLETON ++ ++public: ++ static IUtils* create(QQmlEngine* engine, QJSEngine* jsEngine); ++ ++ Q_INVOKABLE static QUrl urlForPath(const QString& path, int fillMode); ++ ++private: ++ explicit IUtils(QObject* parent = nullptr) ++ : QObject(parent) {}; ++}; ++ ++} // namespace caelestia::images +diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt +index bdc58dbf..f4bbc5fd 100644 +--- a/plugin/src/Caelestia/Internal/CMakeLists.txt ++++ b/plugin/src/Caelestia/Internal/CMakeLists.txt +@@ -1,15 +1,17 @@ + qml_module(caelestia-internal + URI Caelestia.Internal + SOURCES +- cachingimagemanager.hpp cachingimagemanager.cpp +- circularindicatormanager.hpp circularindicatormanager.cpp +- hyprdevices.hpp hyprdevices.cpp +- hyprextras.hpp hyprextras.cpp +- logindmanager.hpp logindmanager.cpp ++ arcgauge.cpp ++ circularbuffer.cpp ++ circularindicatormanager.cpp ++ hyprdevices.cpp ++ hyprextras.cpp ++ logindmanager.cpp ++ sparklineitem.cpp ++ visualiserbars.cpp + LIBRARIES + Qt::Gui + Qt::Quick +- Qt::Concurrent + Qt::Network + Qt::DBus + ) +diff --git a/plugin/src/Caelestia/Internal/arcgauge.cpp b/plugin/src/Caelestia/Internal/arcgauge.cpp +new file mode 100644 +index 00000000..d534f5f7 +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/arcgauge.cpp +@@ -0,0 +1,119 @@ ++#include "arcgauge.hpp" ++ ++#include ++#include ++#include ++ ++namespace caelestia::internal { ++ ++ArcGauge::ArcGauge(QQuickItem* parent) ++ : QQuickPaintedItem(parent) { ++ setAntialiasing(true); ++} ++ ++void ArcGauge::paint(QPainter* painter) { ++ const qreal w = width(); ++ const qreal h = height(); ++ const qreal side = qMin(w, h); ++ const qreal radius = (side - m_lineWidth - 2.0) / 2.0; ++ const qreal cx = w / 2.0; ++ const qreal cy = h / 2.0; ++ ++ const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0); ++ ++ // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees) ++ const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0); ++ const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0); ++ ++ painter->setRenderHint(QPainter::Antialiasing, true); ++ ++ // Draw track arc ++ QPen trackPen(m_trackColor, m_lineWidth); ++ trackPen.setCapStyle(Qt::RoundCap); ++ painter->setPen(trackPen); ++ painter->setBrush(Qt::NoBrush); ++ painter->drawArc(arcRect, startAngle16, sweepAngle16); ++ ++ // Draw value arc ++ if (m_percentage > 0.0) { ++ const int valueSweep16 = qRound(static_cast(sweepAngle16) * m_percentage); ++ QPen valuePen(m_accentColor, m_lineWidth); ++ valuePen.setCapStyle(Qt::RoundCap); ++ painter->setPen(valuePen); ++ painter->drawArc(arcRect, startAngle16, valueSweep16); ++ } ++} ++ ++qreal ArcGauge::percentage() const { ++ return m_percentage; ++} ++ ++void ArcGauge::setPercentage(qreal percentage) { ++ if (qFuzzyCompare(m_percentage, percentage)) ++ return; ++ m_percentage = percentage; ++ emit percentageChanged(); ++ update(); ++} ++ ++QColor ArcGauge::accentColor() const { ++ return m_accentColor; ++} ++ ++void ArcGauge::setAccentColor(const QColor& color) { ++ if (m_accentColor == color) ++ return; ++ m_accentColor = color; ++ emit accentColorChanged(); ++ update(); ++} ++ ++QColor ArcGauge::trackColor() const { ++ return m_trackColor; ++} ++ ++void ArcGauge::setTrackColor(const QColor& color) { ++ if (m_trackColor == color) ++ return; ++ m_trackColor = color; ++ emit trackColorChanged(); ++ update(); ++} ++ ++qreal ArcGauge::startAngle() const { ++ return m_startAngle; ++} ++ ++void ArcGauge::setStartAngle(qreal angle) { ++ if (qFuzzyCompare(m_startAngle, angle)) ++ return; ++ m_startAngle = angle; ++ emit startAngleChanged(); ++ update(); ++} ++ ++qreal ArcGauge::sweepAngle() const { ++ return m_sweepAngle; ++} ++ ++void ArcGauge::setSweepAngle(qreal angle) { ++ if (qFuzzyCompare(m_sweepAngle, angle)) ++ return; ++ m_sweepAngle = angle; ++ emit sweepAngleChanged(); ++ update(); ++} ++ ++qreal ArcGauge::lineWidth() const { ++ return m_lineWidth; ++} ++ ++void ArcGauge::setLineWidth(qreal width) { ++ if (qFuzzyCompare(m_lineWidth, width)) ++ return; ++ m_lineWidth = width; ++ emit lineWidthChanged(); ++ update(); ++} ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/arcgauge.hpp b/plugin/src/Caelestia/Internal/arcgauge.hpp +new file mode 100644 +index 00000000..4ccb1fd0 +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/arcgauge.hpp +@@ -0,0 +1,61 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++ ++namespace caelestia::internal { ++ ++class ArcGauge : public QQuickPaintedItem { ++ Q_OBJECT ++ QML_ELEMENT ++ ++ Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged) ++ Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) ++ Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged) ++ Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged) ++ Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged) ++ Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) ++ ++public: ++ explicit ArcGauge(QQuickItem* parent = nullptr); ++ ++ void paint(QPainter* painter) override; ++ ++ [[nodiscard]] qreal percentage() const; ++ void setPercentage(qreal percentage); ++ ++ [[nodiscard]] QColor accentColor() const; ++ void setAccentColor(const QColor& color); ++ ++ [[nodiscard]] QColor trackColor() const; ++ void setTrackColor(const QColor& color); ++ ++ [[nodiscard]] qreal startAngle() const; ++ void setStartAngle(qreal angle); ++ ++ [[nodiscard]] qreal sweepAngle() const; ++ void setSweepAngle(qreal angle); ++ ++ [[nodiscard]] qreal lineWidth() const; ++ void setLineWidth(qreal width); ++ ++signals: ++ void percentageChanged(); ++ void accentColorChanged(); ++ void trackColorChanged(); ++ void startAngleChanged(); ++ void sweepAngleChanged(); ++ void lineWidthChanged(); ++ ++private: ++ qreal m_percentage = 0.0; ++ QColor m_accentColor; ++ QColor m_trackColor; ++ qreal m_startAngle = 0.75 * M_PI; ++ qreal m_sweepAngle = 1.5 * M_PI; ++ qreal m_lineWidth = 10.0; ++}; ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp +deleted file mode 100644 +index 1c15cd20..00000000 +--- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp ++++ /dev/null +@@ -1,223 +0,0 @@ +-#include "cachingimagemanager.hpp" +- +-#include +-#include +-#include +-#include +-#include +-#include +-#include +-#include +- +-namespace caelestia::internal { +- +-qreal CachingImageManager::effectiveScale() const { +- if (m_item && m_item->window()) { +- return m_item->window()->devicePixelRatio(); +- } +- +- return 1.0; +-} +- +-QSize CachingImageManager::effectiveSize() const { +- if (!m_item) { +- return QSize(); +- } +- +- const qreal scale = effectiveScale(); +- const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); +- m_item->setProperty("sourceSize", size); +- return size; +-} +- +-QQuickItem* CachingImageManager::item() const { +- return m_item; +-} +- +-void CachingImageManager::setItem(QQuickItem* item) { +- if (m_item == item) { +- return; +- } +- +- if (m_widthConn) { +- disconnect(m_widthConn); +- } +- if (m_heightConn) { +- disconnect(m_heightConn); +- } +- +- m_item = item; +- emit itemChanged(); +- +- if (item) { +- m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { +- updateSource(); +- }); +- m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { +- updateSource(); +- }); +- updateSource(); +- } +-} +- +-QUrl CachingImageManager::cacheDir() const { +- return m_cacheDir; +-} +- +-void CachingImageManager::setCacheDir(const QUrl& cacheDir) { +- if (m_cacheDir == cacheDir) { +- return; +- } +- +- m_cacheDir = cacheDir; +- if (!m_cacheDir.path().endsWith("/")) { +- m_cacheDir.setPath(m_cacheDir.path() + "/"); +- } +- emit cacheDirChanged(); +-} +- +-QString CachingImageManager::path() const { +- return m_path; +-} +- +-void CachingImageManager::setPath(const QString& path) { +- if (m_path == path) { +- return; +- } +- +- m_path = path; +- emit pathChanged(); +- +- if (!path.isEmpty()) { +- updateSource(path); +- } +-} +- +-void CachingImageManager::updateSource() { +- updateSource(m_path); +-} +- +-void CachingImageManager::updateSource(const QString& path) { +- if (path.isEmpty() || path == m_shaPath) { +- // Path is empty or already calculating sha for path +- return; +- } +- +- m_shaPath = path; +- +- const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); +- +- const auto watcher = new QFutureWatcher(this); +- +- connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { +- if (m_path != path) { +- // Object is destroyed or path has changed, ignore +- watcher->deleteLater(); +- return; +- } +- +- const QSize size = effectiveSize(); +- +- if (!m_item || !size.width() || !size.height()) { +- watcher->deleteLater(); +- return; +- } +- +- const QString fillMode = m_item->property("fillMode").toString(); +- // clang-format off +- const QString filename = QString("%1@%2x%3-%4.png") +- .arg(watcher->result()).arg(size.width()).arg(size.height()) +- .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); +- // clang-format on +- +- const QUrl cache = m_cacheDir.resolved(QUrl(filename)); +- if (m_cachePath == cache) { +- watcher->deleteLater(); +- return; +- } +- +- m_cachePath = cache; +- emit cachePathChanged(); +- +- if (!cache.isLocalFile()) { +- qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; +- watcher->deleteLater(); +- return; +- } +- +- const QImageReader reader(cache.toLocalFile()); +- if (reader.canRead()) { +- m_item->setProperty("source", cache); +- } else { +- m_item->setProperty("source", QUrl::fromLocalFile(path)); +- createCache(path, cache.toLocalFile(), fillMode, size); +- } +- +- // Clear current running sha if same +- if (m_shaPath == path) { +- m_shaPath = QString(); +- } +- +- watcher->deleteLater(); +- }); +- +- watcher->setFuture(future); +-} +- +-QUrl CachingImageManager::cachePath() const { +- return m_cachePath; +-} +- +-void CachingImageManager::createCache( +- const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { +- QThreadPool::globalInstance()->start([path, cache, fillMode, size] { +- QImage image(path); +- +- if (image.isNull()) { +- qWarning() << "CachingImageManager::createCache: failed to read" << path; +- return; +- } +- +- image.convertTo(QImage::Format_ARGB32); +- +- if (fillMode == "PreserveAspectCrop") { +- image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); +- } else if (fillMode == "PreserveAspectFit") { +- image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); +- } else { +- image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); +- } +- +- if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") { +- QImage canvas(size, QImage::Format_ARGB32); +- canvas.fill(Qt::transparent); +- +- QPainter painter(&canvas); +- painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); +- painter.end(); +- +- image = canvas; +- } +- +- const QString parent = QFileInfo(cache).absolutePath(); +- if (!QDir().mkpath(parent) || !image.save(cache)) { +- qWarning() << "CachingImageManager::createCache: failed to save to" << cache; +- } +- }); +-} +- +-QString CachingImageManager::sha256sum(const QString& path) { +- QFile file(path); +- if (!file.open(QIODevice::ReadOnly)) { +- qWarning() << "CachingImageManager::sha256sum: failed to open" << path; +- return ""; +- } +- +- QCryptographicHash hash(QCryptographicHash::Sha256); +- hash.addData(&file); +- file.close(); +- +- return hash.result().toHex(); +-} +- +-} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp +deleted file mode 100644 +index 3611699b..00000000 +--- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp ++++ /dev/null +@@ -1,65 +0,0 @@ +-#pragma once +- +-#include +-#include +-#include +- +-namespace caelestia::internal { +- +-class CachingImageManager : public QObject { +- Q_OBJECT +- QML_ELEMENT +- +- Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) +- Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) +- +- Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) +- Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) +- +-public: +- explicit CachingImageManager(QObject* parent = nullptr) +- : QObject(parent) +- , m_item(nullptr) {} +- +- [[nodiscard]] QQuickItem* item() const; +- void setItem(QQuickItem* item); +- +- [[nodiscard]] QUrl cacheDir() const; +- void setCacheDir(const QUrl& cacheDir); +- +- [[nodiscard]] QString path() const; +- void setPath(const QString& path); +- +- [[nodiscard]] QUrl cachePath() const; +- +- Q_INVOKABLE void updateSource(); +- Q_INVOKABLE void updateSource(const QString& path); +- +-signals: +- void itemChanged(); +- void cacheDirChanged(); +- +- void pathChanged(); +- void cachePathChanged(); +- void usingCacheChanged(); +- +-private: +- QString m_shaPath; +- +- QQuickItem* m_item; +- QUrl m_cacheDir; +- +- QString m_path; +- QUrl m_cachePath; +- +- QMetaObject::Connection m_widthConn; +- QMetaObject::Connection m_heightConn; +- +- [[nodiscard]] qreal effectiveScale() const; +- [[nodiscard]] QSize effectiveSize() const; +- +- void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; +- [[nodiscard]] static QString sha256sum(const QString& path); +-}; +- +-} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/circularbuffer.cpp b/plugin/src/Caelestia/Internal/circularbuffer.cpp +new file mode 100644 +index 00000000..9701e7fa +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/circularbuffer.cpp +@@ -0,0 +1,94 @@ ++#include "circularbuffer.hpp" ++ ++#include ++ ++namespace caelestia::internal { ++ ++CircularBuffer::CircularBuffer(QObject* parent) ++ : QObject(parent) {} ++ ++int CircularBuffer::capacity() const { ++ return m_capacity; ++} ++ ++void CircularBuffer::setCapacity(int capacity) { ++ if (capacity < 0) ++ capacity = 0; ++ if (m_capacity == capacity) ++ return; ++ ++ const auto old = values(); ++ ++ m_capacity = capacity; ++ m_data.resize(capacity); ++ m_data.fill(0.0); ++ m_head = 0; ++ m_count = 0; ++ ++ // Re-push old values, keeping the most recent ones ++ const auto start = old.size() > capacity ? old.size() - capacity : 0; ++ for (auto i = start; i < old.size(); ++i) { ++ m_data[m_head] = old[i]; ++ m_head = (m_head + 1) % m_capacity; ++ m_count++; ++ } ++ ++ emit capacityChanged(); ++ emit countChanged(); ++ emit valuesChanged(); ++} ++ ++int CircularBuffer::count() const { ++ return m_count; ++} ++ ++QList CircularBuffer::values() const { ++ QList result; ++ result.reserve(m_count); ++ for (int i = 0; i < m_count; ++i) ++ result.append(at(i)); ++ return result; ++} ++ ++qreal CircularBuffer::maximum() const { ++ if (m_count == 0) ++ return 0.0; ++ ++ qreal maxVal = at(0); ++ for (int i = 1; i < m_count; ++i) ++ maxVal = std::max(maxVal, at(i)); ++ return maxVal; ++} ++ ++void CircularBuffer::push(qreal value) { ++ if (m_capacity <= 0) ++ return; ++ ++ m_data[m_head] = value; ++ m_head = (m_head + 1) % m_capacity; ++ if (m_count < m_capacity) { ++ m_count++; ++ emit countChanged(); ++ } ++ emit valuesChanged(); ++} ++ ++void CircularBuffer::clear() { ++ if (m_count == 0) ++ return; ++ ++ m_head = 0; ++ m_count = 0; ++ emit countChanged(); ++ emit valuesChanged(); ++} ++ ++qreal CircularBuffer::at(int index) const { ++ if (index < 0 || index >= m_count) ++ return 0.0; ++ ++ const int actualIndex = (m_head - m_count + index + m_capacity) % m_capacity; ++ return m_data[actualIndex]; ++} ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/circularbuffer.hpp b/plugin/src/Caelestia/Internal/circularbuffer.hpp +new file mode 100644 +index 00000000..ab2dba56 +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/circularbuffer.hpp +@@ -0,0 +1,44 @@ ++#pragma once ++ ++#include ++#include ++#include ++ ++namespace caelestia::internal { ++ ++class CircularBuffer : public QObject { ++ Q_OBJECT ++ QML_ELEMENT ++ ++ Q_PROPERTY(int capacity READ capacity WRITE setCapacity NOTIFY capacityChanged) ++ Q_PROPERTY(int count READ count NOTIFY countChanged) ++ Q_PROPERTY(QList values READ values NOTIFY valuesChanged) ++ Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged) ++ ++public: ++ explicit CircularBuffer(QObject* parent = nullptr); ++ ++ [[nodiscard]] int capacity() const; ++ void setCapacity(int capacity); ++ ++ [[nodiscard]] int count() const; ++ [[nodiscard]] QList values() const; ++ [[nodiscard]] qreal maximum() const; ++ ++ Q_INVOKABLE void push(qreal value); ++ Q_INVOKABLE void clear(); ++ Q_INVOKABLE [[nodiscard]] qreal at(int index) const; ++ ++signals: ++ void capacityChanged(); ++ void countChanged(); ++ void valuesChanged(); ++ ++private: ++ QVector m_data; ++ int m_head = 0; ++ int m_count = 0; ++ int m_capacity = 0; ++}; ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp +index 434b7562..21249979 100644 +--- a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp ++++ b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp +@@ -153,8 +153,10 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { + spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) * + SPIN_ROTATION_DEGREES; + } ++ const auto oldRotation = m_rotation; + m_rotation = constantRotation + spinRotation; +- emit rotationChanged(); ++ if (!qFuzzyCompare(m_rotation + 1.0, oldRotation + 1.0)) ++ emit rotationChanged(); + + // Grow active indicator. + qreal fraction = +@@ -182,6 +184,8 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { + void CircularIndicatorManager::updateAdvance(qreal progress) { + using namespace advance; + const auto playtime = progress * TOTAL_DURATION_IN_MS; ++ const auto oldStart = m_startFraction; ++ const auto oldEnd = m_endFraction; + + // Adds constant rotation to segment positions. + m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET; +@@ -204,8 +208,10 @@ void CircularIndicatorManager::updateAdvance(qreal progress) { + m_startFraction /= 360.0; + m_endFraction /= 360.0; + +- emit startFractionChanged(); +- emit endFractionChanged(); ++ if (!qFuzzyCompare(m_startFraction + 1.0, oldStart + 1.0)) ++ emit startFractionChanged(); ++ if (!qFuzzyCompare(m_endFraction + 1.0, oldEnd + 1.0)) ++ emit endFractionChanged(); + } + + } // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/hyprextras.cpp b/plugin/src/Caelestia/Internal/hyprextras.cpp +index 5308524d..c73b6e0b 100644 +--- a/plugin/src/Caelestia/Internal/hyprextras.cpp ++++ b/plugin/src/Caelestia/Internal/hyprextras.cpp +@@ -1,10 +1,14 @@ + #include "hyprextras.hpp" ++#include "hyprdevices.hpp" + + #include + #include + #include ++#include + #include + ++Q_LOGGING_CATEGORY(lcHypr, "caelestia.internal.hypr", QtInfoMsg) ++ + namespace caelestia::internal::hypr { + + HyprExtras::HyprExtras(QObject* parent) +@@ -16,8 +20,7 @@ HyprExtras::HyprExtras(QObject* parent) + , m_devices(new HyprDevices(this)) { + const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); + if (his.isEmpty()) { +- qWarning() +- << "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; ++ qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; + return; + } + +@@ -26,8 +29,7 @@ HyprExtras::HyprExtras(QObject* parent) + hyprDir = "/tmp/hypr/" + his; + + if (!QDir(hyprDir).exists()) { +- qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " +- "Hyprland socket."; ++ qCWarning(lcHypr) << "Hyprland socket directory does not exist. Unable to connect to Hyprland socket."; + return; + } + } +@@ -62,7 +64,7 @@ void HyprExtras::message(const QString& message) { + + makeRequest(message, [](bool success, const QByteArray& res) { + if (!success) { +- qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); ++ qCWarning(lcHypr) << "message: request error:" << QString::fromUtf8(res); + } + }); + } +@@ -74,7 +76,7 @@ void HyprExtras::batchMessage(const QStringList& messages) { + + makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { + if (!success) { +- qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); ++ qCWarning(lcHypr) << "batchMessage: request error:" << QString::fromUtf8(res); + } + }); + } +@@ -84,16 +86,18 @@ void HyprExtras::applyOptions(const QVariantHash& options) { + return; + } + +- QString request = "[[BATCH]]"; ++ QString request; ++ request.reserve(12 + options.size() * 40); ++ request += QLatin1String("[[BATCH]]"); + for (auto it = options.constBegin(); it != options.constEnd(); ++it) { +- request += QString("keyword %1 %2;").arg(it.key(), it.value().toString()); ++ request += QLatin1String("keyword ") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';'); + } + + makeRequest(request, [this](bool success, const QByteArray& res) { + if (success) { + refreshOptions(); + } else { +- qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); ++ qCWarning(lcHypr) << "applyOptions: request error" << QString::fromUtf8(res); + } + }); + } +@@ -143,15 +147,15 @@ void HyprExtras::refreshDevices() { + + void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { + if (!m_socketValid) { +- qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; ++ qCWarning(lcHypr) << "socketError: unable to connect to Hyprland event socket:" << error; + } else { +- qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; ++ qCWarning(lcHypr) << "socketError: Hyprland event socket error:" << error; + } + } + + void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { + if (state == QLocalSocket::UnconnectedState && m_socketValid) { +- qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; ++ qCWarning(lcHypr) << "socketStateChanged: Hyprland event socket disconnected."; + } + + m_socketValid = state == QLocalSocket::ConnectedState; +@@ -204,7 +208,7 @@ HyprExtras::SocketPtr HyprExtras::makeRequest( + }); + + QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { +- qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; ++ qCWarning(lcHypr) << "makeRequest: error making request:" << err << "| request:" << request; + callback(false, {}); + socket->close(); + }); +diff --git a/plugin/src/Caelestia/Internal/hyprextras.hpp b/plugin/src/Caelestia/Internal/hyprextras.hpp +index 14563c0a..48eceea9 100644 +--- a/plugin/src/Caelestia/Internal/hyprextras.hpp ++++ b/plugin/src/Caelestia/Internal/hyprextras.hpp +@@ -1,15 +1,19 @@ + #pragma once + +-#include "hyprdevices.hpp" + #include + #include + #include ++#include ++#include + + namespace caelestia::internal::hypr { + ++class HyprDevices; ++ + class HyprExtras : public QObject { + Q_OBJECT + QML_ELEMENT ++ Q_MOC_INCLUDE("hyprdevices.hpp") + + Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) + Q_PROPERTY(caelestia::internal::hypr::HyprDevices* devices READ devices CONSTANT) +diff --git a/plugin/src/Caelestia/Internal/logindmanager.cpp b/plugin/src/Caelestia/Internal/logindmanager.cpp +index 4194ee1e..740005d9 100644 +--- a/plugin/src/Caelestia/Internal/logindmanager.cpp ++++ b/plugin/src/Caelestia/Internal/logindmanager.cpp +@@ -4,6 +4,9 @@ + #include + #include + #include ++#include ++ ++Q_LOGGING_CATEGORY(lcLogindManager, "caelestia.internal.logindmanager", QtInfoMsg) + + namespace caelestia::internal { + +@@ -11,7 +14,7 @@ LogindManager::LogindManager(QObject* parent) + : QObject(parent) { + auto bus = QDBusConnection::systemBus(); + if (!bus.isConnected()) { +- qWarning() << "LogindManager::LogindManager: failed to connect to system bus:" << bus.lastError().message(); ++ qCWarning(lcLogindManager) << "Failed to connect to system bus:" << bus.lastError().message(); + return; + } + +@@ -19,14 +22,13 @@ LogindManager::LogindManager(QObject* parent) + "PrepareForSleep", this, SLOT(handlePrepareForSleep(bool))); + + if (!ok) { +- qWarning() << "LogindManager::LogindManager: failed to connect to PrepareForSleep signal:" +- << bus.lastError().message(); ++ qCWarning(lcLogindManager) << "Failed to connect to PrepareForSleep signal:" << bus.lastError().message(); + } + + QDBusInterface login1("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", bus); + const QDBusReply reply = login1.call("GetSession", "auto"); + if (!reply.isValid()) { +- qWarning() << "LogindManager::LogindManager: failed to get session path"; ++ qCWarning(lcLogindManager) << "Failed to get session path"; + return; + } + const auto sessionPath = reply.value().path(); +@@ -35,14 +37,14 @@ LogindManager::LogindManager(QObject* parent) + SLOT(handleLockRequested())); + + if (!ok) { +- qWarning() << "LogindManager::LogindManager: failed to connect to Lock signal:" << bus.lastError().message(); ++ qCWarning(lcLogindManager) << "Failed to connect to Lock signal:" << bus.lastError().message(); + } + + ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Unlock", this, + SLOT(handleUnlockRequested())); + + if (!ok) { +- qWarning() << "LogindManager::LogindManager: failed to connect to Unlock signal:" << bus.lastError().message(); ++ qCWarning(lcLogindManager) << "Failed to connect to Unlock signal:" << bus.lastError().message(); + } + } + +diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp +new file mode 100644 +index 00000000..4e6b0719 +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp +@@ -0,0 +1,216 @@ ++#include "sparklineitem.hpp" ++#include "circularbuffer.hpp" ++ ++#include ++#include ++#include ++ ++namespace caelestia::internal { ++ ++SparklineItem::SparklineItem(QQuickItem* parent) ++ : QQuickPaintedItem(parent) { ++ setAntialiasing(true); ++} ++ ++void SparklineItem::paint(QPainter* painter) { ++ const bool has1 = m_line1 && m_line1->count() >= 2; ++ const bool has2 = m_line2 && m_line2->count() >= 2; ++ if (!has1 && !has2) ++ return; ++ ++ painter->setRenderHint(QPainter::Antialiasing, true); ++ ++ // Draw line1 first (behind), then line2 (in front) ++ if (has1) ++ drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha); ++ if (has2) ++ drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha); ++} ++ ++void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { ++ if (m_historyLength < 2) ++ return; ++ ++ const qreal w = width(); ++ const qreal h = height(); ++ const int len = buffer->count(); ++ const qreal stepX = w / static_cast(m_historyLength - 1); ++ const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX; ++ ++ // Build line path ++ QPainterPath linePath; ++ linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h); ++ for (int i = 1; i < len; ++i) { ++ const qreal x = startX + i * stepX; ++ const qreal y = h - (buffer->at(i) / m_maxValue) * h; ++ linePath.lineTo(x, y); ++ } ++ ++ // Stroke the line ++ QPen pen(color, m_lineWidth); ++ pen.setCapStyle(Qt::RoundCap); ++ pen.setJoinStyle(Qt::RoundJoin); ++ painter->setPen(pen); ++ painter->setBrush(Qt::NoBrush); ++ painter->drawPath(linePath); ++ ++ // Fill under the line ++ QPainterPath fillPath = linePath; ++ fillPath.lineTo(startX + (len - 1) * stepX, h); ++ fillPath.lineTo(startX, h); ++ fillPath.closeSubpath(); ++ ++ QColor fillColor = color; ++ fillColor.setAlphaF(static_cast(fillAlpha)); ++ painter->setPen(Qt::NoPen); ++ painter->setBrush(fillColor); ++ painter->drawPath(fillPath); ++} ++ ++void SparklineItem::connectBuffer(CircularBuffer* buffer) { ++ if (!buffer) ++ return; ++ ++ connect(buffer, &CircularBuffer::valuesChanged, this, [this]() { ++ update(); ++ }); ++ connect(buffer, &QObject::destroyed, this, [this, buffer]() { ++ if (m_line1 == buffer) { ++ m_line1 = nullptr; ++ emit line1Changed(); ++ } ++ if (m_line2 == buffer) { ++ m_line2 = nullptr; ++ emit line2Changed(); ++ } ++ update(); ++ }); ++} ++ ++CircularBuffer* SparklineItem::line1() const { ++ return m_line1; ++} ++ ++void SparklineItem::setLine1(CircularBuffer* buffer) { ++ if (m_line1 == buffer) ++ return; ++ if (m_line1) ++ disconnect(m_line1, nullptr, this, nullptr); ++ m_line1 = buffer; ++ connectBuffer(buffer); ++ emit line1Changed(); ++ update(); ++} ++ ++CircularBuffer* SparklineItem::line2() const { ++ return m_line2; ++} ++ ++void SparklineItem::setLine2(CircularBuffer* buffer) { ++ if (m_line2 == buffer) ++ return; ++ if (m_line2) ++ disconnect(m_line2, nullptr, this, nullptr); ++ m_line2 = buffer; ++ connectBuffer(buffer); ++ emit line2Changed(); ++ update(); ++} ++ ++QColor SparklineItem::line1Color() const { ++ return m_line1Color; ++} ++ ++void SparklineItem::setLine1Color(const QColor& color) { ++ if (m_line1Color == color) ++ return; ++ m_line1Color = color; ++ emit line1ColorChanged(); ++ update(); ++} ++ ++QColor SparklineItem::line2Color() const { ++ return m_line2Color; ++} ++ ++void SparklineItem::setLine2Color(const QColor& color) { ++ if (m_line2Color == color) ++ return; ++ m_line2Color = color; ++ emit line2ColorChanged(); ++ update(); ++} ++ ++qreal SparklineItem::line1FillAlpha() const { ++ return m_line1FillAlpha; ++} ++ ++void SparklineItem::setLine1FillAlpha(qreal alpha) { ++ if (qFuzzyCompare(m_line1FillAlpha, alpha)) ++ return; ++ m_line1FillAlpha = alpha; ++ emit line1FillAlphaChanged(); ++ update(); ++} ++ ++qreal SparklineItem::line2FillAlpha() const { ++ return m_line2FillAlpha; ++} ++ ++void SparklineItem::setLine2FillAlpha(qreal alpha) { ++ if (qFuzzyCompare(m_line2FillAlpha, alpha)) ++ return; ++ m_line2FillAlpha = alpha; ++ emit line2FillAlphaChanged(); ++ update(); ++} ++ ++qreal SparklineItem::maxValue() const { ++ return m_maxValue; ++} ++ ++void SparklineItem::setMaxValue(qreal value) { ++ if (qFuzzyCompare(m_maxValue, value)) ++ return; ++ m_maxValue = value; ++ emit maxValueChanged(); ++ update(); ++} ++ ++qreal SparklineItem::slideProgress() const { ++ return m_slideProgress; ++} ++ ++void SparklineItem::setSlideProgress(qreal progress) { ++ if (qFuzzyCompare(m_slideProgress, progress)) ++ return; ++ m_slideProgress = progress; ++ emit slideProgressChanged(); ++ update(); ++} ++ ++int SparklineItem::historyLength() const { ++ return m_historyLength; ++} ++ ++void SparklineItem::setHistoryLength(int length) { ++ if (m_historyLength == length) ++ return; ++ m_historyLength = length; ++ emit historyLengthChanged(); ++ update(); ++} ++ ++qreal SparklineItem::lineWidth() const { ++ return m_lineWidth; ++} ++ ++void SparklineItem::setLineWidth(qreal width) { ++ if (qFuzzyCompare(m_lineWidth, width)) ++ return; ++ m_lineWidth = width; ++ emit lineWidthChanged(); ++ update(); ++} ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/sparklineitem.hpp b/plugin/src/Caelestia/Internal/sparklineitem.hpp +new file mode 100644 +index 00000000..22632a90 +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/sparklineitem.hpp +@@ -0,0 +1,91 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++ ++namespace caelestia::internal { ++ ++class CircularBuffer; ++ ++class SparklineItem : public QQuickPaintedItem { ++ Q_OBJECT ++ QML_ELEMENT ++ Q_MOC_INCLUDE("circularbuffer.hpp") ++ ++ Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed) ++ Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed) ++ Q_PROPERTY(QColor line1Color READ line1Color WRITE setLine1Color NOTIFY line1ColorChanged) ++ Q_PROPERTY(QColor line2Color READ line2Color WRITE setLine2Color NOTIFY line2ColorChanged) ++ Q_PROPERTY(qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY line1FillAlphaChanged) ++ Q_PROPERTY(qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY line2FillAlphaChanged) ++ Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) ++ Q_PROPERTY(qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY slideProgressChanged) ++ Q_PROPERTY(int historyLength READ historyLength WRITE setHistoryLength NOTIFY historyLengthChanged) ++ Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) ++ ++public: ++ explicit SparklineItem(QQuickItem* parent = nullptr); ++ ++ void paint(QPainter* painter) override; ++ ++ [[nodiscard]] CircularBuffer* line1() const; ++ void setLine1(CircularBuffer* buffer); ++ ++ [[nodiscard]] CircularBuffer* line2() const; ++ void setLine2(CircularBuffer* buffer); ++ ++ [[nodiscard]] QColor line1Color() const; ++ void setLine1Color(const QColor& color); ++ ++ [[nodiscard]] QColor line2Color() const; ++ void setLine2Color(const QColor& color); ++ ++ [[nodiscard]] qreal line1FillAlpha() const; ++ void setLine1FillAlpha(qreal alpha); ++ ++ [[nodiscard]] qreal line2FillAlpha() const; ++ void setLine2FillAlpha(qreal alpha); ++ ++ [[nodiscard]] qreal maxValue() const; ++ void setMaxValue(qreal value); ++ ++ [[nodiscard]] qreal slideProgress() const; ++ void setSlideProgress(qreal progress); ++ ++ [[nodiscard]] int historyLength() const; ++ void setHistoryLength(int length); ++ ++ [[nodiscard]] qreal lineWidth() const; ++ void setLineWidth(qreal width); ++ ++signals: ++ void line1Changed(); ++ void line2Changed(); ++ void line1ColorChanged(); ++ void line2ColorChanged(); ++ void line1FillAlphaChanged(); ++ void line2FillAlphaChanged(); ++ void maxValueChanged(); ++ void slideProgressChanged(); ++ void historyLengthChanged(); ++ void lineWidthChanged(); ++ ++private: ++ void drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha); ++ void connectBuffer(CircularBuffer* buffer); ++ ++ CircularBuffer* m_line1 = nullptr; ++ CircularBuffer* m_line2 = nullptr; ++ QColor m_line1Color; ++ QColor m_line2Color; ++ qreal m_line1FillAlpha = 0.15; ++ qreal m_line2FillAlpha = 0.2; ++ qreal m_maxValue = 1024.0; ++ qreal m_slideProgress = 0.0; ++ int m_historyLength = 30; ++ qreal m_lineWidth = 2.0; ++}; ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/visualiserbars.cpp b/plugin/src/Caelestia/Internal/visualiserbars.cpp +new file mode 100644 +index 00000000..926468b0 +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/visualiserbars.cpp +@@ -0,0 +1,198 @@ ++#include "visualiserbars.hpp" ++ ++#include ++#include ++#include ++#include ++#include ++#include ++ ++namespace caelestia::internal { ++ ++VisualiserBars::VisualiserBars(QQuickItem* parent) ++ : QQuickPaintedItem(parent) { ++ setAntialiasing(true); ++} ++ ++void VisualiserBars::advance(qreal dt) { ++ if (m_displayValues.isEmpty() || m_settled) ++ return; ++ ++ // dt is in seconds (from FrameAnimation.frameTime), convert to ms ++ const qreal dtMs = dt * 1000.0; ++ const qreal tau = m_animationDuration / 3.0; ++ const qreal alpha = 1.0 - std::exp(-dtMs / tau); ++ ++ bool allSettled = true; ++ ++ for (qsizetype i = 0; i < m_displayValues.size(); ++i) { ++ const double diff = m_targetValues[i] - m_displayValues[i]; ++ ++ if (std::abs(diff) > 0.001) { ++ m_displayValues[i] += diff * alpha; ++ allSettled = false; ++ } else { ++ m_displayValues[i] = m_targetValues[i]; ++ } ++ } ++ ++ update(); ++ ++ if (allSettled && !m_settled) { ++ m_settled = true; ++ emit settledChanged(); ++ } ++} ++ ++void VisualiserBars::paint(QPainter* painter) { ++ if (m_displayValues.isEmpty()) ++ return; ++ ++ painter->setRenderHint(QPainter::Antialiasing, true); ++ painter->setPen(Qt::NoPen); ++ ++ const qreal h = height(); ++ const qreal maxBarHeight = h * 0.4; ++ ++ QLinearGradient gradient(0, h - maxBarHeight, 0, h); ++ gradient.setColorAt(0, m_primaryColor); ++ gradient.setColorAt(1, m_secondaryColor); ++ painter->setBrush(gradient); ++ ++ drawSide(painter, false); ++ drawSide(painter, true); ++} ++ ++void VisualiserBars::drawSide(QPainter* painter, bool rightSide) { ++ const qreal w = width(); ++ const qreal h = height(); ++ const auto count = m_displayValues.size(); ++ ++ if (count == 0) ++ return; ++ ++ const qreal sideWidth = w * 0.4; ++ const qreal slotWidth = sideWidth / static_cast(count); ++ const qreal barWidth = slotWidth - m_spacing; ++ ++ if (barWidth <= 0) ++ return; ++ ++ const qreal sideOffset = rightSide ? w * 0.6 : 0; ++ const qreal maxBarHeight = h * 0.4; ++ ++ for (qsizetype i = 0; i < count; ++i) { ++ const qsizetype valueIndex = rightSide ? i : (count - i - 1); ++ const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0); ++ const qreal barHeight = value * maxBarHeight; ++ ++ if (barHeight <= 0) ++ continue; ++ ++ const qreal x = static_cast(i) * slotWidth + sideOffset; ++ const qreal y = h - barHeight; ++ const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight }); ++ ++ QPainterPath path; ++ path.moveTo(x, h); ++ path.lineTo(x, y + r); ++ ++ if (r > 0) { ++ path.arcTo(x, y, r * 2, r * 2, 180, -90); ++ path.lineTo(x + barWidth - r, y); ++ path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90); ++ } else { ++ path.lineTo(x, y); ++ path.lineTo(x + barWidth, y); ++ } ++ ++ path.lineTo(x + barWidth, h); ++ path.closeSubpath(); ++ ++ painter->drawPath(path); ++ } ++} ++ ++QVector VisualiserBars::values() const { ++ return m_targetValues; ++} ++ ++void VisualiserBars::setValues(const QVector& values) { ++ m_targetValues = values; ++ ++ if (m_displayValues.size() != values.size()) { ++ m_displayValues.resize(values.size(), 0.0); ++ } ++ ++ if (m_settled) { ++ m_settled = false; ++ emit settledChanged(); ++ } ++ ++ emit valuesChanged(); ++} ++ ++bool VisualiserBars::settled() const { ++ return m_settled; ++} ++ ++QColor VisualiserBars::primaryColor() const { ++ return m_primaryColor; ++} ++ ++void VisualiserBars::setPrimaryColor(const QColor& color) { ++ if (m_primaryColor == color) ++ return; ++ m_primaryColor = color; ++ emit primaryColorChanged(); ++ update(); ++} ++ ++QColor VisualiserBars::secondaryColor() const { ++ return m_secondaryColor; ++} ++ ++void VisualiserBars::setSecondaryColor(const QColor& color) { ++ if (m_secondaryColor == color) ++ return; ++ m_secondaryColor = color; ++ emit secondaryColorChanged(); ++ update(); ++} ++ ++qreal VisualiserBars::rounding() const { ++ return m_rounding; ++} ++ ++void VisualiserBars::setRounding(qreal rounding) { ++ if (qFuzzyCompare(m_rounding, rounding)) ++ return; ++ m_rounding = rounding; ++ emit roundingChanged(); ++ update(); ++} ++ ++qreal VisualiserBars::spacing() const { ++ return m_spacing; ++} ++ ++void VisualiserBars::setSpacing(qreal spacing) { ++ if (qFuzzyCompare(m_spacing, spacing)) ++ return; ++ m_spacing = spacing; ++ emit spacingChanged(); ++ update(); ++} ++ ++int VisualiserBars::animationDuration() const { ++ return m_animationDuration; ++} ++ ++void VisualiserBars::setAnimationDuration(int duration) { ++ if (m_animationDuration == duration) ++ return; ++ m_animationDuration = duration; ++ emit animationDurationChanged(); ++} ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Internal/visualiserbars.hpp b/plugin/src/Caelestia/Internal/visualiserbars.hpp +new file mode 100644 +index 00000000..95c07124 +--- /dev/null ++++ b/plugin/src/Caelestia/Internal/visualiserbars.hpp +@@ -0,0 +1,72 @@ ++#pragma once ++ ++#include ++#include ++#include ++#include ++#include ++ ++namespace caelestia::internal { ++ ++class VisualiserBars : public QQuickPaintedItem { ++ Q_OBJECT ++ QML_ELEMENT ++ ++ Q_PROPERTY(QVector values READ values WRITE setValues NOTIFY valuesChanged) ++ Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged) ++ Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged) ++ Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged) ++ Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) ++ Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged) ++ Q_PROPERTY(bool settled READ settled NOTIFY settledChanged) ++ ++public: ++ explicit VisualiserBars(QQuickItem* parent = nullptr); ++ ++ void paint(QPainter* painter) override; ++ ++ Q_INVOKABLE void advance(qreal dt); ++ ++ [[nodiscard]] QVector values() const; ++ void setValues(const QVector& values); ++ ++ [[nodiscard]] QColor primaryColor() const; ++ void setPrimaryColor(const QColor& color); ++ ++ [[nodiscard]] QColor secondaryColor() const; ++ void setSecondaryColor(const QColor& color); ++ ++ [[nodiscard]] qreal rounding() const; ++ void setRounding(qreal rounding); ++ ++ [[nodiscard]] qreal spacing() const; ++ void setSpacing(qreal spacing); ++ ++ [[nodiscard]] int animationDuration() const; ++ void setAnimationDuration(int duration); ++ ++ [[nodiscard]] bool settled() const; ++ ++signals: ++ void valuesChanged(); ++ void primaryColorChanged(); ++ void secondaryColorChanged(); ++ void roundingChanged(); ++ void spacingChanged(); ++ void animationDurationChanged(); ++ void settledChanged(); ++ ++private: ++ void drawSide(QPainter* painter, bool rightSide); ++ ++ QVector m_targetValues; ++ QVector m_displayValues; ++ QColor m_primaryColor; ++ QColor m_secondaryColor; ++ qreal m_rounding = 0.0; ++ qreal m_spacing = 0.0; ++ int m_animationDuration = 200; ++ bool m_settled = true; ++}; ++ ++} // namespace caelestia::internal +diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp +index e387ecd0..267a4394 100644 +--- a/plugin/src/Caelestia/Models/filesystemmodel.cpp ++++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp +@@ -57,8 +57,8 @@ bool FileSystemEntry::isImage() const { + + QString FileSystemEntry::mimeType() const { + if (!m_mimeTypeInitialised) { +- const QMimeDatabase db; +- m_mimeType = db.mimeTypeForFile(m_path).name(); ++ static const QMimeDatabase s_db; ++ m_mimeType = s_db.mimeTypeForFile(m_path).name(); + m_mimeTypeInitialised = true; + } + return m_mimeType; +@@ -219,7 +219,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { + if (m_recursive && m_watchChanges) { + const auto currentDir = m_dir; + const bool showHidden = m_showHidden; +- const auto future = QtConcurrent::run([showHidden, path]() { ++ auto future = QtConcurrent::run([showHidden, path]() { + QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; + if (showHidden) { + filters |= QDir::Hidden; +@@ -232,16 +232,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { + } + return dirs; + }); +- const auto watcher = new QFutureWatcher(this); +- connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { +- const auto paths = watcher->result(); ++ future.then(this, [currentDir, showHidden, this](const QStringList& paths) { + if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { + // Ignore if dir or showHidden has changed + m_watcher.addPaths(paths); + } +- watcher->deleteLater(); + }); +- watcher->setFuture(future); + } + } + +@@ -295,7 +291,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { + oldPaths << entry->path(); + } + +- const auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { ++ auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { + const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; + + std::optional iter; +@@ -353,7 +349,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { + newPaths.insert(path); + } + +- if (promise.isCanceled() || newPaths == oldPaths) { ++ if (promise.isCanceled()) { + return; + } + +@@ -365,23 +361,17 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { + } + m_futures.insert(dir, future); + +- const auto watcher = new QFutureWatcher, QSet>>(this); +- +- connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { +- m_futures.remove(dir); +- +- if (!watcher->future().isResultReadyAt(0)) { +- watcher->deleteLater(); +- return; +- } +- +- const auto result = watcher->result(); +- applyChanges(result.first, result.second); +- +- watcher->deleteLater(); +- }); +- +- watcher->setFuture(future); ++ future ++ .then(this, ++ [dir, this](QPair, QSet> result) { ++ m_futures.remove(dir); ++ if (!result.first.isEmpty() || !result.second.isEmpty()) { ++ applyChanges(result.first, result.second); ++ } ++ }) ++ .onCanceled(this, [dir, this]() { ++ m_futures.remove(dir); ++ }); + } + + void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { +diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp +index cf8eae82..c3315858 100644 +--- a/plugin/src/Caelestia/Models/filesystemmodel.hpp ++++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp +@@ -132,7 +132,7 @@ private: + bool m_recursive; + bool m_watchChanges; + bool m_showHidden; +- bool m_sortReverse; ++ bool m_sortReverse = false; + Filter m_filter; + QStringList m_nameFilters; + +diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp +index 15634059..69309f75 100644 +--- a/plugin/src/Caelestia/Services/audiocollector.cpp ++++ b/plugin/src/Caelestia/Services/audiocollector.cpp +@@ -3,13 +3,16 @@ + #include "service.hpp" + #include + #include +-#include ++#include + #include + #include + #include + #include + #include + ++Q_LOGGING_CATEGORY(lcAc, "caelestia.services.ac", QtInfoMsg) ++Q_LOGGING_CATEGORY(lcAcWorker, "caelestia.services.ac.worker", QtInfoMsg) ++ + namespace caelestia::services { + + PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) +@@ -23,13 +26,19 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) + + m_loop = pw_main_loop_new(nullptr); + if (!m_loop) { +- qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop"; ++ qCWarning(lcAcWorker) << "init: failed to create PipeWire main loop"; + pw_deinit(); + return; + } + + timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC }; + m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this); ++ if (!m_timer) { ++ qCWarning(lcAcWorker) << "init: failed to create timer"; ++ pw_main_loop_destroy(m_loop); ++ pw_deinit(); ++ return; ++ } + pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false); + + auto props = pw_properties_new( +@@ -55,6 +64,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) + params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); + + pw_stream_events events{}; ++ events.version = PW_VERSION_STREAM_EVENTS; + events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) { + auto* self = static_cast(data); + self->streamStateChanged(state); +@@ -65,13 +75,19 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) + }; + + m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this); ++ if (!m_stream) { ++ qCWarning(lcAcWorker) << "init: failed to create stream"; ++ pw_main_loop_destroy(m_loop); ++ pw_deinit(); ++ return; ++ } + + const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY, + static_cast( + PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), + params, 1); + if (success < 0) { +- qWarning() << "PipeWireWorker::init: failed to connect stream"; ++ qCWarning(lcAcWorker) << "init: failed to connect stream"; + pw_stream_destroy(m_stream); + pw_main_loop_destroy(m_loop); + pw_deinit(); +@@ -221,7 +237,7 @@ AudioCollector::AudioCollector(QObject* parent) + , m_writeBuffer(&m_buffer2) {} + + AudioCollector::~AudioCollector() { +- stop(); ++ AudioCollector::stop(); + } + + void AudioCollector::start() { +diff --git a/plugin/src/Caelestia/Services/audioprovider.cpp b/plugin/src/Caelestia/Services/audioprovider.cpp +index 1fac9eea..d2916f45 100644 +--- a/plugin/src/Caelestia/Services/audioprovider.cpp ++++ b/plugin/src/Caelestia/Services/audioprovider.cpp +@@ -2,9 +2,12 @@ + + #include "audiocollector.hpp" + #include "service.hpp" +-#include ++#include + #include + ++Q_LOGGING_CATEGORY(lcAp, "caelestia.services.ap", QtInfoMsg) ++Q_LOGGING_CATEGORY(lcApProcessor, "caelestia.services.ap.processor", QtInfoMsg) ++ + namespace caelestia::services { + + AudioProcessor::AudioProcessor(QObject* parent) +@@ -48,7 +51,7 @@ AudioProvider::~AudioProvider() { + + void AudioProvider::init() { + if (!m_processor) { +- qWarning() << "AudioProvider::init: attempted to init with no processor set"; ++ qCWarning(lcAp) << "init: attempted to init with no processor set"; + return; + } + +diff --git a/plugin/src/Caelestia/Services/audioprovider.hpp b/plugin/src/Caelestia/Services/audioprovider.hpp +index 5bf9bb00..4b85929f 100644 +--- a/plugin/src/Caelestia/Services/audioprovider.hpp ++++ b/plugin/src/Caelestia/Services/audioprovider.hpp +@@ -23,7 +23,7 @@ protected: + virtual void process() = 0; + + private: +- QTimer* m_timer; ++ QTimer* m_timer = nullptr; + }; + + class AudioProvider : public Service { +diff --git a/plugin/src/Caelestia/Services/beattracker.cpp b/plugin/src/Caelestia/Services/beattracker.cpp +index 93addc67..64970579 100644 +--- a/plugin/src/Caelestia/Services/beattracker.cpp ++++ b/plugin/src/Caelestia/Services/beattracker.cpp +@@ -19,7 +19,9 @@ BeatProcessor::~BeatProcessor() { + if (m_in) { + del_fvec(m_in); + } +- del_fvec(m_out); ++ if (m_out) { ++ del_fvec(m_out); ++ } + } + + void BeatProcessor::process() { +diff --git a/plugin/src/Caelestia/Services/cavaprovider.cpp b/plugin/src/Caelestia/Services/cavaprovider.cpp +index 7b6cc1f2..a57f4040 100644 +--- a/plugin/src/Caelestia/Services/cavaprovider.cpp ++++ b/plugin/src/Caelestia/Services/cavaprovider.cpp +@@ -4,7 +4,10 @@ + #include "audioprovider.hpp" + #include + #include +-#include ++#include ++ ++Q_LOGGING_CATEGORY(lcCava, "caelestia.services.cava", QtInfoMsg) ++Q_LOGGING_CATEGORY(lcCavaProcessor, "caelestia.services.cava.processor", QtInfoMsg) + + namespace caelestia::services { + +@@ -57,7 +60,7 @@ void CavaProcessor::process() { + + void CavaProcessor::setBars(int bars) { + if (bars < 0) { +- qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0."; ++ qCWarning(lcCavaProcessor) << "setBars: bars must be greater than 0. Setting to 0."; + bars = 0; + } + +@@ -109,7 +112,7 @@ int CavaProvider::bars() const { + + void CavaProvider::setBars(int bars) { + if (bars < 0) { +- qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0."; ++ qCWarning(lcCava) << "setBars: bars must be greater than 0. Setting to 0."; + bars = 0; + } + +diff --git a/plugin/src/Caelestia/Services/service.cpp b/plugin/src/Caelestia/Services/service.cpp +index bc215671..4e1921e8 100644 +--- a/plugin/src/Caelestia/Services/service.cpp ++++ b/plugin/src/Caelestia/Services/service.cpp +@@ -1,6 +1,5 @@ + #include "service.hpp" + +-#include + #include + + namespace caelestia::services { +diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp +index 6e37e16f..8d80ced9 100644 +--- a/plugin/src/Caelestia/appdb.cpp ++++ b/plugin/src/Caelestia/appdb.cpp +@@ -1,9 +1,12 @@ + #include "appdb.hpp" + ++#include + #include + #include + #include + ++Q_LOGGING_CATEGORY(lcAppDb, "caelestia.appdb", QtInfoMsg) ++ + namespace caelestia { + + AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) +@@ -11,7 +14,7 @@ AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) + , m_entry(entry) + , m_frequency(frequency) { + const auto mo = m_entry->metaObject(); +- const auto tmo = metaObject(); ++ const auto tmo = &AppEntry::staticMetaObject; + + for (const auto& prop : + { "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) { +@@ -162,6 +165,39 @@ void AppDb::setEntries(const QObjectList& entries) { + m_timer->start(); + } + ++QStringList AppDb::favouriteApps() const { ++ return m_favouriteApps; ++} ++ ++void AppDb::setFavouriteApps(const QStringList& favApps) { ++ if (m_favouriteApps == favApps) { ++ return; ++ } ++ ++ m_favouriteApps = favApps; ++ emit favouriteAppsChanged(); ++ m_favouriteAppsRegex.clear(); ++ m_favouriteAppsRegex.reserve(m_favouriteApps.size()); ++ for (const QString& item : std::as_const(m_favouriteApps)) { ++ const QRegularExpression re(regexifyString(item)); ++ if (re.isValid()) { ++ m_favouriteAppsRegex << re; ++ } else { ++ qCWarning(lcAppDb) << "setFavouriteApps: regular expression is not valid:" << re.pattern(); ++ } ++ } ++ ++ emit appsChanged(); ++} ++ ++QString AppDb::regexifyString(const QString& original) const { ++ if (original.startsWith('^') && original.endsWith('$')) ++ return original; ++ ++ const QString escaped = QRegularExpression::escape(original); ++ return QStringLiteral("^%1$").arg(escaped); ++} ++ + QQmlListProperty AppDb::apps() { + return QQmlListProperty(this, &getSortedApps()); + } +@@ -179,28 +215,48 @@ void AppDb::incrementFrequency(const QString& id) { + auto* app = m_apps.value(id); + if (app) { + const auto before = getSortedApps(); +- + app->incrementFrequency(); +- +- if (before != getSortedApps()) { ++ getSortedApps(); ++ if (before != m_sortedApps) { + emit appsChanged(); + } + } else { +- qWarning() << "AppDb::incrementFrequency: could not find app with id" << id; ++ qCWarning(lcAppDb) << "incrementFrequency: could not find app with id" << id; + } + } + + QList& AppDb::getSortedApps() const { + m_sortedApps = m_apps.values(); +- std::sort(m_sortedApps.begin(), m_sortedApps.end(), [](AppEntry* a, AppEntry* b) { +- if (a->frequency() != b->frequency()) { ++ ++ // Pre-compute favourite status to avoid repeated regex matching during sort ++ QSet favSet; ++ favSet.reserve(m_sortedApps.size()); ++ for (const auto* app : std::as_const(m_sortedApps)) { ++ if (isFavourite(app)) ++ favSet.insert(app->id()); ++ } ++ ++ std::sort(m_sortedApps.begin(), m_sortedApps.end(), [&favSet](AppEntry* a, AppEntry* b) { ++ const bool aIsFav = favSet.contains(a->id()); ++ const bool bIsFav = favSet.contains(b->id()); ++ if (aIsFav != bIsFav) ++ return aIsFav; ++ if (a->frequency() != b->frequency()) + return a->frequency() > b->frequency(); +- } + return a->name().localeAwareCompare(b->name()) < 0; + }); + return m_sortedApps; + } + ++bool AppDb::isFavourite(const AppEntry* app) const { ++ for (const QRegularExpression& re : m_favouriteAppsRegex) { ++ if (re.match(app->id()).hasMatch()) { ++ return true; ++ } ++ } ++ return false; ++} ++ + quint32 AppDb::getFrequency(const QString& id) const { + auto db = QSqlDatabase::database(m_uuid); + QSqlQuery query(db); +@@ -222,7 +278,8 @@ void AppDb::updateAppFrequencies() { + app->setFrequency(getFrequency(app->id())); + } + +- if (before != getSortedApps()) { ++ getSortedApps(); ++ if (before != m_sortedApps) { + emit appsChanged(); + } + } +@@ -249,11 +306,13 @@ void AppDb::updateApps() { + newIds.insert(entry->property("id").toString()); + } + +- for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) { +- const auto& id = *it; +- if (!newIds.contains(id)) { ++ for (auto it = m_apps.begin(); it != m_apps.end();) { ++ if (!newIds.contains(it.key())) { + dirty = true; +- m_apps.take(id)->deleteLater(); ++ it.value()->deleteLater(); ++ it = m_apps.erase(it); ++ } else { ++ ++it; + } + } + +diff --git a/plugin/src/Caelestia/appdb.hpp b/plugin/src/Caelestia/appdb.hpp +index 5f9b9604..ce5f2706 100644 +--- a/plugin/src/Caelestia/appdb.hpp ++++ b/plugin/src/Caelestia/appdb.hpp +@@ -4,6 +4,7 @@ + #include + #include + #include ++#include + #include + + namespace caelestia { +@@ -66,6 +67,7 @@ class AppDb : public QObject { + Q_PROPERTY(QString uuid READ uuid CONSTANT) + Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED) + Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED) ++ Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED) + Q_PROPERTY(QQmlListProperty apps READ apps NOTIFY appsChanged) + + public: +@@ -79,6 +81,9 @@ public: + [[nodiscard]] QObjectList entries() const; + void setEntries(const QObjectList& entries); + ++ [[nodiscard]] QStringList favouriteApps() const; ++ void setFavouriteApps(const QStringList& favApps); ++ + [[nodiscard]] QQmlListProperty apps(); + + Q_INVOKABLE void incrementFrequency(const QString& id); +@@ -86,6 +91,7 @@ public: + signals: + void pathChanged(); + void entriesChanged(); ++ void favouriteAppsChanged(); + void appsChanged(); + + private: +@@ -94,10 +100,14 @@ private: + const QString m_uuid; + QString m_path; + QObjectList m_entries; ++ QStringList m_favouriteApps; // unedited string list from qml ++ QList m_favouriteAppsRegex; // pre-regexified m_favouriteApps list + QHash m_apps; + mutable QList m_sortedApps; + ++ QString regexifyString(const QString& original) const; + QList& getSortedApps() const; ++ bool isFavourite(const AppEntry* app) const; + quint32 getFrequency(const QString& id) const; + void updateAppFrequencies(); + void updateApps(); +diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp +index 6e3bfa99..28a0e178 100644 +--- a/plugin/src/Caelestia/cutils.cpp ++++ b/plugin/src/Caelestia/cutils.cpp +@@ -6,8 +6,11 @@ + #include + #include + #include ++#include + #include + ++Q_LOGGING_CATEGORY(lcCUtils, "caelestia.cutils", QtInfoMsg) ++ + namespace caelestia { + + void CUtils::saveItem(QQuickItem* target, const QUrl& path) { +@@ -32,17 +35,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q + + void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { + if (!target) { +- qWarning() << "CUtils::saveItem: a target is required"; ++ qCWarning(lcCUtils) << "saveItem: a target is required"; + return; + } + + if (!path.isLocalFile()) { +- qWarning() << "CUtils::saveItem:" << path << "is not a local file"; ++ qCWarning(lcCUtils) << "saveItem:" << path << "is not a local file"; + return; + } + + if (!target->window()) { +- qWarning() << "CUtils::saveItem: unable to save target" << target << "without a window"; ++ qCWarning(lcCUtils) << "saveItem: unable to save target" << target << "without a window"; + return; + } + +@@ -75,13 +78,20 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q + QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { + if (watcher->result()) { + if (onSaved.isCallable()) { +- onSaved.call( +- { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); ++ QJSValueList args = { QJSValue(path.toLocalFile()) }; ++ if (engine) { ++ args << engine->toScriptValue(QVariant::fromValue(path)); ++ } ++ onSaved.call(args); + } + } else { +- qWarning() << "CUtils::saveItem: failed to save" << path; ++ qCWarning(lcCUtils) << "saveItem: failed to save" << path; + if (onFailed.isCallable()) { +- onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); ++ if (engine) { ++ onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); ++ } else { ++ onFailed.call(); ++ } + } + } + watcher->deleteLater(); +@@ -92,17 +102,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q + + bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { + if (!source.isLocalFile()) { +- qWarning() << "CUtils::copyFile: source" << source << "is not a local file"; ++ qCWarning(lcCUtils) << "copyFile: source" << source << "is not a local file"; + return false; + } + if (!target.isLocalFile()) { +- qWarning() << "CUtils::copyFile: target" << target << "is not a local file"; ++ qCWarning(lcCUtils) << "copyFile: target" << target << "is not a local file"; + return false; + } + + if (overwrite && QFile::exists(target.toLocalFile())) { + if (!QFile::remove(target.toLocalFile())) { +- qWarning() << "CUtils::copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); ++ qCWarning(lcCUtils) << "copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); + return false; + } + } +@@ -112,7 +122,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co + + bool CUtils::deleteFile(const QUrl& path) const { + if (!path.isLocalFile()) { +- qWarning() << "CUtils::deleteFile: path" << path << "is not a local file"; ++ qCWarning(lcCUtils) << "deleteFile: path" << path << "is not a local file"; + return false; + } + +@@ -121,7 +131,7 @@ bool CUtils::deleteFile(const QUrl& path) const { + + QString CUtils::toLocalFile(const QUrl& url) const { + if (!url.isLocalFile()) { +- qWarning() << "CUtils::toLocalFile: given url is not a local file" << url; ++ qCWarning(lcCUtils) << "toLocalFile: given url is not a local file" << url; + return QString(); + } + +diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp +index 880b0785..7f3ba526 100644 +--- a/plugin/src/Caelestia/imageanalyser.cpp ++++ b/plugin/src/Caelestia/imageanalyser.cpp +@@ -4,8 +4,11 @@ + #include + #include + #include ++#include + #include + ++Q_LOGGING_CATEGORY(lcImageAnalyser, "caelestia.imageanalyser", QtInfoMsg) ++ + namespace caelestia { + + ImageAnalyser::ImageAnalyser(QObject* parent) +@@ -134,6 +137,11 @@ void ImageAnalyser::update() { + + if (m_sourceItem) { + const QSharedPointer grabResult = m_sourceItem->grabToImage(); ++ if (!grabResult) { ++ QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, ++ Qt::SingleShotConnection); ++ return; ++ } + QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { + m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); + }); +@@ -147,7 +155,7 @@ void ImageAnalyser::update() { + + void ImageAnalyser::analyse(QPromise& promise, const QImage& image, int rescaleSize) { + if (image.isNull()) { +- qWarning() << "ImageAnalyser::analyse: image is null"; ++ qCWarning(lcImageAnalyser) << "analyse: image is null"; + return; + } + +@@ -191,14 +199,14 @@ void ImageAnalyser::analyse(QPromise& promise, const QImage& imag + continue; + } + +- const quint32 mr = static_cast(pixel[0] & 0xF8); ++ const quint32 mr = static_cast(pixel[2] & 0xF8); + const quint32 mg = static_cast(pixel[1] & 0xF8); +- const quint32 mb = static_cast(pixel[2] & 0xF8); ++ const quint32 mb = static_cast(pixel[0] & 0xF8); + ++colours[(mr << 16) | (mg << 8) | mb]; + +- const qreal r = pixel[0] / 255.0; ++ const qreal r = pixel[2] / 255.0; + const qreal g = pixel[1] / 255.0; +- const qreal b = pixel[2] / 255.0; ++ const qreal b = pixel[0] / 255.0; + totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); + ++count; + } +diff --git a/plugin/src/Caelestia/imageanalyser.hpp b/plugin/src/Caelestia/imageanalyser.hpp +index bbea2b32..63fbf969 100644 +--- a/plugin/src/Caelestia/imageanalyser.hpp ++++ b/plugin/src/Caelestia/imageanalyser.hpp +@@ -4,6 +4,7 @@ + #include + #include + #include ++#include + #include + + namespace caelestia { +@@ -48,7 +49,7 @@ private: + QFutureWatcher* const m_futureWatcher; + + QString m_source; +- QQuickItem* m_sourceItem; ++ QPointer m_sourceItem; + int m_rescaleSize; + + QColor m_dominantColour; +diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp +index 44e8d21e..c7242179 100644 +--- a/plugin/src/Caelestia/qalculator.cpp ++++ b/plugin/src/Caelestia/qalculator.cpp +@@ -1,13 +1,19 @@ + #include "qalculator.hpp" + + #include ++#include + + namespace caelestia { + ++QMutex Qalculator::s_calculatorMutex; ++ + Qalculator::Qalculator(QObject* parent) + : QObject(parent) { + if (!CALCULATOR) { +- new Calculator(); ++ // Calculator constructor sets the global `calculator` pointer (CALCULATOR macro), ++ // but we need to assign it to a var so compiler doesn't flag it as a leak ++ static const auto* const instance = new Calculator(); ++ Q_UNUSED(instance) + CALCULATOR->loadExchangeRates(); + CALCULATOR->loadGlobalDefinitions(); + CALCULATOR->loadLocalDefinitions(); +@@ -19,6 +25,8 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { + return QString(); + } + ++ QMutexLocker locker(&s_calculatorMutex); ++ + EvaluationOptions eo; + PrintOptions po; + +@@ -49,4 +57,92 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { + return QString::fromStdString(result); + } + ++void Qalculator::evalAsync(const QString& expr) { ++ const quint64 gen = ++m_generation; ++ ++ if (expr.isEmpty()) { ++ if (!m_result.isEmpty()) { ++ m_result.clear(); ++ emit resultChanged(); ++ } ++ if (!m_rawResult.isEmpty()) { ++ m_rawResult.clear(); ++ emit rawResultChanged(); ++ } ++ if (m_busy) { ++ m_busy = false; ++ emit busyChanged(); ++ } ++ return; ++ } ++ ++ if (!m_busy) { ++ m_busy = true; ++ emit busyChanged(); ++ } ++ ++ QtConcurrent::run([expr]() -> QPair { ++ QMutexLocker locker(&s_calculatorMutex); ++ ++ EvaluationOptions eo; ++ PrintOptions po; ++ ++ std::string parsed; ++ std::string result = CALCULATOR->calculateAndPrint( ++ CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); ++ ++ std::string error; ++ while (CALCULATOR->message()) { ++ if (!CALCULATOR->message()->message().empty()) { ++ if (CALCULATOR->message()->type() == MESSAGE_ERROR) { ++ error += "error: "; ++ } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { ++ error += "warning: "; ++ } ++ error += CALCULATOR->message()->message(); ++ } ++ CALCULATOR->nextMessage(); ++ } ++ ++ if (!error.empty()) { ++ const QString errorStr = QString::fromStdString(error); ++ return { errorStr, errorStr }; ++ } ++ ++ const QString rawStr = QString::fromStdString(result); ++ return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; ++ }).then(this, [this, gen](QPair result) { ++ if (gen != m_generation) { ++ return; ++ } ++ ++ const auto& [formatted, raw] = result; ++ ++ if (m_result != formatted) { ++ m_result = formatted; ++ emit resultChanged(); ++ } ++ if (m_rawResult != raw) { ++ m_rawResult = raw; ++ emit rawResultChanged(); ++ } ++ if (m_busy) { ++ m_busy = false; ++ emit busyChanged(); ++ } ++ }); ++} ++ ++QString Qalculator::result() const { ++ return m_result; ++} ++ ++QString Qalculator::rawResult() const { ++ return m_rawResult; ++} ++ ++bool Qalculator::busy() const { ++ return m_busy; ++} ++ + } // namespace caelestia +diff --git a/plugin/src/Caelestia/qalculator.hpp b/plugin/src/Caelestia/qalculator.hpp +index a07a8a2f..b2f5517f 100644 +--- a/plugin/src/Caelestia/qalculator.hpp ++++ b/plugin/src/Caelestia/qalculator.hpp +@@ -1,5 +1,6 @@ + #pragma once + ++#include + #include + #include + +@@ -10,10 +11,32 @@ class Qalculator : public QObject { + QML_ELEMENT + QML_SINGLETON + ++ Q_PROPERTY(QString result READ result NOTIFY resultChanged) ++ Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged) ++ Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) ++ + public: + explicit Qalculator(QObject* parent = nullptr); + + Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; ++ Q_INVOKABLE void evalAsync(const QString& expr); ++ ++ [[nodiscard]] QString result() const; ++ [[nodiscard]] QString rawResult() const; ++ [[nodiscard]] bool busy() const; ++ ++signals: ++ void resultChanged(); ++ void rawResultChanged(); ++ void busyChanged(); ++ ++private: ++ static QMutex s_calculatorMutex; ++ ++ QString m_result; ++ QString m_rawResult; ++ bool m_busy = false; ++ quint64 m_generation = 0; + }; + + } // namespace caelestia +diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp +index 2ceddb35..862dea7f 100644 +--- a/plugin/src/Caelestia/requests.cpp ++++ b/plugin/src/Caelestia/requests.cpp +@@ -1,22 +1,41 @@ + #include "requests.hpp" + ++#include ++#include + #include ++#include + #include + #include + ++Q_LOGGING_CATEGORY(lcRequests, "caelestia.requests", QtInfoMsg) ++ + namespace caelestia { + + Requests::Requests(QObject* parent) + : QObject(parent) + , m_manager(new QNetworkAccessManager(this)) {} + +-void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const { ++void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { + if (!onSuccess.isCallable()) { +- qWarning() << "Requests::get: onSuccess is not callable"; ++ qCWarning(lcRequests) << "get: onSuccess is not callable"; + return; + } + + QNetworkRequest request(url); ++ request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); ++ request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); ++ request.setRawHeader("Cache-Control", "no-cache, no-store"); ++ request.setRawHeader("Pragma", "no-cache"); ++ request.setRawHeader("Connection", "close"); ++ ++ if (headers.isObject()) { ++ QJSValueIterator it(headers); ++ while (it.hasNext()) { ++ it.next(); ++ request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8()); ++ } ++ } ++ + auto reply = m_manager->get(request); + + QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() { +@@ -25,11 +44,15 @@ void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const + } else if (onError.isCallable()) { + onError.call({ reply->errorString() }); + } else { +- qWarning() << "Requests::get: request failed with error" << reply->errorString(); ++ qCWarning(lcRequests) << "get: request failed with error" << reply->errorString(); + } + + reply->deleteLater(); + }); + } + ++void Requests::resetCookies() const { ++ m_manager->setCookieJar(new QNetworkCookieJar(m_manager)); ++} ++ + } // namespace caelestia +diff --git a/plugin/src/Caelestia/requests.hpp b/plugin/src/Caelestia/requests.hpp +index 1db2f4cf..d07d7e8f 100644 +--- a/plugin/src/Caelestia/requests.hpp ++++ b/plugin/src/Caelestia/requests.hpp +@@ -14,7 +14,9 @@ class Requests : public QObject { + public: + explicit Requests(QObject* parent = nullptr); + +- Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const; ++ Q_INVOKABLE void get( ++ const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; ++ Q_INVOKABLE void resetCookies() const; + + private: + QNetworkAccessManager* m_manager; +diff --git a/plugin/src/Caelestia/toaster.cpp b/plugin/src/Caelestia/toaster.cpp +index 978805de..b51c77f8 100644 +--- a/plugin/src/Caelestia/toaster.cpp ++++ b/plugin/src/Caelestia/toaster.cpp +@@ -1,6 +1,5 @@ + #include "toaster.hpp" + +-#include + #include + #include + +diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py +new file mode 100755 +index 00000000..cef46657 +--- /dev/null ++++ b/scripts/qml-lint-conventions.py +@@ -0,0 +1,648 @@ ++#!/usr/bin/env python3 ++"""Checks QML files for Qt coding convention violations. ++ ++https://doc.qt.io/qt-6/qml-codingconventions.html ++ ++Required ordering within each QML object (with blank line between sections): ++ 1. id ++ 2. property declarations ++ 3. signal declarations ++ 4. JavaScript functions ++ 5. object properties (bindings) ++ 6. child objects ++ 7. component definitions ++""" ++ ++import re ++import sys ++from enum import IntEnum ++from pathlib import Path ++ ++RED = "\033[0;31m" ++YELLOW = "\033[0;33m" ++CYAN = "\033[0;36m" ++GREEN = "\033[0;32m" ++MAGENTA = "\033[0;35m" ++BOLD = "\033[1m" ++RESET = "\033[0m" ++ ++REPO_ROOT = Path(__file__).resolve().parent.parent ++ ++ ++class Section(IntEnum): ++ ID = 0 ++ PROPERTY = 1 ++ SIGNAL = 2 ++ FUNCTION = 3 ++ BINDING = 4 ++ CHILD = 5 ++ COMPONENT_DEF = 6 ++ ++ ++SECTION_NAMES = { ++ Section.ID: "id", ++ Section.PROPERTY: "property declarations", ++ Section.SIGNAL: "signal declarations", ++ Section.FUNCTION: "functions", ++ Section.BINDING: "bindings", ++ Section.CHILD: "child objects", ++ Section.COMPONENT_DEF: "component definitions", ++} ++ ++RULE_COLOURS = { ++ "file-structure": RED, ++ "import-order": GREEN, ++ "section-order": YELLOW, ++ "missing-section-separator": CYAN, ++ "blank-after-open-brace": MAGENTA, ++ "blank-before-close-brace": MAGENTA, ++} ++ ++IMPORT_RE = re.compile(r"^import\s+(\S+)") ++ ++ ++def import_group(module: str) -> tuple[int, int] | None: ++ """Return (group, depth) for a module import, or None to skip.""" ++ if module.startswith('"'): ++ return None ++ depth = module.count(".") + 1 ++ if module == "QtQuick" or module.startswith("QtQuick."): ++ return (1, depth) ++ if module.startswith("Qt"): ++ return (2, depth) ++ if module == "Quickshell" or module.startswith("Quickshell."): ++ return (3, depth) ++ if module == "M3Shapes": ++ return (4, depth) ++ if module == "Caelestia" or module.startswith("Caelestia."): ++ return (5, depth) ++ if module == "qs.components" or module.startswith("qs.components."): ++ return (6, depth) ++ if module == "qs.services": ++ return (7, depth) ++ if module == "qs.config": ++ return (8, depth) ++ if module == "qs.utils": ++ return (9, depth) ++ if module == "qs.modules" or module.startswith("qs.modules."): ++ return (10, depth) ++ return None ++ ++ ++def parse_imports(lines: list[str]) -> tuple[int | None, int | None, list[str], list[tuple[str, int, int, str]]]: ++ """Parse the import block, returning (first_idx, last_idx, relative_imports, module_imports). ++ ++ module_imports entries are (line_text, group, depth, module_name). ++ """ ++ first_import = None ++ last_import = None ++ relative_imports: list[str] = [] ++ module_imports: list[tuple[str, int, int, str]] = [] ++ ++ for i, line in enumerate(lines): ++ stripped = line.strip() ++ if not stripped or stripped.startswith("//") or stripped.startswith("pragma "): ++ continue ++ m = IMPORT_RE.match(stripped) ++ if m: ++ if first_import is None: ++ first_import = i ++ last_import = i ++ module = m.group(1) ++ result = import_group(module) ++ if result is None: ++ relative_imports.append(line) ++ else: ++ group, depth = result ++ module_imports.append((line, group, depth, module)) ++ continue ++ break ++ ++ return first_import, last_import, relative_imports, module_imports ++ ++ ++def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]: ++ """Check that module imports are in the required order.""" ++ violations = [] ++ _, _, _, module_imports = parse_imports(lines) ++ imports = [(i, *entry) for i, entry in enumerate(module_imports)] ++ ++ for j in range(1, len(imports)): ++ _, prev_line, prev_group, prev_depth, prev_mod = imports[j - 1] ++ _, curr_line, curr_group, curr_depth, curr_mod = imports[j] ++ ++ # Find actual line number for the current import ++ lineno = 0 ++ count = 0 ++ for li, line in enumerate(lines): ++ stripped = line.strip() ++ m = IMPORT_RE.match(stripped) ++ if m and import_group(m.group(1)) is not None: ++ if count == j: ++ lineno = li + 1 ++ break ++ count += 1 ++ ++ if curr_group < prev_group: ++ violations.append( ++ Violation( ++ rel, ++ lineno, ++ "import-order", ++ f"'{curr_mod}' should appear before '{prev_mod}'", ++ ) ++ ) ++ elif curr_group == prev_group and curr_depth < prev_depth: ++ violations.append( ++ Violation( ++ rel, ++ lineno, ++ "import-order", ++ f"'{curr_mod}' should appear before '{prev_mod}' (less nested first)", ++ ) ++ ) ++ ++ return violations ++ ++ ++def fix_imports(lines: list[str]) -> list[str]: ++ """Sort imports and return the modified lines.""" ++ first, last, relative, module = parse_imports(lines) ++ if first is None: ++ return lines ++ ++ module.sort(key=lambda x: (x[1], x[2], x[3])) ++ sorted_imports = relative + [entry[0] for entry in module] ++ return lines[:first] + sorted_imports + lines[last + 1 :] ++ ++ ++def check_file_structure(lines: list[str], rel: str) -> list[Violation]: ++ """Check file-level structure: pragmas, then imports, then content.""" ++ violations = [] ++ pragma_indices: list[int] = [] ++ import_indices: list[int] = [] ++ content_start: int | None = None ++ ++ for i, line in enumerate(lines): ++ stripped = line.strip() ++ if not stripped: ++ continue ++ if stripped.startswith("pragma "): ++ pragma_indices.append(i) ++ elif IMPORT_RE.match(stripped): ++ import_indices.append(i) ++ else: ++ content_start = i ++ break ++ ++ # Pragmas must come before imports ++ if pragma_indices and import_indices: ++ if pragma_indices[-1] > import_indices[0]: ++ violations.append( ++ Violation(rel, pragma_indices[-1] + 1, "file-structure", "pragmas should appear before imports") ++ ) ++ ++ # Separator between pragmas and imports ++ if pragma_indices and import_indices: ++ gap = import_indices[0] - pragma_indices[-1] - 1 ++ if gap == 0: ++ violations.append( ++ Violation( ++ rel, import_indices[0] + 1, "file-structure", "blank line expected between pragmas and imports" ++ ) ++ ) ++ elif gap > 1: ++ violations.append( ++ Violation( ++ rel, ++ pragma_indices[-1] + 3, ++ "file-structure", ++ "only one blank line expected between pragmas and imports", ++ ) ++ ) ++ ++ # No blank lines within imports ++ for j in range(1, len(import_indices)): ++ if import_indices[j] != import_indices[j - 1] + 1: ++ for gap_line in range(import_indices[j - 1] + 1, import_indices[j]): ++ if not lines[gap_line].strip(): ++ violations.append( ++ Violation(rel, gap_line + 1, "file-structure", "no blank lines expected within imports") ++ ) ++ ++ # Separator between imports/pragmas and content ++ last_header = import_indices[-1] if import_indices else (pragma_indices[-1] if pragma_indices else None) ++ if last_header is not None and content_start is not None: ++ gap = content_start - last_header - 1 ++ label = "imports" if import_indices else "pragmas" ++ if gap == 0: ++ violations.append( ++ Violation( ++ rel, content_start + 1, "file-structure", f"blank line expected between {label} and content" ++ ) ++ ) ++ elif gap > 1: ++ violations.append( ++ Violation( ++ rel, ++ last_header + 3, ++ "file-structure", ++ f"only one blank line expected between {label} and content", ++ ) ++ ) ++ ++ return violations ++ ++ ++def fix_file_structure(lines: list[str]) -> list[str]: ++ """Ensure correct file structure: pragmas, blank, imports (no gaps), blank, content.""" ++ pragmas: list[str] = [] ++ imports: list[str] = [] ++ content_start: int | None = None ++ ++ for i, line in enumerate(lines): ++ stripped = line.strip() ++ if not stripped: ++ continue ++ if stripped.startswith("pragma "): ++ pragmas.append(line) ++ elif IMPORT_RE.match(stripped): ++ imports.append(line) ++ else: ++ content_start = i ++ break ++ ++ if content_start is None: ++ content_start = len(lines) ++ ++ # Skip any blank lines at the start of content ++ while content_start < len(lines) and not lines[content_start].strip(): ++ content_start += 1 ++ ++ result: list[str] = [] ++ has_content = content_start < len(lines) ++ if pragmas: ++ result.extend(pragmas) ++ if imports or has_content: ++ result.append("") ++ if imports: ++ result.extend(imports) ++ if has_content: ++ result.append("") ++ result.extend(lines[content_start:]) ++ return result ++ ++ ++def fix_section_separators(lines: list[str]) -> list[str]: ++ """Insert blank lines between different sections and return modified lines.""" ++ insertions: list[int] = [] ++ scopes: dict[str, ScopeTracker] = {} ++ in_block_comment = False ++ func_skip_depth = 0 ++ prev_blank: dict[str, bool] = {} ++ ++ for i, line in enumerate(lines): ++ stripped = line.strip() ++ indent = get_indent(line) ++ ++ if in_block_comment: ++ if BLOCK_COMMENT_END.search(stripped): ++ in_block_comment = False ++ continue ++ if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): ++ in_block_comment = True ++ continue ++ ++ if not stripped: ++ for key in prev_blank: ++ prev_blank[key] = True ++ continue ++ ++ if COMMENT_LINE_RE.match(stripped): ++ continue ++ ++ if func_skip_depth > 0: ++ func_skip_depth += stripped.count("{") - stripped.count("}") ++ if func_skip_depth <= 0: ++ func_skip_depth = 0 ++ continue ++ ++ if stripped == "}": ++ to_remove = [k for k in scopes if len(k) > len(indent)] ++ for k in to_remove: ++ del scopes[k] ++ prev_blank.pop(k, None) ++ continue ++ ++ section = classify_line(stripped) ++ if section is None: ++ continue ++ ++ if indent not in scopes: ++ scopes[indent] = ScopeTracker() ++ prev_blank[indent] = True ++ ++ tracker = scopes[indent] ++ had_blank = prev_blank.get(indent, True) ++ ++ if tracker.last_section is not None and section != tracker.last_section and not had_blank: ++ insertions.append(i) ++ ++ if tracker.last_section is None or section >= tracker.last_section: ++ tracker.last_section = section ++ tracker.last_section_line = i + 1 ++ ++ prev_blank[indent] = False ++ ++ brace_count = stripped.count("{") - stripped.count("}") ++ if brace_count > 0 and section == Section.FUNCTION: ++ func_skip_depth = brace_count ++ if brace_count > 0 and section == Section.BINDING: ++ colon_idx = stripped.index(":") ++ after_colon = stripped[colon_idx + 1 :].strip() ++ if not re.match(r"^[A-Z]", after_colon): ++ func_skip_depth = brace_count ++ if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): ++ to_remove = [k for k in scopes if len(k) > len(indent)] ++ for k in to_remove: ++ del scopes[k] ++ prev_blank.pop(k, None) ++ ++ result = list(lines) ++ for idx in reversed(insertions): ++ result.insert(idx, "") ++ return result ++ ++ ++def fix_file(filepath: Path) -> bool: ++ """Fix auto-fixable violations. Returns True if file was modified.""" ++ try: ++ text = filepath.read_text() ++ except (OSError, UnicodeDecodeError): ++ return False ++ ++ lines = text.splitlines() ++ lines = fix_imports(lines) ++ lines = fix_file_structure(lines) ++ lines = fix_section_separators(lines) ++ new_text = "\n".join(lines) ++ if text.endswith("\n"): ++ new_text += "\n" ++ if new_text != text: ++ filepath.write_text(new_text) ++ return True ++ return False ++ ++ ++# Regexes ++PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s") ++SIGNAL_RE = re.compile(r"^signal\s") ++FUNCTION_RE = re.compile(r"^function\s") ++ID_RE = re.compile(r"^id\s*:\s*[a-zA-Z_]\w*\s*$") ++ENUM_RE = re.compile(r"^enum\s") ++COMPONENT_DEF_RE = re.compile(r"^component\s+\w+\s*:") ++COMMENT_LINE_RE = re.compile(r"^//") ++BLOCK_COMMENT_START = re.compile(r"/\*") ++BLOCK_COMMENT_END = re.compile(r"\*/") ++BINDING_RE = re.compile(r"^[a-z][a-zA-Z0-9_.]*\s*:") ++SIGNAL_HANDLER_RE = re.compile(r"^on[A-Z][a-zA-Z]*\s*:") ++# Child object: starts with uppercase or is a known child-like pattern ++CHILD_OBJECT_RE = re.compile(r"^[A-Z][a-zA-Z0-9_.]*\s*\{") ++# Inline component: Component { ... } ++INLINE_COMPONENT_RE = re.compile(r"^Component\s*\{") ++# Behavior on {, NumberAnimation on {, etc. ++BEHAVIOR_ON_RE = re.compile(r"^[A-Z]\w+\s+on\s+\w[\w.]*\s*\{") ++# Attached signal handler: Component.onCompleted:, Drag.onDragStarted:, etc. ++ATTACHED_HANDLER_RE = re.compile(r"^[A-Z]\w+\.on[A-Z]\w*\s*:") ++ ++ ++class Violation: ++ def __init__(self, file: str, line: int, rule: str, msg: str): ++ self.file = file ++ self.line = line ++ self.rule = rule ++ self.msg = msg ++ ++ def __str__(self): ++ c = RULE_COLOURS.get(self.rule, "") ++ return f"{c}[{self.rule}]{RESET} {self.file}:{self.line}: {self.msg}" ++ ++ ++class ScopeTracker: ++ """Tracks the current section and last-seen state for one indent level.""" ++ ++ def __init__(self): ++ self.last_section: Section | None = None ++ self.last_section_line: int = 0 ++ self.had_blank_before_current: bool = True # no separator needed at start ++ ++ ++def get_indent(line: str) -> str: ++ return line[: len(line) - len(line.lstrip())] ++ ++ ++def classify_line(stripped: str) -> Section | None: ++ """Classify a stripped QML line into a section category.""" ++ if ID_RE.match(stripped): ++ return Section.ID ++ if PROPERTY_DECL_RE.match(stripped): ++ return Section.PROPERTY ++ if SIGNAL_RE.match(stripped): ++ return Section.SIGNAL ++ if FUNCTION_RE.match(stripped): ++ return Section.FUNCTION ++ if ENUM_RE.match(stripped): ++ return Section.PROPERTY # enums go with declarations ++ if COMPONENT_DEF_RE.match(stripped): ++ return Section.COMPONENT_DEF ++ if BEHAVIOR_ON_RE.match(stripped): ++ return Section.CHILD ++ if CHILD_OBJECT_RE.match(stripped): ++ return Section.CHILD ++ if INLINE_COMPONENT_RE.match(stripped): ++ return Section.CHILD ++ if BINDING_RE.match(stripped) or SIGNAL_HANDLER_RE.match(stripped): ++ return Section.BINDING ++ if ATTACHED_HANDLER_RE.match(stripped): ++ return Section.BINDING ++ return None ++ ++ ++def check_file(filepath: Path) -> list[Violation]: ++ violations = [] ++ rel = str(filepath.relative_to(REPO_ROOT)) ++ ++ try: ++ lines = filepath.read_text().splitlines() ++ except (OSError, UnicodeDecodeError): ++ return violations ++ ++ violations.extend(check_file_structure(lines, rel)) ++ violations.extend(check_imports(filepath, lines, rel)) ++ ++ scopes: dict[str, ScopeTracker] = {} # indent -> tracker ++ in_block_comment = False ++ func_skip_depth = 0 # brace depth for skipping function bodies only ++ prev_blank: dict[str, bool] = {} # indent -> was previous relevant line a blank? ++ ++ for i, line in enumerate(lines): ++ lineno = i + 1 ++ stripped = line.strip() ++ indent = get_indent(line) ++ ++ # Handle block comments ++ if in_block_comment: ++ if BLOCK_COMMENT_END.search(stripped): ++ in_block_comment = False ++ continue ++ if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): ++ in_block_comment = True ++ continue ++ ++ # Track blank lines per indent ++ if not stripped: ++ # Check: blank line right after opening brace of a QML object ++ if i > 0 and func_skip_depth == 0 and not in_block_comment and lines[i - 1].strip().endswith("{"): ++ violations.append( ++ Violation( ++ rel, ++ lineno, ++ "blank-after-open-brace", ++ "no blank line expected after opening brace", ++ ) ++ ) ++ for key in prev_blank: ++ prev_blank[key] = True ++ continue ++ ++ # Skip line comments ++ if COMMENT_LINE_RE.match(stripped): ++ continue ++ ++ # Skip inside function bodies (JS code, not QML structure) ++ if func_skip_depth > 0: ++ func_skip_depth += stripped.count("{") - stripped.count("}") ++ if func_skip_depth <= 0: ++ func_skip_depth = 0 ++ continue ++ ++ # Closing brace: pop all scopes deeper than this indent ++ # (the scope at this indent belongs to the parent object and must persist) ++ if stripped == "}": ++ # Check: blank line right before closing brace ++ if i > 0 and not lines[i - 1].strip(): ++ violations.append( ++ Violation( ++ rel, ++ lineno, ++ "blank-before-close-brace", ++ "no blank line expected before closing brace", ++ ) ++ ) ++ to_remove = [k for k in scopes if len(k) > len(indent)] ++ for k in to_remove: ++ del scopes[k] ++ prev_blank.pop(k, None) ++ continue ++ ++ section = classify_line(stripped) ++ if section is None: ++ continue ++ ++ # Get or create scope tracker for this indent ++ if indent not in scopes: ++ scopes[indent] = ScopeTracker() ++ prev_blank[indent] = True # treat start of object as having separator ++ ++ tracker = scopes[indent] ++ had_blank = prev_blank.get(indent, True) ++ ++ # --- Check 1: Section ordering --- ++ if tracker.last_section is not None and section < tracker.last_section: ++ violations.append( ++ Violation( ++ rel, ++ lineno, ++ "section-order", ++ f"{SECTION_NAMES[section]} should appear before " ++ f"{SECTION_NAMES[tracker.last_section]} " ++ f"(seen at line {tracker.last_section_line})", ++ ) ++ ) ++ ++ # --- Check 2: Missing blank line between different sections --- ++ if tracker.last_section is not None and section != tracker.last_section and not had_blank: ++ violations.append( ++ Violation( ++ rel, ++ lineno, ++ "missing-section-separator", ++ f"blank line expected between {SECTION_NAMES[tracker.last_section]} and {SECTION_NAMES[section]}", ++ ) ++ ) ++ ++ # Update tracker ++ if tracker.last_section is None or section >= tracker.last_section: ++ tracker.last_section = section ++ tracker.last_section_line = lineno ++ ++ prev_blank[indent] = False ++ ++ # Skip function bodies (they contain JS, not QML structure) ++ brace_count = stripped.count("{") - stripped.count("}") ++ if brace_count > 0 and section == Section.FUNCTION: ++ func_skip_depth = brace_count ++ ++ # Skip JS blocks in bindings (signal handlers, attached handlers, ++ # and expression blocks like `color: { ... }`) ++ if brace_count > 0 and section == Section.BINDING: ++ colon_idx = stripped.index(":") ++ after_colon = stripped[colon_idx + 1 :].strip() ++ # If content after : doesn't start with an uppercase type name, ++ # it's a JS block (not an inline QML object like `contentItem: Rect {`) ++ if not re.match(r"^[A-Z]", after_colon): ++ func_skip_depth = brace_count ++ ++ # Child object/component opening resets deeper scopes ++ if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): ++ to_remove = [k for k in scopes if len(k) > len(indent)] ++ for k in to_remove: ++ del scopes[k] ++ prev_blank.pop(k, None) ++ ++ return violations ++ ++ ++def main(): ++ fix_mode = "--fix" in sys.argv ++ qml_files = sorted(p for p in REPO_ROOT.rglob("*.qml") if "build" not in p.parts) ++ ++ if fix_mode: ++ fixed = sum(1 for f in qml_files if fix_file(f)) ++ print(f"{BOLD}Fixed {fixed} file(s).{RESET}\n") ++ ++ print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n") ++ ++ all_violations: list[Violation] = [] ++ for f in qml_files: ++ all_violations.extend(check_file(f)) ++ ++ for v in all_violations: ++ print(v) ++ ++ print() ++ if all_violations: ++ by_rule: dict[str, int] = {} ++ for v in all_violations: ++ by_rule[v.rule] = by_rule.get(v.rule, 0) + 1 ++ for rule, count in sorted(by_rule.items()): ++ print(f" {RULE_COLOURS.get(rule, '')}{rule}{RESET}: {count}") ++ print(f"\n{BOLD}Found {len(all_violations)} violation(s).{RESET}") ++ return 1 ++ else: ++ print(f"{BOLD}No violations found.{RESET}") ++ return 0 ++ ++ ++if __name__ == "__main__": ++ sys.exit(main()) +diff --git a/services/Audio.qml b/services/Audio.qml +index 908d1563..6268b92e 100644 +--- a/services/Audio.qml ++++ b/services/Audio.qml +@@ -1,11 +1,12 @@ + pragma Singleton + +-import qs.config +-import Caelestia.Services +-import Caelestia ++import QtQuick + import Quickshell ++import Quickshell.Io + import Quickshell.Services.Pipewire +-import QtQuick ++import Caelestia ++import Caelestia.Config ++import Caelestia.Services + + Singleton { + id: root +@@ -13,26 +14,9 @@ Singleton { + property string previousSinkName: "" + property string previousSourceName: "" + +- readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { +- if (!node.isStream) { +- if (node.isSink) +- acc.sinks.push(node); +- else if (node.audio) +- acc.sources.push(node); +- } else if (node.isStream && node.audio) { +- // Application streams (output streams) +- acc.streams.push(node); +- } +- return acc; +- }, { +- sources: [], +- sinks: [], +- streams: [] +- }) +- +- readonly property list sinks: nodes.sinks +- readonly property list sources: nodes.sources +- readonly property list streams: nodes.streams ++ property list sinks: [] ++ property list sources: [] ++ property list streams: [] + + readonly property PwNode sink: Pipewire.defaultAudioSink + readonly property PwNode source: Pipewire.defaultAudioSource +@@ -49,31 +33,31 @@ Singleton { + function setVolume(newVolume: real): void { + if (sink?.ready && sink?.audio) { + sink.audio.muted = false; +- sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); ++ sink.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); + } + } + + function incrementVolume(amount: real): void { +- setVolume(volume + (amount || Config.services.audioIncrement)); ++ setVolume(volume + (amount || GlobalConfig.services.audioIncrement)); + } + + function decrementVolume(amount: real): void { +- setVolume(volume - (amount || Config.services.audioIncrement)); ++ setVolume(volume - (amount || GlobalConfig.services.audioIncrement)); + } + + function setSourceVolume(newVolume: real): void { + if (source?.ready && source?.audio) { + source.audio.muted = false; +- source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); ++ source.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); + } + } + + function incrementSourceVolume(amount: real): void { +- setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); ++ setSourceVolume(sourceVolume + (amount || GlobalConfig.services.audioIncrement)); + } + + function decrementSourceVolume(amount: real): void { +- setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); ++ setSourceVolume(sourceVolume - (amount || GlobalConfig.services.audioIncrement)); + } + + function setAudioSink(newSink: PwNode): void { +@@ -84,10 +68,19 @@ Singleton { + Pipewire.preferredDefaultAudioSource = newSource; + } + ++ function cycleNextAudioOutput(): void { ++ if (sinks.length === 0) ++ return; ++ ++ const currentIndex = sinks.findIndex(s => s === sink); ++ const nextIndex = (currentIndex + 1) % sinks.length; ++ setAudioSink(sinks[nextIndex]); ++ } ++ + function setStreamVolume(stream: PwNode, newVolume: real): void { + if (stream?.ready && stream?.audio) { + stream.audio.muted = false; +- stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); ++ stream.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); + } + } + +@@ -109,7 +102,7 @@ Singleton { + if (!stream) + return qsTr("Unknown"); + // Try application name first, then description, then name +- return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); ++ return stream.properties["application.name"] || stream.description || stream.name || qsTr("Unknown Application"); + } + + onSinkChanged: { +@@ -118,7 +111,7 @@ Singleton { + + const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); + +- if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) ++ if (previousSinkName && previousSinkName !== newSinkName && GlobalConfig.utilities.toasts.audioOutputChanged) + Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); + + previousSinkName = newSinkName; +@@ -130,7 +123,7 @@ Singleton { + + const newSourceName = source.description || source.name || qsTr("Unknown Device"); + +- if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) ++ if (previousSourceName && previousSourceName !== newSourceName && GlobalConfig.utilities.toasts.audioInputChanged) + Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); + + previousSourceName = newSourceName; +@@ -141,6 +134,31 @@ Singleton { + previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); + } + ++ Connections { ++ function onValuesChanged(): void { ++ const newSinks = []; ++ const newSources = []; ++ const newStreams = []; ++ ++ for (const node of Pipewire.nodes.values) { ++ if (!node.isStream) { ++ if (node.isSink) ++ newSinks.push(node); ++ else if (node.audio) ++ newSources.push(node); ++ } else if (node.audio) { ++ newStreams.push(node); ++ } ++ } ++ ++ root.sinks = newSinks; ++ root.sources = newSources; ++ root.streams = newStreams; ++ } ++ ++ target: Pipewire.nodes ++ } ++ + PwObjectTracker { + objects: [...root.sinks, ...root.sources, ...root.streams] + } +@@ -148,10 +166,18 @@ Singleton { + CavaProvider { + id: cava + +- bars: Config.services.visualiserBars ++ bars: GlobalConfig.services.visualiserBars + } + + BeatTracker { + id: beatTracker + } ++ ++ IpcHandler { ++ function cycleOutput(): void { ++ root.cycleNextAudioOutput(); ++ } ++ ++ target: "audio" ++ } + } +diff --git a/services/Brightness.qml b/services/Brightness.qml +index 12920eed..ac34cbc1 100644 +--- a/services/Brightness.qml ++++ b/services/Brightness.qml +@@ -1,56 +1,62 @@ + pragma Singleton + pragma ComponentBehavior: Bound + +-import qs.config +-import qs.components.misc ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick ++import Caelestia.Config ++import qs.components.misc + + Singleton { + id: root + + property list ddcMonitors: [] +- readonly property list monitors: variants.instances ++ readonly property var ddcMonitorMap: { ++ const map = {}; ++ for (const m of ddcMonitors) ++ map[m.connector] = m; ++ return map; ++ } ++ readonly property list monitors: variants.instances // qmllint disable incompatible-type + property bool appleDisplayPresent: false + + function getMonitorForScreen(screen: ShellScreen): var { +- return monitors.find(m => m.modelData === screen); ++ return monitors.find(m => m.modelData === screen); // qmllint disable missing-property + } + + function getMonitor(query: string): var { + if (query === "active") { +- return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); ++ return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property + } + + if (query.startsWith("model:")) { + const model = query.slice(6); +- return monitors.find(m => m.modelData.model === model); ++ return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property + } + + if (query.startsWith("serial:")) { + const serial = query.slice(7); +- return monitors.find(m => m.modelData.serialNumber === serial); ++ return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property + } + + if (query.startsWith("id:")) { + const id = parseInt(query.slice(3), 10); +- return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); ++ return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property + } + +- return monitors.find(m => m.modelData.name === query); ++ return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property + } + + function increaseBrightness(): void { + const monitor = getMonitor("active"); + if (monitor) +- monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); ++ monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); + } + + function decreaseBrightness(): void { + const monitor = getMonitor("active"); + if (monitor) +- monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); ++ monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); + } + + onMonitorsChanged: { +@@ -61,7 +67,7 @@ Singleton { + Variants { + id: variants + +- model: Quickshell.screens ++ model: Quickshell.screens // Don't respect excluded screens cause ipc + + Monitor {} + } +@@ -86,21 +92,23 @@ Singleton { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "brightnessUp" + description: "Increase brightness" + onPressed: root.increaseBrightness() + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "brightnessDown" + description: "Decrease brightness" + onPressed: root.decreaseBrightness() + } + + IpcHandler { +- target: "brightness" +- + function get(): real { + return getFor("active"); + } +@@ -149,14 +157,17 @@ Singleton { + + return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; + } ++ ++ target: "brightness" + } + + component Monitor: QtObject { + id: monitor + + required property ShellScreen modelData +- readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name) +- readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" ++ readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null ++ readonly property bool isDdc: ddcInfo !== null ++ readonly property string busNum: ddcInfo?.busNum ?? "" + readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") + property real brightness + property real queuedBrightness: NaN +diff --git a/services/Colours.qml b/services/Colours.qml +index cd86c8fb..922b51db 100644 +--- a/services/Colours.qml ++++ b/services/Colours.qml +@@ -1,12 +1,13 @@ + pragma Singleton + pragma ComponentBehavior: Bound + +-import qs.config +-import qs.utils +-import Caelestia ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick ++import Caelestia ++import Caelestia.Config ++import qs.services ++import qs.utils + + Singleton { + id: root +@@ -78,6 +79,21 @@ Singleton { + Quickshell.execDetached(["caelestia", "scheme", "set", "--notify", "-m", mode]); + } + ++ function reloadHyprRules(): void { ++ const str = "keyword layerrule %1 %2, match:namespace caelestia-drawers"; ++ Hypr.extras.batchMessage([str.arg("blur").arg(transparency.enabled ? 1 : 0), str.arg("ignore_alpha").arg(transparency.base - 0.03)]); ++ } ++ ++ Component.onCompleted: debounceTimer.triggered() ++ ++ Connections { ++ function onConfigReloaded(): void { ++ root.reloadHyprRules(); ++ } ++ ++ target: Hypr ++ } ++ + FileView { + path: `${Paths.state}/scheme.json` + watchChanges: true +@@ -91,10 +107,20 @@ Singleton { + source: Wallpapers.current + } + ++ Timer { ++ id: debounceTimer ++ ++ interval: 300 ++ onTriggered: root.reloadHyprRules() ++ } ++ + component Transparency: QtObject { +- readonly property bool enabled: Appearance.transparency.enabled +- readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0) +- readonly property real layers: Appearance.transparency.layers ++ readonly property bool enabled: Tokens.transparency.enabled ++ readonly property real base: Math.max(0, Math.min(1, Tokens.transparency.base - (root.light ? 0.1 : 0))) ++ readonly property real layers: Tokens.transparency.layers ++ ++ onEnabledChanged: debounceTimer.restart() ++ onBaseChanged: debounceTimer.restart() + } + + component M3TPalette: QtObject { +diff --git a/services/GameMode.qml b/services/GameMode.qml +index 83770b79..6dfc791c 100644 +--- a/services/GameMode.qml ++++ b/services/GameMode.qml +@@ -1,11 +1,11 @@ + pragma Singleton + +-import qs.services +-import qs.config +-import Caelestia ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick ++import Caelestia ++import Caelestia.Config ++import qs.services + + Singleton { + id: root +@@ -28,11 +28,11 @@ Singleton { + onEnabledChanged: { + if (enabled) { + setDynamicConfs(); +- if (Config.utilities.toasts.gameModeChanged) ++ if (GlobalConfig.utilities.toasts.gameModeChanged) + Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad"); + } else { + Hypr.extras.message("reload"); +- if (Config.utilities.toasts.gameModeChanged) ++ if (GlobalConfig.utilities.toasts.gameModeChanged) + Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad"); + } + } +@@ -40,23 +40,21 @@ Singleton { + PersistentProperties { + id: props + +- property bool enabled: Hypr.options["animations:enabled"] === 0 ++ property bool enabled: Hypr.options["animations:enabled"] === 0 // qmllint disable missing-property + + reloadableId: "gameMode" + } + + Connections { +- target: Hypr +- + function onConfigReloaded(): void { + if (props.enabled) + root.setDynamicConfs(); + } ++ ++ target: Hypr + } + + IpcHandler { +- target: "gameMode" +- + function isEnabled(): bool { + return props.enabled; + } +@@ -72,5 +70,7 @@ Singleton { + function disable(): void { + props.enabled = false; + } ++ ++ target: "gameMode" + } + } +diff --git a/services/Hypr.qml b/services/Hypr.qml +index a654fdd3..181967e7 100644 +--- a/services/Hypr.qml ++++ b/services/Hypr.qml +@@ -1,13 +1,13 @@ + pragma Singleton + +-import qs.components.misc +-import qs.config +-import Caelestia +-import Caelestia.Internal ++import QtQuick + import Quickshell + import Quickshell.Hyprland + import Quickshell.Io +-import QtQuick ++import Caelestia ++import Caelestia.Config ++import Caelestia.Internal ++import qs.components.misc + + Singleton { + id: root +@@ -16,7 +16,10 @@ Singleton { + readonly property var workspaces: Hyprland.workspaces + readonly property var monitors: Hyprland.monitors + +- readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null ++ readonly property HyprlandToplevel activeToplevel: { ++ const t = Hyprland.activeToplevel; ++ return t?.workspace?.name.startsWith("special:") || Hyprland.focusedWorkspace?.toplevels.values.length > 0 ? t : null; ++ } + readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace + readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor + readonly property int activeWsId: focusedWorkspace?.id ?? 1 +@@ -34,6 +37,7 @@ Singleton { + readonly property alias devices: extras.devices + + property bool hadKeyboard ++ property string lastSpecialWorkspace: "" + + signal configReloaded + +@@ -41,6 +45,43 @@ Singleton { + Hyprland.dispatch(request); + } + ++ function cycleSpecialWorkspace(direction: string): void { ++ const openSpecials = workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0); ++ ++ if (openSpecials.length === 0) ++ return; ++ ++ const activeSpecial = focusedMonitor.lastIpcObject.specialWorkspace.name ?? ""; ++ ++ if (!activeSpecial) { ++ if (lastSpecialWorkspace) { ++ const workspace = workspaces.values.find(w => w.name === lastSpecialWorkspace); ++ if (workspace && workspace.lastIpcObject.windows > 0) { ++ dispatch(`workspace ${lastSpecialWorkspace}`); ++ return; ++ } ++ } ++ dispatch(`workspace ${openSpecials[0].name}`); ++ return; ++ } ++ ++ const currentIndex = openSpecials.findIndex(w => w.name === activeSpecial); ++ let nextIndex = 0; ++ ++ if (currentIndex !== -1) { ++ if (direction === "next") ++ nextIndex = (currentIndex + 1) % openSpecials.length; ++ else ++ nextIndex = (currentIndex - 1 + openSpecials.length) % openSpecials.length; ++ } ++ ++ dispatch(`workspace ${openSpecials[nextIndex].name}`); ++ } ++ ++ function monitorNames(): list { ++ return monitors.values.map(e => e.name); ++ } ++ + function monitorFor(screen: ShellScreen): HyprlandMonitor { + return Hyprland.monitorFor(screen); + } +@@ -52,7 +93,7 @@ Singleton { + Component.onCompleted: reloadDynamicConfs() + + onCapsLockChanged: { +- if (!Config.utilities.toasts.capsLockChanged) ++ if (!GlobalConfig.utilities.toasts.capsLockChanged) + return; + + if (capsLock) +@@ -62,7 +103,7 @@ Singleton { + } + + onNumLockChanged: { +- if (!Config.utilities.toasts.numLockChanged) ++ if (!GlobalConfig.utilities.toasts.numLockChanged) + return; + + if (numLock) +@@ -72,15 +113,13 @@ Singleton { + } + + onKbLayoutFullChanged: { +- if (hadKeyboard && Config.utilities.toasts.kbLayoutChanged) ++ if (hadKeyboard && GlobalConfig.utilities.toasts.kbLayoutChanged) + Toaster.toast(qsTr("Keyboard layout changed"), qsTr("Layout changed to: %1").arg(kbLayoutFull), "keyboard"); + + hadKeyboard = !!keyboard; + } + + Connections { +- target: Hyprland +- + function onRawEvent(event: HyprlandEvent): void { + const n = event.name; + if (n.endsWith("v2")) +@@ -103,6 +142,20 @@ Singleton { + Hyprland.refreshToplevels(); + } + } ++ ++ target: Hyprland ++ } ++ ++ Connections { ++ function onLastIpcObjectChanged(): void { ++ const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; ++ ++ if (specialName && specialName.startsWith("special:")) { ++ root.lastSpecialWorkspace = specialName; ++ } ++ } ++ ++ target: root.focusedMonitor + } + + FileView { +@@ -139,14 +192,24 @@ Singleton { + } + + IpcHandler { +- target: "hypr" +- + function refreshDevices(): void { + extras.refreshDevices(); + } ++ ++ function cycleSpecialWorkspace(direction: string): void { ++ root.cycleSpecialWorkspace(direction); ++ } ++ ++ function listSpecialWorkspaces(): string { ++ return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); ++ } ++ ++ target: "hypr" + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "refreshDevices" + description: "Reload devices" + onPressed: extras.refreshDevices() +diff --git a/services/IdleInhibitor.qml b/services/IdleInhibitor.qml +index 29409abc..9f556b3a 100644 +--- a/services/IdleInhibitor.qml ++++ b/services/IdleInhibitor.qml +@@ -35,8 +35,6 @@ Singleton { + } + + IpcHandler { +- target: "idleInhibitor" +- + function isEnabled(): bool { + return props.enabled; + } +@@ -52,5 +50,7 @@ Singleton { + function disable(): void { + props.enabled = false; + } ++ ++ target: "idleInhibitor" + } + } +diff --git a/services/LyricsService.qml b/services/LyricsService.qml +new file mode 100644 +index 00000000..26a75e9b +--- /dev/null ++++ b/services/LyricsService.qml +@@ -0,0 +1,403 @@ ++pragma Singleton ++ ++import "../utils/scripts/lrcparser.js" as Lrc ++import QtQuick ++import Quickshell ++import Quickshell.Io ++import Caelestia ++import Caelestia.Config ++import qs.utils ++ ++Singleton { ++ id: root ++ ++ property var player: Players.active ++ property int currentIndex: -1 ++ property bool loading: false ++ property bool isManualSeeking: false ++ property bool lyricsVisible: GlobalConfig.services.showLyrics ++ property string backend: "Local" ++ property string preferredBackend: GlobalConfig.services.lyricsBackend ++ property real currentSongId: 0 ++ property string loadedLocalFile: "" ++ property real offset ++ property int currentRequestId: 0 ++ property var lyricsMap: ({}) ++ ++ readonly property string lyricsDir: Paths.absolutePath(GlobalConfig.paths.lyricsDir) ++ readonly property string lyricsMapFile: lyricsDir + "/lyrics_map.json" ++ readonly property alias model: lyricsModel ++ readonly property alias candidatesModel: fetchedCandidatesModel ++ readonly property var _netEaseHeaders: ({ ++ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", ++ "Referer": "https://music.163.com/" ++ }) ++ ++ function getMetadata() { ++ if (!player || !player.metadata) ++ return null; ++ let artist = player.metadata["xesam:artist"]; ++ const title = player.metadata["xesam:title"]; ++ if (Array.isArray(artist)) ++ artist = artist.join(", "); ++ return { ++ artist: artist || "Unknown", ++ title: title || "Unknown" ++ }; ++ } ++ ++ function _metaKey(meta) { ++ return `${meta.artist} - ${meta.title}`; ++ } ++ ++ function savePrefs() { ++ let meta = getMetadata(); ++ if (!meta) ++ return; ++ let key = _metaKey(meta); ++ let existing = root.lyricsMap[key] ?? {}; ++ root.lyricsMap[key] = { ++ offset: root.offset, ++ backend: root.backend, ++ neteaseId: existing.neteaseId ?? null ++ }; ++ // reassign to notify QML bindings of the map change ++ root.lyricsMap = root.lyricsMap; ++ saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; ++ saveLyricsMap.running = true; ++ } ++ ++ function toggleVisibility() { ++ GlobalConfig.services.showLyrics = !GlobalConfig.services.showLyrics; ++ } ++ ++ function loadLyrics() { ++ loadDebounce.restart(); ++ } ++ ++ function _doLoadLyrics() { ++ const meta = getMetadata(); ++ if (!meta) { ++ lyricsModel.clear(); ++ root.currentIndex = -1; ++ return; ++ } ++ ++ loading = true; ++ lyricsModel.clear(); ++ currentIndex = -1; ++ root.currentSongId = 0; ++ ++ root.currentRequestId++; ++ let requestId = root.currentRequestId; ++ ++ let key = _metaKey(meta); ++ let saved = root.lyricsMap[key]; ++ root.offset = saved?.offset ?? 0.0; ++ ++ if (root.preferredBackend === "NetEase") { ++ root.backend = "NetEase"; ++ fetchNetEase(meta.title, meta.artist, requestId); ++ return; ++ } ++ ++ if (root.preferredBackend === "Local") { ++ root.backend = "Local"; ++ let cleanDir = lyricsDir.replace(/\/$/, ""); ++ let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; ++ ++ // Search for files matching "Artist - Title.lrc" pattern ++ const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); ++ const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); ++ const escapedTitle = titleStr.replace(/'/g, "'\\''"); ++ const escapedArtist = artistStr.replace(/'/g, "'\\''"); ++ findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; ++ findLyricsInSubdirs.requestId = requestId; ++ findLyricsInSubdirs.running = true; ++ ++ lrcFile.path = ""; ++ lrcFile.path = flatPath; ++ return; ++ } ++ ++ // Auto mode: try local first ++ root.backend = "Local"; ++ let cleanDir = lyricsDir.replace(/\/$/, ""); ++ let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; ++ ++ const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); ++ const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); ++ const escapedTitle = titleStr.replace(/'/g, "'\\''"); ++ const escapedArtist = artistStr.replace(/'/g, "'\\''"); ++ findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; ++ findLyricsInSubdirs.requestId = requestId; ++ findLyricsInSubdirs.running = true; ++ ++ lrcFile.path = ""; ++ lrcFile.path = flatPath; ++ fetchNetEaseCandidates(meta.title, meta.artist, requestId); ++ } ++ ++ function updateModel(parsedArray) { ++ root.currentIndex = -1; ++ lyricsModel.clear(); ++ for (let line of parsedArray) { ++ lyricsModel.append({ ++ time: line.time, ++ lyricLine: line.text ++ }); ++ } ++ } ++ ++ function fallbackToOnline() { ++ let meta = getMetadata(); ++ if (!meta) ++ return; ++ fetchNetEase(meta.title, meta.artist, root.currentRequestId); ++ } ++ ++ // NetEase ++ ++ // searches NetEase and populates the candidates model. returns the result array via the onResults callback ++ function _searchNetEase(title, artist, reqId, onResults) { ++ Requests.resetCookies(); ++ const query = encodeURIComponent(`${title} ${artist}`); ++ const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; ++ ++ Requests.get(url, text => { ++ if (reqId !== root.currentRequestId) ++ return; ++ const res = JSON.parse(text); ++ const songs = res.result?.songs || []; ++ ++ fetchedCandidatesModel.clear(); ++ for (let s of songs) { ++ fetchedCandidatesModel.append({ ++ id: s.id, ++ title: s.name || "Unknown Title", ++ artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" ++ }); ++ } ++ ++ onResults(songs); ++ }, err => {}, root._netEaseHeaders); ++ } ++ ++ // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. ++ function fetchNetEaseCandidates(title, artist, reqId) { ++ _searchNetEase(title, artist, reqId, _songs => {}); ++ } ++ ++ // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. ++ function fetchNetEase(title, artist, reqId) { ++ _searchNetEase(title, artist, reqId, songs => { ++ const bestMatch = songs.find(s => { ++ const inputArtist = String(artist || "").toLowerCase(); ++ const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); ++ return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); ++ }); ++ ++ if (!bestMatch) { ++ return; // No reliable lyrics found ++ } ++ ++ let key = `${artist} - ${title}`; ++ root.lyricsMap[key] = { ++ offset: root.lyricsMap[key]?.offset ?? 0.0, ++ backend: "NetEase", ++ neteaseId: bestMatch.id ++ }; ++ root.currentSongId = bestMatch.id; ++ savePrefs(); ++ fetchNetEaseLyrics(bestMatch.id, reqId); ++ }); ++ } ++ ++ function fetchNetEaseLyrics(id, reqId) { ++ const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; ++ Requests.get(url, text => { ++ if (reqId !== root.currentRequestId) ++ return; ++ const res = JSON.parse(text); ++ if (res.lrc?.lyric) { ++ updateModel(Lrc.parseLrc(res.lrc.lyric)); ++ loading = false; ++ } ++ }); ++ } ++ ++ function selectCandidate(songId) { ++ let meta = getMetadata(); ++ if (!meta) ++ return; ++ root.backend = "NetEase"; ++ root.currentSongId = songId; ++ let key = _metaKey(meta); ++ root.lyricsMap[key] = { ++ offset: root.lyricsMap[key]?.offset ?? 0.0, ++ neteaseId: songId ++ }; ++ savePrefs(); ++ fetchNetEaseLyrics(songId, currentRequestId); ++ } ++ ++ function updatePosition() { ++ if (isManualSeeking || loading || !player || lyricsModel.count === 0) ++ return; ++ ++ let pos = player.position - root.offset; ++ let newIdx = -1; ++ for (let i = lyricsModel.count - 1; i >= 0; i--) { ++ if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor ++ newIdx = i; ++ break; ++ } ++ } ++ ++ if (newIdx !== currentIndex) { ++ root.currentIndex = newIdx; ++ } ++ } ++ ++ function jumpTo(index, time) { ++ root.isManualSeeking = true; ++ root.currentIndex = index; ++ ++ if (player) { ++ player.position = time + root.offset + 0.01; // compensate for rounding ++ } ++ ++ seekTimer.restart(); ++ } ++ ++ onPreferredBackendChanged: { ++ if (GlobalConfig.services.lyricsBackend !== preferredBackend) { ++ GlobalConfig.services.lyricsBackend = preferredBackend; ++ } ++ } ++ ++ ListModel { ++ id: lyricsModel ++ } ++ ++ ListModel { ++ id: fetchedCandidatesModel ++ } ++ ++ Timer { ++ id: seekTimer ++ ++ interval: 500 ++ onTriggered: root.isManualSeeking = false ++ } ++ ++ // If no local lyrics were loaded within the interval, fall back to NetEase ++ Timer { ++ id: fallbackTimer ++ ++ interval: 200 ++ onTriggered: { ++ if (lyricsModel.count === 0) { ++ root.backend = "NetEase"; ++ fallbackToOnline(); ++ } ++ } ++ } ++ ++ Timer { ++ id: loadDebounce ++ ++ interval: 50 ++ onTriggered: root._doLoadLyrics() ++ } ++ ++ FileView { ++ id: lyricsMapFileView ++ ++ path: root.lyricsMapFile ++ printErrors: false ++ onLoaded: { ++ try { ++ root.lyricsMap = JSON.parse(text()); ++ } catch (e) { ++ root.lyricsMap = {}; ++ } ++ } ++ } ++ ++ FileView { ++ id: lrcFile ++ ++ printErrors: false ++ onLoaded: { ++ fallbackTimer.stop(); ++ let parsed = Lrc.parseLrc(text()); ++ if (parsed.length > 0) { ++ root.backend = "Local"; ++ root.loadedLocalFile = path; ++ updateModel(parsed); ++ loading = false; ++ } else if (root.preferredBackend === "Local") { ++ // Local mode only - fail immediately ++ root.backend = "NetEase"; ++ fallbackToOnline(); ++ } ++ // In Auto mode, let the Process onExited handle fallback ++ } ++ } ++ ++ Connections { ++ function onActiveChanged() { ++ root.player = Players.active; ++ loadLyrics(); ++ } ++ ++ target: Players ++ } ++ ++ Connections { ++ function onMetadataChanged() { ++ loadLyrics(); ++ } ++ ++ target: root.player ++ ignoreUnknownSignals: true ++ } ++ ++ Process { ++ id: saveLyricsMap ++ ++ command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] ++ } ++ ++ Process { ++ id: findLyricsInSubdirs ++ ++ property int requestId: -1 ++ property bool foundFile: false ++ ++ stdout: SplitParser { ++ onRead: data => { ++ if (findLyricsInSubdirs.requestId === root.currentRequestId) { ++ const foundPath = data.trim(); ++ if (foundPath && foundPath.length > 0) { ++ findLyricsInSubdirs.foundFile = true; ++ fallbackTimer.stop(); ++ root.loadedLocalFile = foundPath; ++ lrcFile.path = ""; ++ lrcFile.path = foundPath; ++ } ++ } ++ } ++ } ++ ++ onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters ++ if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") { ++ if (lyricsModel.count === 0) { ++ fallbackTimer.restart(); ++ } ++ } ++ foundFile = false; ++ } ++ } ++} +diff --git a/services/Monitors.qml b/services/Monitors.qml +index e64d14e5..c880ada5 100644 +--- a/services/Monitors.qml ++++ b/services/Monitors.qml +@@ -29,29 +29,37 @@ Singleton { + identifyTimer.stop(); + } + +- // Safely iterate UntypedObjectModel — .find() doesn't work on it ++ function sourceMonitors(): var { ++ if ((Hyprctl.monitors?.length ?? 0) > 0) ++ return Hyprctl.monitors; ++ return Hypr.monitors.values ?? []; ++ } ++ ++ // Safely iterate monitor data — .find() doesn't work on UntypedObjectModel + function findMonitorByName(name: string): var { +- for (let i = 0; i < Hypr.monitors.length; i++) { +- if (Hypr.monitors[i].name === name) +- return Hypr.monitors[i]; ++ const monitors = sourceMonitors(); ++ for (let i = 0; i < monitors.length; i++) { ++ if (monitors[i].name === name) ++ return monitors[i]; + } + return null; + } + + function findMonitorById(id: int): var { +- for (let i = 0; i < Hypr.monitors.length; i++) { +- if (Hypr.monitors[i].id === id) +- return Hypr.monitors[i]; ++ const monitors = sourceMonitors(); ++ for (let i = 0; i < monitors.length; i++) { ++ if (monitors[i].id === id) ++ return monitors[i]; + } + return null; + } + + // Build the monitor string Hyprland expects: + // NAME,WIDTHxHEIGHT@RATE,XxY,SCALE[,transform,N] +- function monitorStr(mon: var, overrideScale: real, overrideTransform: int): string { +- const scale = overrideScale >= 0 ? overrideScale : (mon.scale || 1); ++ function monitorStr(mon: var, overrideScale: real, overrideTransform: int, overrideRefreshRate: real): string { ++ const scale = overrideScale >= 0 ? overrideScale : (mon.scale || 1); + const transform = overrideTransform >= 0 ? overrideTransform : (mon.transform || 0); +- const rr = (mon.refreshRate || 60).toFixed(3); ++ const rr = (overrideRefreshRate > 0 ? overrideRefreshRate : (mon.refreshRate || 60)).toFixed(3); + let s = `${mon.name},${mon.width}x${mon.height}@${rr},${mon.x}x${mon.y},${scale}`; + if (transform !== 0) + s += `,transform,${transform}`; +@@ -62,6 +70,7 @@ Singleton { + // "keyword" is a config command, not a dispatcher action. + function sendKeyword(monStr: string): void { + Hypr.extras.batchMessage([`keyword monitor ${monStr}`]); ++ Hyprctl.update(); + } + + function arrange(monitorName: string, pos: string, relativeToId: int): void { +@@ -82,7 +91,7 @@ Singleton { + else if (pos === "top") y -= movingH; + else if (pos === "bottom") y += targetH; + +- sendKeyword(monitorStr(moving, moving.scale || 1, moving.transform || 0) ++ sendKeyword(monitorStr(moving, moving.scale || 1, moving.transform || 0, moving.refreshRate || 60) + .replace(`${moving.x}x${moving.y}`, `${Math.round(x)}x${Math.round(y)}`)); + } + +@@ -95,13 +104,19 @@ Singleton { + else if (angle === 180) transform = 2; + else if (angle === 270) transform = 3; + +- sendKeyword(monitorStr(mon, mon.scale || 1, transform)); ++ sendKeyword(monitorStr(mon, mon.scale || 1, transform, mon.refreshRate || 60)); + } + + function setScale(monitorName: string, scale: real): void { + const mon = findMonitorByName(monitorName); + if (!mon) return; + const s = Math.max(0.5, Math.min(3.0, scale)); +- sendKeyword(monitorStr(mon, s, mon.transform || 0)); ++ sendKeyword(monitorStr(mon, s, mon.transform || 0, mon.refreshRate || 60)); ++ } ++ ++ function setRefreshRate(monitorName: string, refreshRate: real): void { ++ const mon = findMonitorByName(monitorName); ++ if (!mon) return; ++ sendKeyword(monitorStr(mon, mon.scale || 1, mon.transform || 0, Math.max(1, refreshRate))); + } + } +diff --git a/services/Network.qml b/services/Network.qml +index f3dfc3ea..4e0b809c 100644 +--- a/services/Network.qml ++++ b/services/Network.qml +@@ -1,44 +1,28 @@ + pragma Singleton + ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick + import qs.services + + Singleton { + id: root + +- Component.onCompleted: { +- // Trigger ethernet device detection after initialization +- Qt.callLater(() => { +- getEthernetDevices(); +- }); +- // Load saved connections on startup +- Nmcli.loadSavedConnections(() => { +- root.savedConnections = Nmcli.savedConnections; +- root.savedConnectionSsids = Nmcli.savedConnectionSsids; +- }); +- // Get initial WiFi status +- Nmcli.getWifiStatus(enabled => { +- root.wifiEnabled = enabled; +- }); +- // Sync networks from Nmcli on startup +- Qt.callLater(() => { +- syncNetworksFromNmcli(); +- }, 100); +- } +- + readonly property list networks: [] + readonly property AccessPoint active: networks.find(n => n.active) ?? null + property bool wifiEnabled: true + readonly property bool scanning: Nmcli.scanning +- + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null + property int ethernetDeviceCount: 0 + property bool ethernetProcessRunning: false + property var ethernetDeviceDetails: null + property var wirelessDeviceDetails: null ++ property var pendingConnection: null ++ property list savedConnections: [] ++ property list savedConnectionSsids: [] ++ ++ signal connectionFailed(string ssid) + + function enableWifi(enabled: bool): void { + Nmcli.enableWifi(enabled, result => { +@@ -66,9 +50,6 @@ Singleton { + Nmcli.rescanWifi(); + } + +- property var pendingConnection: null +- signal connectionFailed(string ssid) +- + function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { + // Set up pending connection tracking if callback provided + if (callback) { +@@ -159,20 +140,6 @@ Singleton { + }); + } + +- property list savedConnections: [] +- property list savedConnectionSsids: [] +- +- // Sync saved connections from Nmcli when they're updated +- Connections { +- target: Nmcli +- function onSavedConnectionsChanged() { +- root.savedConnections = Nmcli.savedConnections; +- } +- function onSavedConnectionSsidsChanged() { +- root.savedConnectionSsids = Nmcli.savedConnectionSsids; +- } +- } +- + function syncNetworksFromNmcli(): void { + const rNetworks = root.networks; + const nNetworks = Nmcli.networks; +@@ -217,22 +184,6 @@ Singleton { + } + } + +- component AccessPoint: QtObject { +- required property var lastIpcObject +- readonly property string ssid: lastIpcObject.ssid +- readonly property string bssid: lastIpcObject.bssid +- readonly property int strength: lastIpcObject.strength +- readonly property int frequency: lastIpcObject.frequency +- readonly property bool active: lastIpcObject.active +- readonly property string security: lastIpcObject.security +- readonly property bool isSecure: security.length > 0 +- } +- +- Component { +- id: apComp +- AccessPoint {} +- } +- + function hasSavedProfile(ssid: string): bool { + // Use Nmcli's hasSavedProfile which has the same logic + return Nmcli.hasSavedProfile(ssid); +@@ -309,16 +260,73 @@ Singleton { + return octets.join("."); + } + ++ Component.onCompleted: { ++ // Trigger ethernet device detection after initialization ++ Qt.callLater(() => { ++ getEthernetDevices(); ++ }); ++ // Load saved connections on startup ++ Nmcli.loadSavedConnections(() => { ++ root.savedConnections = Nmcli.savedConnections; ++ root.savedConnectionSsids = Nmcli.savedConnectionSsids; ++ }); ++ // Get initial WiFi status ++ Nmcli.getWifiStatus(enabled => { ++ root.wifiEnabled = enabled; ++ }); ++ // Sync networks from Nmcli on startup ++ Qt.callLater(() => { ++ syncNetworksFromNmcli(); ++ }, 100); ++ } ++ ++ // Sync saved connections from Nmcli when they're updated ++ Connections { ++ function onSavedConnectionsChanged() { ++ root.savedConnections = Nmcli.savedConnections; ++ } ++ ++ function onSavedConnectionSsidsChanged() { ++ root.savedConnectionSsids = Nmcli.savedConnectionSsids; ++ } ++ ++ target: Nmcli ++ } ++ ++ Timer { ++ id: monitorDebounce ++ ++ interval: 200 ++ onTriggered: { ++ Nmcli.getNetworks(() => { ++ syncNetworksFromNmcli(); ++ }); ++ getEthernetDevices(); ++ } ++ } ++ + Process { + running: true + command: ["nmcli", "m"] + stdout: SplitParser { +- onRead: { +- Nmcli.getNetworks(() => { +- syncNetworksFromNmcli(); +- }); +- getEthernetDevices(); +- } ++ onRead: monitorDebounce.start() + } + } ++ ++ Component { ++ id: apComp ++ ++ AccessPoint {} ++ } ++ ++ component AccessPoint: QtObject { ++ required property var lastIpcObject ++ readonly property string ssid: lastIpcObject.ssid ++ readonly property string bssid: lastIpcObject.bssid ++ readonly property int strength: lastIpcObject.strength ++ readonly property int frequency: lastIpcObject.frequency ++ readonly property bool active: lastIpcObject.active ++ readonly property string security: lastIpcObject.security ++ readonly property bool isSecure: security.length > 0 ++ } + } +diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml +new file mode 100644 +index 00000000..6c4dc8d2 +--- /dev/null ++++ b/services/NetworkUsage.qml +@@ -0,0 +1,229 @@ ++pragma Singleton ++ ++import QtQuick ++import Quickshell ++import Quickshell.Io ++import Caelestia.Config ++import Caelestia.Internal ++ ++Singleton { ++ id: root ++ ++ property int refCount: 0 ++ ++ // Current speeds in bytes per second ++ readonly property real downloadSpeed: _downloadSpeed ++ readonly property real uploadSpeed: _uploadSpeed ++ ++ // Total bytes transferred since tracking started ++ readonly property real downloadTotal: _downloadTotal ++ readonly property real uploadTotal: _uploadTotal ++ ++ // History buffers for sparkline ++ readonly property CircularBuffer downloadBuffer: _downloadBuffer ++ readonly property CircularBuffer uploadBuffer: _uploadBuffer ++ readonly property int historyLength: 30 ++ ++ // Private properties ++ property real _downloadSpeed: 0 ++ property real _uploadSpeed: 0 ++ property real _downloadTotal: 0 ++ property real _uploadTotal: 0 ++ ++ // Previous readings for calculating speed ++ property real _prevRxBytes: 0 ++ property real _prevTxBytes: 0 ++ property real _prevTimestamp: 0 ++ ++ // Initial readings for calculating totals ++ property real _initialRxBytes: 0 ++ property real _initialTxBytes: 0 ++ property bool _initialized: false ++ ++ function formatBytes(bytes: real): var { ++ // Handle negative or invalid values ++ if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { ++ return { ++ value: 0, ++ unit: "B/s" ++ }; ++ } ++ ++ if (bytes < 1024) { ++ return { ++ value: bytes, ++ unit: "B/s" ++ }; ++ } else if (bytes < 1024 * 1024) { ++ return { ++ value: bytes / 1024, ++ unit: "KB/s" ++ }; ++ } else if (bytes < 1024 * 1024 * 1024) { ++ return { ++ value: bytes / (1024 * 1024), ++ unit: "MB/s" ++ }; ++ } else { ++ return { ++ value: bytes / (1024 * 1024 * 1024), ++ unit: "GB/s" ++ }; ++ } ++ } ++ ++ function formatBytesTotal(bytes: real): var { ++ // Handle negative or invalid values ++ if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { ++ return { ++ value: 0, ++ unit: "B" ++ }; ++ } ++ ++ if (bytes < 1024) { ++ return { ++ value: bytes, ++ unit: "B" ++ }; ++ } else if (bytes < 1024 * 1024) { ++ return { ++ value: bytes / 1024, ++ unit: "KB" ++ }; ++ } else if (bytes < 1024 * 1024 * 1024) { ++ return { ++ value: bytes / (1024 * 1024), ++ unit: "MB" ++ }; ++ } else { ++ return { ++ value: bytes / (1024 * 1024 * 1024), ++ unit: "GB" ++ }; ++ } ++ } ++ ++ function parseNetDev(content: string): var { ++ const lines = content.split("\n"); ++ let totalRx = 0; ++ let totalTx = 0; ++ ++ for (let i = 2; i < lines.length; i++) { ++ const line = lines[i].trim(); ++ if (!line) ++ continue; ++ ++ const parts = line.split(/\s+/); ++ if (parts.length < 10) ++ continue; ++ ++ const iface = parts[0].replace(":", ""); ++ // Skip loopback interface ++ if (iface === "lo") ++ continue; ++ ++ const rxBytes = parseFloat(parts[1]) || 0; ++ const txBytes = parseFloat(parts[9]) || 0; ++ ++ totalRx += rxBytes; ++ totalTx += txBytes; ++ } ++ ++ return { ++ rx: totalRx, ++ tx: totalTx ++ }; ++ } ++ ++ CircularBuffer { ++ id: _downloadBuffer ++ ++ capacity: root.historyLength + 1 ++ } ++ ++ CircularBuffer { ++ id: _uploadBuffer ++ ++ capacity: root.historyLength + 1 ++ } ++ ++ FileView { ++ id: netDevFile ++ ++ path: "/proc/net/dev" ++ } ++ ++ Timer { ++ interval: GlobalConfig.dashboard.resourceUpdateInterval ++ running: root.refCount > 0 ++ repeat: true ++ triggeredOnStart: true ++ ++ onTriggered: { ++ netDevFile.reload(); ++ const content = netDevFile.text(); ++ if (!content) ++ return; ++ ++ const data = root.parseNetDev(content); ++ const now = Date.now(); ++ ++ if (!root._initialized) { ++ root._initialRxBytes = data.rx; ++ root._initialTxBytes = data.tx; ++ root._prevRxBytes = data.rx; ++ root._prevTxBytes = data.tx; ++ root._prevTimestamp = now; ++ root._initialized = true; ++ return; ++ } ++ ++ const timeDelta = (now - root._prevTimestamp) / 1000; // seconds ++ if (timeDelta > 0) { ++ // Calculate byte deltas ++ let rxDelta = data.rx - root._prevRxBytes; ++ let txDelta = data.tx - root._prevTxBytes; ++ ++ // Handle counter overflow (when counters wrap around from max to 0) ++ // This happens when counters exceed 32-bit or 64-bit limits ++ if (rxDelta < 0) { ++ // Counter wrapped around - assume 64-bit counter ++ rxDelta += Math.pow(2, 64); ++ } ++ if (txDelta < 0) { ++ txDelta += Math.pow(2, 64); ++ } ++ ++ // Calculate speeds ++ root._downloadSpeed = rxDelta / timeDelta; ++ root._uploadSpeed = txDelta / timeDelta; ++ ++ if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) ++ _downloadBuffer.push(root._downloadSpeed); ++ ++ if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) ++ _uploadBuffer.push(root._uploadSpeed); ++ } ++ ++ // Calculate totals with overflow handling ++ let downTotal = data.rx - root._initialRxBytes; ++ let upTotal = data.tx - root._initialTxBytes; ++ ++ // Handle counter overflow for totals ++ if (downTotal < 0) { ++ downTotal += Math.pow(2, 64); ++ } ++ if (upTotal < 0) { ++ upTotal += Math.pow(2, 64); ++ } ++ ++ root._downloadTotal = downTotal; ++ root._uploadTotal = upTotal; ++ ++ root._prevRxBytes = data.rx; ++ root._prevTxBytes = data.tx; ++ root._prevTimestamp = now; ++ } ++ } ++} +diff --git a/services/Nmcli.qml b/services/Nmcli.qml +index 36bd3e6d..ea5d7eb3 100644 +--- a/services/Nmcli.qml ++++ b/services/Nmcli.qml +@@ -1,9 +1,9 @@ + pragma Singleton + pragma ComponentBehavior: Bound + ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick + + Singleton { + id: root +@@ -24,14 +24,15 @@ Singleton { + property var wifiConnectionQueue: [] + property int currentSsidQueryIndex: 0 + property var pendingConnection: null +- signal connectionFailed(string ssid) + property var wirelessDeviceDetails: null + property var ethernetDeviceDetails: null + property list ethernetDevices: [] + readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null +- + property list activeProcesses: [] + ++ readonly property alias connectionCheckTimer: connectionCheckTimer ++ readonly property alias immediateCheckTimer: immediateCheckTimer ++ + // Constants + readonly property string deviceTypeWifi: "wifi" + readonly property string deviceTypeEthernet: "ethernet" +@@ -55,6 +56,8 @@ Singleton { + readonly property string connectionParamPassword: "password" + readonly property string connectionParamBssid: "802-11-wireless.bssid" + ++ signal connectionFailed(string ssid) ++ + function detectPasswordRequired(error: string): bool { + if (!error || error.length === 0) { + return false; +@@ -165,7 +168,7 @@ Singleton { + + function executeCommand(args: list, callback: var): void { + const proc = commandProc.createObject(root); +- proc.command = ["nmcli", ...args]; ++ proc.cmdArgs = ["nmcli", ...args]; + proc.callback = callback; + + activeProcesses.push(proc); +@@ -178,7 +181,7 @@ Singleton { + }); + + Qt.callLater(() => { +- proc.exec(proc.command); ++ proc.exec(proc.cmdArgs); + }); + } + +@@ -388,7 +391,7 @@ Singleton { + } + + if (!result.success && root.pendingConnection && retries < maxRetries) { +- console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); ++ console.warn(lc, "Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + Qt.callLater(() => { + connectWireless(ssid, password, bssid, callback, retries + 1); + }, 1000); +@@ -414,7 +417,7 @@ Singleton { + loadSavedConnections(() => {}); + activateConnection(ssid, callback); + } else { +- console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); ++ console.warn(lc, "Connection profile creation failed, trying fallback..."); + let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; + executeCommand(fallbackCmd, fallbackResult => { + if (callback) +@@ -751,17 +754,25 @@ Singleton { + const networks = deduplicateNetworks(allNetworks); + const rNetworks = root.networks; + +- const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); +- for (const network of destroyed) { +- const index = rNetworks.indexOf(network); +- if (index >= 0) { +- rNetworks.splice(index, 1); +- network.destroy(); ++ const newMap = new Map(); ++ for (const n of networks) ++ newMap.set(`${n.frequency}:${n.ssid}:${n.bssid}`, n); ++ ++ for (let i = rNetworks.length - 1; i >= 0; i--) { ++ const rn = rNetworks[i]; ++ const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; ++ if (!newMap.has(key)) { ++ rNetworks.splice(i, 1); ++ rn.destroy(); + } + } + +- for (const network of networks) { +- const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); ++ const existingMap = new Map(); ++ for (const rn of rNetworks) ++ existingMap.set(`${rn.frequency}:${rn.ssid}:${rn.bssid}`, rn); ++ ++ for (const [key, network] of newMap) { ++ const match = existingMap.get(key); + if (match) { + match.lastIpcObject = network; + } else { +@@ -829,7 +840,7 @@ Singleton { + return false; + } + +- if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { ++ if (!isConnectionCommand(proc.cmdArgs) || !root.pendingConnection || !root.pendingConnection.callback) { + return false; + } + +@@ -861,247 +872,6 @@ Singleton { + return false; + } + +- component CommandProcess: Process { +- id: proc +- +- property var callback: null +- property list command: [] +- property bool callbackCalled: false +- property int exitCode: 0 +- +- signal processFinished +- +- environment: ({ +- LANG: "C.UTF-8", +- LC_ALL: "C.UTF-8" +- }) +- +- stdout: StdioCollector { +- id: stdoutCollector +- } +- +- stderr: StdioCollector { +- id: stderrCollector +- +- onStreamFinished: { +- const error = text.trim(); +- if (error && error.length > 0) { +- const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; +- root.handlePasswordRequired(proc, error, output, -1); +- } +- } +- } +- +- onExited: code => { +- exitCode = code; +- +- Qt.callLater(() => { +- if (callbackCalled) { +- processFinished(); +- return; +- } +- +- if (proc.callback) { +- const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; +- const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; +- const success = exitCode === 0; +- const cmdIsConnection = isConnectionCommand(proc.command); +- +- if (root.handlePasswordRequired(proc, error, output, exitCode)) { +- processFinished(); +- return; +- } +- +- const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); +- +- if (!success && cmdIsConnection && root.pendingConnection) { +- const failedSsid = root.pendingConnection.ssid; +- root.connectionFailed(failedSsid); +- } +- +- callbackCalled = true; +- callback({ +- success: success, +- output: output, +- error: error, +- exitCode: proc.exitCode, +- needsPassword: needsPassword || false +- }); +- processFinished(); +- } else { +- processFinished(); +- } +- }); +- } +- } +- +- Component { +- id: commandProc +- +- CommandProcess {} +- } +- +- component AccessPoint: QtObject { +- required property var lastIpcObject +- readonly property string ssid: lastIpcObject.ssid +- readonly property string bssid: lastIpcObject.bssid +- readonly property int strength: lastIpcObject.strength +- readonly property int frequency: lastIpcObject.frequency +- readonly property bool active: lastIpcObject.active +- readonly property string security: lastIpcObject.security +- readonly property bool isSecure: security.length > 0 +- } +- +- Component { +- id: apComp +- +- AccessPoint {} +- } +- +- Timer { +- id: connectionCheckTimer +- +- interval: 4000 +- onTriggered: { +- if (root.pendingConnection) { +- const connected = root.active && root.active.ssid === root.pendingConnection.ssid; +- +- if (!connected && root.pendingConnection.callback) { +- let foundPasswordError = false; +- for (let i = 0; i < root.activeProcesses.length; i++) { +- const proc = root.activeProcesses[i]; +- if (proc && proc.stderr && proc.stderr.text) { +- const error = proc.stderr.text.trim(); +- if (error && error.length > 0) { +- if (root.isConnectionCommand(proc.command)) { +- const needsPassword = root.detectPasswordRequired(error); +- +- if (needsPassword && !proc.callbackCalled && root.pendingConnection) { +- const pending = root.pendingConnection; +- root.pendingConnection = null; +- immediateCheckTimer.stop(); +- immediateCheckTimer.checkCount = 0; +- proc.callbackCalled = true; +- const result = { +- success: false, +- output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", +- error: error, +- exitCode: -1, +- needsPassword: true +- }; +- if (pending.callback) { +- pending.callback(result); +- } +- if (proc.callback && proc.callback !== pending.callback) { +- proc.callback(result); +- } +- foundPasswordError = true; +- break; +- } +- } +- } +- } +- } +- +- if (!foundPasswordError) { +- const pending = root.pendingConnection; +- const failedSsid = pending.ssid; +- root.pendingConnection = null; +- immediateCheckTimer.stop(); +- immediateCheckTimer.checkCount = 0; +- root.connectionFailed(failedSsid); +- pending.callback({ +- success: false, +- output: "", +- error: "Connection timeout", +- exitCode: -1, +- needsPassword: false +- }); +- } +- } else if (connected) { +- root.pendingConnection = null; +- immediateCheckTimer.stop(); +- immediateCheckTimer.checkCount = 0; +- } +- } +- } +- } +- +- Timer { +- id: immediateCheckTimer +- +- property int checkCount: 0 +- +- interval: 500 +- repeat: true +- triggeredOnStart: false +- +- onTriggered: { +- if (root.pendingConnection) { +- checkCount++; +- const connected = root.active && root.active.ssid === root.pendingConnection.ssid; +- +- if (connected) { +- connectionCheckTimer.stop(); +- immediateCheckTimer.stop(); +- immediateCheckTimer.checkCount = 0; +- if (root.pendingConnection.callback) { +- root.pendingConnection.callback({ +- success: true, +- output: "Connected", +- error: "", +- exitCode: 0 +- }); +- } +- root.pendingConnection = null; +- } else { +- for (let i = 0; i < root.activeProcesses.length; i++) { +- const proc = root.activeProcesses[i]; +- if (proc && proc.stderr && proc.stderr.text) { +- const error = proc.stderr.text.trim(); +- if (error && error.length > 0) { +- if (root.isConnectionCommand(proc.command)) { +- const needsPassword = root.detectPasswordRequired(error); +- +- if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { +- connectionCheckTimer.stop(); +- immediateCheckTimer.stop(); +- immediateCheckTimer.checkCount = 0; +- const pending = root.pendingConnection; +- root.pendingConnection = null; +- proc.callbackCalled = true; +- const result = { +- success: false, +- output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", +- error: error, +- exitCode: -1, +- needsPassword: true +- }; +- if (pending.callback) { +- pending.callback(result); +- } +- if (proc.callback && proc.callback !== pending.callback) { +- proc.callback(result); +- } +- return; +- } +- } +- } +- } +- } +- +- if (checkCount >= 6) { +- immediateCheckTimer.stop(); +- immediateCheckTimer.checkCount = 0; +- } +- } +- } else { +- immediateCheckTimer.stop(); +- immediateCheckTimer.checkCount = 0; +- } +- } +- } +- + function checkPendingConnection(): void { + if (root.pendingConnection) { + Qt.callLater(() => { +@@ -1253,39 +1023,9 @@ Singleton { + return details; + } + +- Process { +- id: rescanProc +- +- command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] +- onExited: root.getNetworks() +- } +- +- Process { +- id: monitorProc +- +- running: true +- command: ["nmcli", "monitor"] +- environment: ({ +- LANG: "C.UTF-8", +- LC_ALL: "C.UTF-8" +- }) +- stdout: SplitParser { +- onRead: root.refreshOnConnectionChange() +- } +- onExited: monitorRestartTimer.start() +- } +- +- Timer { +- id: monitorRestartTimer +- interval: 2000 +- onTriggered: { +- monitorProc.running = true; +- } +- } +- +- function refreshOnConnectionChange(): void { +- getNetworks(networks => { +- const newActive = root.active; ++ function refreshOnConnectionChange(): void { ++ getNetworks(networks => { ++ const newActive = root.active; + + if (newActive && newActive.active) { + Qt.callLater(() => { +@@ -1349,4 +1089,283 @@ Singleton { + } + }, 2000); + } ++ ++ Component { ++ id: commandProc ++ ++ CommandProcess {} ++ } ++ ++ Component { ++ id: apComp ++ ++ AccessPoint {} ++ } ++ ++ Timer { ++ id: connectionCheckTimer ++ ++ interval: 4000 ++ onTriggered: { ++ if (root.pendingConnection) { ++ const connected = root.active && root.active.ssid === root.pendingConnection.ssid; ++ ++ if (!connected && root.pendingConnection.callback) { ++ let foundPasswordError = false; ++ for (let i = 0; i < root.activeProcesses.length; i++) { ++ const proc = root.activeProcesses[i]; ++ if (proc && proc.stderr && proc.stderr.text) { ++ const error = proc.stderr.text.trim(); ++ if (error && error.length > 0) { ++ if (root.isConnectionCommand(proc.cmdArgs)) { ++ const needsPassword = root.detectPasswordRequired(error); ++ ++ if (needsPassword && !proc.callbackCalled && root.pendingConnection) { ++ const pending = root.pendingConnection; ++ root.pendingConnection = null; ++ immediateCheckTimer.stop(); ++ immediateCheckTimer.checkCount = 0; ++ proc.callbackCalled = true; ++ const result = { ++ success: false, ++ output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", ++ error: error, ++ exitCode: -1, ++ needsPassword: true ++ }; ++ if (pending.callback) { ++ pending.callback(result); ++ } ++ if (proc.callback && proc.callback !== pending.callback) { ++ proc.callback(result); ++ } ++ foundPasswordError = true; ++ break; ++ } ++ } ++ } ++ } ++ } ++ ++ if (!foundPasswordError) { ++ const pending = root.pendingConnection; ++ const failedSsid = pending.ssid; ++ root.pendingConnection = null; ++ immediateCheckTimer.stop(); ++ immediateCheckTimer.checkCount = 0; ++ root.connectionFailed(failedSsid); ++ pending.callback({ ++ success: false, ++ output: "", ++ error: "Connection timeout", ++ exitCode: -1, ++ needsPassword: false ++ }); ++ } ++ } else if (connected) { ++ root.pendingConnection = null; ++ immediateCheckTimer.stop(); ++ immediateCheckTimer.checkCount = 0; ++ } ++ } ++ } ++ } ++ ++ Timer { ++ id: immediateCheckTimer ++ ++ property int checkCount: 0 ++ ++ interval: 500 ++ repeat: true ++ triggeredOnStart: false ++ ++ onTriggered: { ++ if (root.pendingConnection) { ++ checkCount++; ++ const connected = root.active && root.active.ssid === root.pendingConnection.ssid; ++ ++ if (connected) { ++ connectionCheckTimer.stop(); ++ immediateCheckTimer.stop(); ++ immediateCheckTimer.checkCount = 0; ++ if (root.pendingConnection.callback) { ++ root.pendingConnection.callback({ ++ success: true, ++ output: "Connected", ++ error: "", ++ exitCode: 0 ++ }); ++ } ++ root.pendingConnection = null; ++ } else { ++ for (let i = 0; i < root.activeProcesses.length; i++) { ++ const proc = root.activeProcesses[i]; ++ if (proc && proc.stderr && proc.stderr.text) { ++ const error = proc.stderr.text.trim(); ++ if (error && error.length > 0) { ++ if (root.isConnectionCommand(proc.cmdArgs)) { ++ const needsPassword = root.detectPasswordRequired(error); ++ ++ if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { ++ connectionCheckTimer.stop(); ++ immediateCheckTimer.stop(); ++ immediateCheckTimer.checkCount = 0; ++ const pending = root.pendingConnection; ++ root.pendingConnection = null; ++ proc.callbackCalled = true; ++ const result = { ++ success: false, ++ output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", ++ error: error, ++ exitCode: -1, ++ needsPassword: true ++ }; ++ if (pending.callback) { ++ pending.callback(result); ++ } ++ if (proc.callback && proc.callback !== pending.callback) { ++ proc.callback(result); ++ } ++ return; ++ } ++ } ++ } ++ } ++ } ++ ++ if (checkCount >= 6) { ++ immediateCheckTimer.stop(); ++ immediateCheckTimer.checkCount = 0; ++ } ++ } ++ } else { ++ immediateCheckTimer.stop(); ++ immediateCheckTimer.checkCount = 0; ++ } ++ } ++ } ++ ++ Process { ++ id: rescanProc ++ ++ command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] ++ onExited: root.getNetworks() // qmllint disable signal-handler-parameters ++ } ++ ++ Process { ++ id: monitorProc ++ ++ running: true ++ command: ["nmcli", "monitor"] ++ environment: ({ ++ LANG: "C.UTF-8", ++ LC_ALL: "C.UTF-8" ++ }) ++ stdout: SplitParser { ++ onRead: root.refreshOnConnectionChange() ++ } ++ onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters ++ } ++ ++ Timer { ++ id: monitorRestartTimer ++ ++ interval: 2000 ++ onTriggered: { ++ monitorProc.running = true; ++ } ++ } ++ ++ LoggingCategory { ++ id: lc ++ ++ name: "caelestia.qml.services.nmcli" ++ defaultLogLevel: LoggingCategory.Info ++ } ++ ++ component CommandProcess: Process { ++ id: proc ++ ++ property var callback: null ++ property list cmdArgs: [] ++ property bool callbackCalled: false ++ property int exitCode: 0 ++ ++ signal processFinished ++ ++ environment: ({ ++ LANG: "C.UTF-8", ++ LC_ALL: "C.UTF-8" ++ }) ++ ++ stdout: StdioCollector { ++ id: stdoutCollector ++ } ++ ++ stderr: StdioCollector { ++ id: stderrCollector ++ ++ onStreamFinished: { ++ const error = text.trim(); ++ if (error && error.length > 0) { ++ const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; ++ root.handlePasswordRequired(proc, error, output, -1); ++ } ++ } ++ } ++ ++ onExited: code => { // qmllint disable signal-handler-parameters ++ exitCode = code; ++ ++ Qt.callLater(() => { ++ if (callbackCalled) { ++ processFinished(); ++ return; ++ } ++ ++ if (proc.callback) { ++ const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; ++ const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; ++ const success = exitCode === 0; ++ const cmdIsConnection = isConnectionCommand(proc.cmdArgs); ++ ++ if (root.handlePasswordRequired(proc, error, output, exitCode)) { ++ processFinished(); ++ return; ++ } ++ ++ const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); ++ ++ if (!success && cmdIsConnection && root.pendingConnection) { ++ const failedSsid = root.pendingConnection.ssid; ++ root.connectionFailed(failedSsid); ++ } ++ ++ callbackCalled = true; ++ callback({ ++ success: success, ++ output: output, ++ error: error, ++ exitCode: proc.exitCode, ++ needsPassword: needsPassword || false ++ }); ++ processFinished(); ++ } else { ++ processFinished(); ++ } ++ }); ++ } ++ } ++ ++ component AccessPoint: QtObject { ++ required property var lastIpcObject ++ readonly property string ssid: lastIpcObject.ssid ++ readonly property string bssid: lastIpcObject.bssid ++ readonly property int strength: lastIpcObject.strength ++ readonly property int frequency: lastIpcObject.frequency ++ readonly property bool active: lastIpcObject.active ++ readonly property string security: lastIpcObject.security ++ readonly property bool isSecure: security.length > 0 ++ } + } +diff --git a/services/NotifData.qml b/services/NotifData.qml +new file mode 100644 +index 00000000..3c6ae23b +--- /dev/null ++++ b/services/NotifData.qml +@@ -0,0 +1,243 @@ ++pragma ComponentBehavior: Bound ++ ++import QtQuick ++import Quickshell ++import Quickshell.Services.Notifications ++import Caelestia ++import Caelestia.Config ++import qs.services ++import qs.utils ++ ++QtObject { ++ id: notif ++ ++ property bool popup ++ property bool closed ++ property var locks: new Set() ++ ++ property date time: new Date() ++ property string timeStr: qsTr("now") ++ ++ readonly property Timer timeStrTimer: Timer { ++ running: !notif.closed ++ repeat: true ++ interval: 5000 ++ onTriggered: notif.updateTimeStr() ++ } ++ ++ property Notification notification ++ property string id ++ property string summary ++ property string body ++ property string appIcon ++ property string appName ++ property string image ++ property var hints // Hints are not persisted across restarts ++ property real expireTimeout: GlobalConfig.notifs.defaultExpireTimeout ++ property int urgency: NotificationUrgency.Normal ++ property bool resident ++ property bool hasActionIcons ++ property list actions ++ ++ readonly property bool hasFullscreen: { ++ const monitor = Hypr.focusedMonitor; ++ const specialName = monitor?.lastIpcObject.specialWorkspace?.name; ++ if (specialName) { ++ const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); ++ return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; ++ } ++ return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; ++ } ++ ++ readonly property Timer timer: Timer { ++ running: true ++ interval: notif.expireTimeout > 0 ? notif.expireTimeout : notif.hasFullscreen ? GlobalConfig.notifs.fullscreenExpireTimeout : GlobalConfig.notifs.defaultExpireTimeout ++ onTriggered: { ++ // Always expire if the active workspace has a fullscreen window ++ if (GlobalConfig.notifs.expire || notif.hasFullscreen) ++ notif.popup = false; ++ } ++ } ++ ++ readonly property LazyLoader dummyImageLoader: LazyLoader { ++ active: false ++ ++ // qmllint disable uncreatable-type ++ PanelWindow { ++ // qmllint enable uncreatable-type ++ implicitWidth: TokenConfig.sizes.notifs.image ++ implicitHeight: TokenConfig.sizes.notifs.image ++ color: "transparent" ++ mask: Region {} ++ ++ Image { ++ function tryCache(): void { ++ if (status !== Image.Ready || width != TokenConfig.sizes.notifs.image || height != TokenConfig.sizes.notifs.image) ++ return; ++ ++ const cacheKey = notif.appName + notif.summary + notif.id + notif.image; ++ let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; ++ for (let i = 0; i < cacheKey.length; i++) { ++ ch = cacheKey.charCodeAt(i); ++ h1 = Math.imul(h1 ^ ch, 2654435761); ++ h2 = Math.imul(h2 ^ ch, 1597334677); ++ } ++ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); ++ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); ++ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); ++ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); ++ const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); ++ ++ const cache = `${Paths.notifimagecache}/${hash}.png`; ++ CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { ++ notif.image = cache; ++ notif.dummyImageLoader.active = false; ++ }); ++ } ++ ++ anchors.fill: parent ++ source: Qt.resolvedUrl(notif.image) ++ fillMode: Image.PreserveAspectCrop ++ cache: false ++ asynchronous: true ++ opacity: 0 ++ ++ onStatusChanged: tryCache() ++ onWidthChanged: tryCache() ++ onHeightChanged: tryCache() ++ } ++ } ++ } ++ ++ readonly property Connections conn: Connections { ++ function onClosed(): void { ++ notif.close(); ++ } ++ ++ function onSummaryChanged(): void { ++ notif.summary = notif.notification.summary; ++ } ++ ++ function onBodyChanged(): void { ++ notif.body = notif.notification.body; ++ } ++ ++ function onAppIconChanged(): void { ++ notif.appIcon = notif.notification.appIcon; ++ } ++ ++ function onAppNameChanged(): void { ++ notif.appName = notif.notification.appName; ++ } ++ ++ function onImageChanged(): void { ++ notif.image = notif.notification.image; ++ notif.maybeTriggerDummyImageLoader(); ++ } ++ ++ function onExpireTimeoutChanged(): void { ++ notif.expireTimeout = notif.notification.expireTimeout; ++ } ++ ++ function onUrgencyChanged(): void { ++ notif.urgency = notif.notification.urgency; ++ } ++ ++ function onResidentChanged(): void { ++ notif.resident = notif.notification.resident; ++ } ++ ++ function onHasActionIconsChanged(): void { ++ notif.hasActionIcons = notif.notification.hasActionIcons; ++ } ++ ++ function onActionsChanged(): void { ++ // qmllint disable unresolved-type ++ notif.actions = notif.notification.actions.map(a => ({ ++ // qmllint enable unresolved-type ++ identifier: a.identifier, ++ text: a.text, ++ invoke: () => a.invoke() ++ })); ++ } ++ ++ function onHintsChanged(): void { ++ notif.hints = notif.notification.hints; ++ } ++ ++ target: notif.notification ++ } ++ ++ function updateTimeStr(): void { ++ const diff = Date.now() - time.getTime(); ++ const m = Math.floor(diff / 60000); ++ ++ if (m < 1) { ++ timeStr = qsTr("now"); ++ timeStrTimer.interval = 5000; ++ } else { ++ const h = Math.floor(m / 60); ++ const d = Math.floor(h / 24); ++ ++ if (d > 0) { ++ timeStr = `${d}d`; ++ timeStrTimer.interval = 3600000; ++ } else if (h > 0) { ++ timeStr = `${h}h`; ++ timeStrTimer.interval = 300000; ++ } else { ++ timeStr = `${m}m`; ++ timeStrTimer.interval = m < 10 ? 30000 : 60000; ++ } ++ } ++ } ++ ++ function maybeTriggerDummyImageLoader(): void { ++ if (image && !image.startsWith("image://icon/") && !image.startsWith(Paths.notifimagecache)) ++ dummyImageLoader.active = true; ++ } ++ ++ function lock(item: Item): void { ++ locks.add(item); ++ } ++ ++ function unlock(item: Item): void { ++ locks.delete(item); ++ if (closed) ++ close(); ++ } ++ ++ function close(): void { ++ closed = true; ++ if (locks.size === 0 && Notifs.list.includes(this)) { ++ Notifs.list = Notifs.list.filter(n => n !== this); ++ notification?.dismiss(); ++ destroy(); ++ } ++ } ++ ++ Component.onCompleted: { ++ if (!notification) ++ return; ++ ++ id = notification.id; ++ summary = notification.summary; ++ body = notification.body; ++ appIcon = notification.appIcon; ++ appName = notification.appName; ++ image = notification.image; ++ maybeTriggerDummyImageLoader(); ++ expireTimeout = notification.expireTimeout; ++ hints = notification.hints; ++ urgency = notification.urgency; ++ resident = notification.resident; ++ hasActionIcons = notification.hasActionIcons; ++ // qmllint disable unresolved-type ++ actions = notification.actions.map(a => ({ ++ // qmllint enable unresolved-type ++ identifier: a.identifier, ++ text: a.text, ++ invoke: () => a.invoke() ++ })); ++ } ++} +diff --git a/services/Notifs.qml b/services/Notifs.qml +index 2ebc32db..d539d0a2 100644 +--- a/services/Notifs.qml ++++ b/services/Notifs.qml +@@ -1,27 +1,44 @@ + pragma Singleton + pragma ComponentBehavior: Bound + +-import qs.components.misc +-import qs.config +-import qs.utils +-import Caelestia ++import QtQuick + import Quickshell + import Quickshell.Io + import Quickshell.Services.Notifications +-import QtQuick ++import Caelestia ++import Caelestia.Config ++import qs.components.misc ++import qs.services ++import qs.utils + + Singleton { + id: root + +- property list list: [] +- readonly property list notClosed: list.filter(n => !n.closed) +- readonly property list popups: list.filter(n => n.popup) ++ property list list: [] ++ readonly property list notClosed: list.filter(n => !n.closed) ++ readonly property list popups: list.filter(n => n.popup) + property alias dnd: props.dnd + + property bool loaded + ++ function hasFullscreen(): bool { ++ for (const monitor of Hypr.monitors.values) { ++ if (monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1)) ++ return true; ++ } ++ return false; ++ } ++ ++ function shouldShowPopup(): bool { ++ if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) ++ return false; ++ if (GlobalConfig.notifs.fullscreen === "off" && hasFullscreen()) ++ return false; ++ return true; ++ } ++ + onDndChanged: { +- if (!Config.utilities.toasts.dndChanged) ++ if (!GlobalConfig.utilities.toasts.dndChanged) + return; + + if (dnd) +@@ -78,7 +95,7 @@ Singleton { + notif.tracked = true; + + const comp = notifComp.createObject(root, { +- popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), ++ popup: root.shouldShowPopup(), + notification: notif + }); + root.list = [comp, ...root.list]; +@@ -88,6 +105,7 @@ Singleton { + FileView { + id: storage + ++ printErrors: false + path: `${Paths.state}/notifs.json` + onLoaded: { + const data = JSON.parse(text()); +@@ -99,12 +117,14 @@ Singleton { + onLoadFailed: err => { + if (err === FileViewError.FileNotFound) { + root.loaded = true; +- setText("[]"); ++ Qt.callLater(() => setText("[]")); + } + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "clearNotifs" + description: "Clear all notifications" + onPressed: { +@@ -114,8 +134,6 @@ Singleton { + } + + IpcHandler { +- target: "notifs" +- + function clear(): void { + for (const notif of root.list.slice()) + notif.close(); +@@ -136,203 +154,13 @@ Singleton { + function disableDnd(): void { + props.dnd = false; + } +- } +- +- component Notif: QtObject { +- id: notif +- +- property bool popup +- property bool closed +- property var locks: new Set() +- +- property date time: new Date() +- readonly property string timeStr: { +- const diff = Time.date.getTime() - time.getTime(); +- const m = Math.floor(diff / 60000); +- +- if (m < 1) +- return qsTr("now"); +- +- const h = Math.floor(m / 60); +- const d = Math.floor(h / 24); +- +- if (d > 0) +- return `${d}d`; +- if (h > 0) +- return `${h}h`; +- return `${m}m`; +- } +- +- property Notification notification +- property string id +- property string summary +- property string body +- property string appIcon +- property string appName +- property string image +- property real expireTimeout: Config.notifs.defaultExpireTimeout +- property int urgency: NotificationUrgency.Normal +- property bool resident +- property bool hasActionIcons +- property list actions +- +- readonly property Timer timer: Timer { +- running: true +- interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout +- onTriggered: { +- if (Config.notifs.expire) +- notif.popup = false; +- } +- } +- +- readonly property LazyLoader dummyImageLoader: LazyLoader { +- active: false +- +- PanelWindow { +- implicitWidth: Config.notifs.sizes.image +- implicitHeight: Config.notifs.sizes.image +- color: "transparent" +- mask: Region {} +- +- Image { +- function tryCache(): void { +- if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) +- return; +- +- const cacheKey = notif.appName + notif.summary + notif.id; +- let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; +- for (let i = 0; i < cacheKey.length; i++) { +- ch = cacheKey.charCodeAt(i); +- h1 = Math.imul(h1 ^ ch, 2654435761); +- h2 = Math.imul(h2 ^ ch, 1597334677); +- } +- h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); +- h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); +- h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); +- h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); +- const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); +- +- const cache = `${Paths.notifimagecache}/${hash}.png`; +- CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { +- notif.image = cache; +- notif.dummyImageLoader.active = false; +- }); +- } +- +- anchors.fill: parent +- source: Qt.resolvedUrl(notif.image) +- fillMode: Image.PreserveAspectCrop +- cache: false +- asynchronous: true +- opacity: 0 +- +- onStatusChanged: tryCache() +- onWidthChanged: tryCache() +- onHeightChanged: tryCache() +- } +- } +- } + +- readonly property Connections conn: Connections { +- target: notif.notification +- +- function onClosed(): void { +- notif.close(); +- } +- +- function onSummaryChanged(): void { +- notif.summary = notif.notification.summary; +- } +- +- function onBodyChanged(): void { +- notif.body = notif.notification.body; +- } +- +- function onAppIconChanged(): void { +- notif.appIcon = notif.notification.appIcon; +- } +- +- function onAppNameChanged(): void { +- notif.appName = notif.notification.appName; +- } +- +- function onImageChanged(): void { +- notif.image = notif.notification.image; +- if (notif.notification?.image) +- notif.dummyImageLoader.active = true; +- } +- +- function onExpireTimeoutChanged(): void { +- notif.expireTimeout = notif.notification.expireTimeout; +- } +- +- function onUrgencyChanged(): void { +- notif.urgency = notif.notification.urgency; +- } +- +- function onResidentChanged(): void { +- notif.resident = notif.notification.resident; +- } +- +- function onHasActionIconsChanged(): void { +- notif.hasActionIcons = notif.notification.hasActionIcons; +- } +- +- function onActionsChanged(): void { +- notif.actions = notif.notification.actions.map(a => ({ +- identifier: a.identifier, +- text: a.text, +- invoke: () => a.invoke() +- })); +- } +- } +- +- function lock(item: Item): void { +- locks.add(item); +- } +- +- function unlock(item: Item): void { +- locks.delete(item); +- if (closed) +- close(); +- } +- +- function close(): void { +- closed = true; +- if (locks.size === 0 && root.list.includes(this)) { +- root.list = root.list.filter(n => n !== this); +- notification?.dismiss(); +- destroy(); +- } +- } +- +- Component.onCompleted: { +- if (!notification) +- return; +- +- id = notification.id; +- summary = notification.summary; +- body = notification.body; +- appIcon = notification.appIcon; +- appName = notification.appName; +- image = notification.image; +- if (notification?.image) +- dummyImageLoader.active = true; +- expireTimeout = notification.expireTimeout; +- urgency = notification.urgency; +- resident = notification.resident; +- hasActionIcons = notification.hasActionIcons; +- actions = notification.actions.map(a => ({ +- identifier: a.identifier, +- text: a.text, +- invoke: () => a.invoke() +- })); +- } ++ target: "notifs" + } + + Component { + id: notifComp + +- Notif {} ++ NotifData {} + } + } +diff --git a/services/Players.qml b/services/Players.qml +index 1191696a..41507839 100644 +--- a/services/Players.qml ++++ b/services/Players.qml +@@ -1,36 +1,51 @@ + pragma Singleton + +-import qs.components.misc +-import qs.config ++import QtQml + import Quickshell + import Quickshell.Io + import Quickshell.Services.Mpris +-import QtQml + import Caelestia ++import Caelestia.Config ++import qs.components.misc + + Singleton { + id: root + + readonly property list list: Mpris.players.values +- readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null ++ readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === GlobalConfig.services.defaultPlayer) ?? list[0] ?? null + property alias manualActive: props.manualActive + + function getIdentity(player: MprisPlayer): string { +- const alias = Config.services.playerAliases.find(a => a.from === player.identity); ++ const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity); + return alias?.to ?? player.identity; + } + +- Connections { +- target: active ++ function getArtUrl(player: MprisPlayer): string { ++ if (!player) ++ return ""; ++ if (player.trackArtUrl) ++ return player.trackArtUrl; ++ ++ const url = player.metadata["xesam:url"] ?? ""; ++ if (url.startsWith("https://www.youtube.com/watch")) { ++ // Fallback for youtube ++ const id = url.match(/[?&]v=([\w-]{11})/)?.[1]; ++ return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : ""; ++ } ++ return ""; ++ } + ++ Connections { + function onPostTrackChanged() { +- if (!Config.utilities.toasts.nowPlaying) { ++ if (!GlobalConfig.utilities.toasts.nowPlaying) { + return; + } +- if (active.trackArtist != "" && active.trackTitle != "") { +- Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note"); ++ if (root.active.trackArtist != "" && root.active.trackTitle != "") { ++ Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note"); + } + } ++ ++ target: root.active + } + + PersistentProperties { +@@ -41,7 +56,9 @@ Singleton { + reloadableId: "players" + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "mediaToggle" + description: "Toggle media playback" + onPressed: { +@@ -51,7 +68,9 @@ Singleton { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "mediaPrev" + description: "Previous track" + onPressed: { +@@ -61,7 +80,9 @@ Singleton { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "mediaNext" + description: "Next track" + onPressed: { +@@ -71,15 +92,15 @@ Singleton { + } + } + ++ // qmllint disable unresolved-type + CustomShortcut { ++ // qmllint enable unresolved-type + name: "mediaStop" + description: "Stop media playback" + onPressed: root.active?.stop() + } + + IpcHandler { +- target: "mpris" +- + function getActive(prop: string): string { + const active = root.active; + return active ? active[prop] ?? "Invalid property" : "No active player"; +@@ -122,5 +143,7 @@ Singleton { + function stop(): void { + root.active?.stop(); + } ++ ++ target: "mpris" + } + } +diff --git a/services/Recorder.qml b/services/Recorder.qml +index be83629c..253bac17 100644 +--- a/services/Recorder.qml ++++ b/services/Recorder.qml +@@ -1,54 +1,66 @@ + pragma Singleton + ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick + + Singleton { + id: root + + readonly property alias running: props.running ++ readonly property alias starting: props.starting + readonly property alias paused: props.paused + readonly property alias elapsed: props.elapsed ++ readonly property alias videoMode: props.videoMode ++ readonly property alias audioMode: props.audioMode ++ property int startChecks: 0 ++ readonly property int maxStartChecks: 30 + + signal errorOccurred(string errorMsg) + signal recordingStarted() + signal recordingStopped() + + function start(videoMode: string, audioMode: string): bool { +- if (props.running) { ++ if (props.running || props.starting) { + console.warn("Recording already running"); + errorOccurred("Recording already in progress"); + return false; + } + ++ const requestedVideoMode = videoMode || "fullscreen"; ++ const requestedAudioMode = audioMode || "none"; ++ + // Build command array +- const args = ["caelestia", "record", "--mode", videoMode]; ++ const args = ["caelestia", "record", "--mode", requestedVideoMode]; + +- if (audioMode) { +- args.push("--audio", audioMode); ++ if (requestedAudioMode) { ++ args.push("--audio", requestedAudioMode); + } + + console.log("Executing:", args.join(" ")); + + try { + Quickshell.execDetached(args); +- props.running = true; ++ props.starting = true; ++ props.running = false; + props.paused = false; + props.elapsed = 0; ++ props.videoMode = requestedVideoMode; ++ props.audioMode = requestedAudioMode; ++ root.startChecks = 0; + verifyTimer.restart(); +- recordingStarted(); + return true; + } catch (error) { + console.error("Failed to start recording:", error); + errorOccurred("Failed to execute recording command: " + error); ++ props.starting = false; + props.running = false; + return false; + } + } + + function stop(): void { +- if (!props.running) { ++ if (!props.running && !props.starting) { + console.warn("No recording to stop"); + return; + } +@@ -57,12 +69,21 @@ Singleton { + + try { + Quickshell.execDetached(["caelestia", "record", "--stop"]); ++ if (props.starting) { ++ props.starting = false; ++ props.running = false; ++ props.paused = false; ++ props.elapsed = 0; ++ recordingStopped(); ++ return; ++ } + // Don't immediately set running to false - wait for process to confirm + stopVerifyTimer.restart(); + } catch (error) { + console.error("Failed to stop recording:", error); + errorOccurred("Failed to stop recording: " + error); + // Force state reset on error ++ props.starting = false; + props.running = false; + props.paused = false; + props.elapsed = 0; +@@ -71,7 +92,7 @@ Singleton { + } + + function togglePause(): void { +- if (!props.running) { ++ if (!props.running || props.starting) { + console.warn("No recording to pause"); + return; + } +@@ -96,8 +117,11 @@ Singleton { + id: props + + property bool running: false ++ property bool starting: false + property bool paused: false + property real elapsed: 0 ++ property string videoMode: "fullscreen" ++ property string audioMode: "none" + + reloadableId: "recorder" + } +@@ -116,6 +140,7 @@ Singleton { + // Detect unexpected stop + if (wasRunning && !isRunning) { + console.warn("Recording process stopped unexpectedly"); ++ props.starting = false; + props.running = false; + props.paused = false; + props.elapsed = 0; +@@ -132,7 +157,7 @@ Singleton { + // Verification timer after start + Timer { + id: verifyTimer +- interval: 1500 ++ interval: 1000 + repeat: false + onTriggered: { + console.log("Verifying recording started"); +@@ -161,9 +186,26 @@ Singleton { + onExited: code => { + const isRunning = code === 0; + +- if (!isRunning && props.running) { ++ if (isRunning && props.starting) { ++ console.log("Recording verified running"); ++ props.starting = false; ++ props.running = true; ++ props.paused = false; ++ recordingStarted(); ++ statusCheckTimer.restart(); ++ return; ++ } ++ ++ if (!isRunning && props.starting) { ++ root.startChecks++; ++ if (root.startChecks < root.maxStartChecks) { ++ verifyTimer.restart(); ++ return; ++ } ++ + console.error("Recording process failed to start"); +- errorOccurred("Recording process failed to start"); ++ errorOccurred("Recording did not start"); ++ props.starting = false; + props.running = false; + props.paused = false; + props.elapsed = 0; +@@ -186,6 +228,7 @@ Singleton { + + if (!isRunning) { + console.log("Recording stopped successfully"); ++ props.starting = false; + props.running = false; + props.paused = false; + props.elapsed = 0; +@@ -240,10 +283,12 @@ Singleton { + onExited: code => { + if (code === 0) { + console.log("Found existing recording process"); ++ props.starting = false; + props.running = true; + statusCheckTimer.restart(); + } else { + console.log("No existing recording process"); ++ props.starting = false; + props.running = false; + props.paused = false; + props.elapsed = 0; +diff --git a/services/Screens.qml b/services/Screens.qml +new file mode 100644 +index 00000000..ac26d27d +--- /dev/null ++++ b/services/Screens.qml +@@ -0,0 +1,14 @@ ++pragma Singleton ++ ++import Quickshell ++import Caelestia.Config ++ ++Singleton { ++ id: root ++ ++ readonly property list screens: Quickshell.screens.filter(s => GlobalConfig.forScreen(s.name).enabled) ++ ++ function isExcluded(screen: ShellScreen): bool { ++ return !GlobalConfig.forScreen(screen.name).enabled; ++ } ++} +diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml +index bd02da36..15dda611 100644 +--- a/services/SystemUsage.qml ++++ b/services/SystemUsage.qml +@@ -1,31 +1,57 @@ + pragma Singleton + +-import qs.config ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick ++import Caelestia.Config + + Singleton { + id: root + ++ // CPU properties ++ property string cpuName: "" + property real cpuPerc + property real cpuTemp +- readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType ++ ++ // GPU properties ++ readonly property string gpuType: GlobalConfig.services.gpuType.toUpperCase() || autoGpuType + property string autoGpuType: "NONE" ++ property string gpuName: "" + property real gpuPerc + property real gpuTemp ++ ++ // Memory properties + property real memUsed + property real memTotal + readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 +- property real storageUsed +- property real storageTotal +- property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 ++ ++ // Storage properties (aggregated) ++ readonly property real storagePerc: { ++ let totalUsed = 0; ++ let totalSize = 0; ++ for (const disk of disks) { ++ totalUsed += disk.used; ++ totalSize += disk.total; ++ } ++ return totalSize > 0 ? totalUsed / totalSize : 0; ++ } ++ ++ // Individual disks: Array of { mount, used, total, free, perc } ++ property var disks: [] + + property real lastCpuIdle + property real lastCpuTotal + + property int refCount + ++ function cleanCpuName(name: string): string { ++ return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim(); ++ } ++ ++ function cleanGpuName(name: string): string { ++ return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim(); ++ } ++ + function formatKib(kib: real): var { + const mib = 1024; + const gib = 1024 ** 2; +@@ -54,7 +80,7 @@ Singleton { + + Timer { + running: root.refCount > 0 +- interval: 3000 ++ interval: GlobalConfig.dashboard.resourceUpdateInterval + repeat: true + triggeredOnStart: true + onTriggered: { +@@ -66,6 +92,18 @@ Singleton { + } + } + ++ // One-time CPU info detection (name) ++ FileView { ++ id: cpuinfoInit ++ ++ path: "/proc/cpuinfo" ++ onLoaded: { ++ const nameMatch = text().match(/model name\s*:\s*(.+)/); ++ if (nameMatch) ++ root.cpuName = root.cleanCpuName(nameMatch[1]); ++ } ++ } ++ + FileView { + id: stat + +@@ -101,41 +139,111 @@ Singleton { + Process { + id: storage + +- command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] ++ // Get physical disks with aggregated usage from their partitions ++ // -J triggers JSON output. -b triggers bytes. ++ command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] ++ + stdout: StdioCollector { + onStreamFinished: { +- const deviceMap = new Map(); ++ const data = JSON.parse(text); ++ const diskList = []; ++ const seenDevices = new Set(); ++ ++ // Helper to recursively sum usage from children (partitions, crypt, lvm) ++ const aggregateUsage = dev => { ++ let used = 0; ++ let size = 0; ++ let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/")); ++ ++ if (!seenDevices.has(dev.name)) { ++ // lsblk returns null for empty/unformatted partitions, which parses to 0 here ++ used = parseInt(dev.fsused) || 0; ++ size = parseInt(dev.fssize) || 0; ++ seenDevices.add(dev.name); ++ } + +- for (const line of text.trim().split("\n")) { +- if (line.trim() === "") +- continue; +- +- const parts = line.trim().split(/\s+/); +- if (parts.length >= 3) { +- const device = parts[0]; +- const used = parseInt(parts[1], 10) || 0; +- const avail = parseInt(parts[2], 10) || 0; +- +- // Only keep the entry with the largest total space for each device +- if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { +- deviceMap.set(device, { +- used: used, +- avail: avail +- }); ++ if (dev.children) { ++ for (const child of dev.children) { ++ const stats = aggregateUsage(child); ++ used += stats.used; ++ size += stats.size; ++ if (stats.isRoot) ++ isRoot = true; + } + } ++ return { ++ used, ++ size, ++ isRoot ++ }; ++ }; ++ ++ for (const dev of data.blockdevices) { ++ // Only process physical disks at the top level ++ if (dev.type === "disk" && !dev.name.startsWith("zram")) { ++ const stats = aggregateUsage(dev); ++ ++ if (stats.size === 0) { ++ continue; ++ } ++ ++ const total = stats.size; ++ const used = stats.used; ++ ++ diskList.push({ ++ mount: dev.name, ++ used: used / 1024 // KiB ++ , ++ total: total / 1024 // KiB ++ , ++ free: (total - used) / 1024, ++ perc: total > 0 ? used / total : 0, ++ hasRoot: stats.isRoot ++ }); ++ } + } + +- let totalUsed = 0; +- let totalAvail = 0; ++ // Sort by putting the disk with root first, then sort the rest alphabetically ++ root.disks = diskList.sort((a, b) => { ++ if (a.hasRoot && !b.hasRoot) ++ return -1; ++ if (!a.hasRoot && b.hasRoot) ++ return 1; ++ return a.mount.localeCompare(b.mount); ++ }); ++ } ++ } ++ } ++ ++ // GPU name detection (one-time) ++ Process { ++ id: gpuNameDetect + +- for (const [device, stats] of deviceMap) { +- totalUsed += stats.used; +- totalAvail += stats.avail; +- } ++ running: true ++ command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"] ++ stdout: StdioCollector { ++ onStreamFinished: { ++ const output = text.trim(); ++ if (!output) ++ return; + +- root.storageUsed = totalUsed; +- root.storageTotal = totalUsed + totalAvail; ++ // Check if it's from nvidia-smi (clean GPU name) ++ if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { ++ root.gpuName = root.cleanGpuName(output); ++ } else if (output.toLowerCase().includes("rx")) { ++ root.gpuName = root.cleanGpuName(output); ++ } else { ++ // Parse lspci output: extract name from brackets or after colon ++ // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0) ++ const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/); ++ if (bracketMatch) { ++ root.gpuName = root.cleanGpuName(bracketMatch[1]); ++ } else { ++ const colonMatch = output.match(/:\s*(.+)/); ++ if (colonMatch) ++ root.gpuName = root.cleanGpuName(colonMatch[1]); ++ } ++ } + } + } + } +@@ -143,7 +251,7 @@ Singleton { + Process { + id: gpuTypeCheck + +- running: !Config.services.gpuType ++ running: !GlobalConfig.services.gpuType + command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] + stdout: StdioCollector { + onStreamFinished: root.autoGpuType = text.trim() +diff --git a/services/Time.qml b/services/Time.qml +index a07d9ef8..0db520d0 100644 +--- a/services/Time.qml ++++ b/services/Time.qml +@@ -1,8 +1,8 @@ + pragma Singleton + +-import qs.config +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia.Config + + Singleton { + property alias enabled: clock.enabled +@@ -11,7 +11,7 @@ Singleton { + readonly property int minutes: clock.minutes + readonly property int seconds: clock.seconds + +- readonly property string timeStr: format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") ++ readonly property string timeStr: format(GlobalConfig.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") + readonly property list timeComponents: timeStr.split(":") + readonly property string hourStr: timeComponents[0] ?? "" + readonly property string minuteStr: timeComponents[1] ?? "" +@@ -23,6 +23,7 @@ Singleton { + + SystemClock { + id: clock ++ + precision: SystemClock.Seconds + } + } +diff --git a/services/VPN.qml b/services/VPN.qml +index 2d08631a..61e91d0b 100644 +--- a/services/VPN.qml ++++ b/services/VPN.qml +@@ -1,20 +1,26 @@ + pragma Singleton + ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick +-import qs.config + import Caelestia ++import Caelestia.Config + + Singleton { + id: root + + property bool connected: false ++ property var status: ({ ++ connected: false, ++ state: "disconnected", ++ reason: "", ++ authUrl: "" ++ }) + + readonly property bool connecting: connectProc.running || disconnectProc.running +- readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) ++ readonly property bool enabled: GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) + readonly property var providerInput: { +- const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); ++ const enabledProvider = GlobalConfig.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); + return enabledProvider || "wireguard"; + } + readonly property bool isCustomProvider: typeof providerInput === "object" +@@ -53,7 +59,7 @@ Singleton { + displayName: "Warp" + }, + "netbird": { +- connectCmd: ["netbird", "up"], ++ connectCmd: ["netbird", "up", "--no-browser"], + disconnectCmd: ["netbird", "down"], + interface: "wt0", + displayName: "NetBird" +@@ -75,6 +81,10 @@ Singleton { + } + + function connect(): void { ++ if (status.state === "needs-auth" && status.authUrl) { ++ emitStatusToast(status); ++ return; ++ } + if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { + connectProc.exec(root.currentConfig.connectCmd); + } +@@ -87,11 +97,7 @@ Singleton { + } + + function toggle(): void { +- if (connected) { +- disconnect(); +- } else { +- connect(); +- } ++ connected ? disconnect() : connect(); + } + + function checkStatus(): void { +@@ -100,18 +106,223 @@ Singleton { + } + } + +- onConnectedChanged: { +- if (!Config.utilities.toasts.vpnChanged) ++ function getStatusCommand(): var { ++ switch (providerName) { ++ case "tailscale": ++ return ["tailscale", "status", "--json"]; ++ case "netbird": ++ return ["netbird", "status", "--json"]; ++ case "warp": ++ return ["warp-cli", "status"]; ++ case "wireguard": ++ return ["ip", "link", "show"]; ++ default: ++ return ["ip", "link", "show"]; ++ } ++ } ++ ++ function parseTailscaleStatus(output: string): var { ++ const status = { ++ connected: false, ++ state: "disconnected", ++ reason: "", ++ authUrl: "" ++ }; ++ ++ // Handle empty or whitespace-only output ++ if (!output || output.trim().length === 0) { ++ return status; ++ } ++ ++ // Check for common non-JSON states first ++ if (output.includes("Logged out") || output.includes("Stopped") || output.includes("not running") || output.includes("Tailscale is not running")) { ++ status.state = "disconnected"; ++ return status; ++ } ++ ++ // Try to parse as JSON ++ try { ++ const data = JSON.parse(output); ++ const backendState = data.BackendState || ""; ++ ++ if (backendState === "Running") { ++ status.connected = true; ++ status.state = "connected"; ++ } else if (backendState === "Starting") { ++ status.state = "connecting"; ++ } else if (backendState === "NeedsLogin" || backendState === "NeedsMachineAuth") { ++ status.state = "needs-auth"; ++ status.reason = backendState === "NeedsLogin" ? "Login required" : "Machine authorization required"; ++ status.authUrl = data.AuthURL || ""; ++ } ++ } catch (e) { ++ // JSON parsing failed - treat as disconnected unless it looks like an error ++ if (output.includes("error") || output.includes("Error") || output.includes("failed")) { ++ status.state = "disconnected"; ++ status.reason = "Tailscale may not be running"; ++ } else { ++ status.state = "disconnected"; ++ } ++ } ++ return status; ++ } ++ ++ function parseNetBirdStatus(output: string): var { ++ const status = { ++ connected: false, ++ state: "disconnected", ++ reason: "", ++ authUrl: "" ++ }; ++ try { ++ const data = JSON.parse(output); ++ const mgmtConnected = data.management?.connected; ++ const signalConnected = data.signal?.connected; ++ ++ if (mgmtConnected && signalConnected) { ++ status.connected = true; ++ status.state = "connected"; ++ } else if (data.management?.error) { ++ const error = data.management.error; ++ if (error.includes("auth") || error.includes("login")) { ++ status.state = "needs-auth"; ++ status.reason = "Authentication required"; ++ } else { ++ status.reason = error; ++ } ++ } ++ } catch (e) { ++ status.state = "error"; ++ status.reason = "Failed to parse status"; ++ } ++ return status; ++ } ++ ++ function parseWarpStatus(output: string): var { ++ const status = { ++ connected: false, ++ state: "disconnected", ++ reason: "", ++ authUrl: "" ++ }; ++ ++ if (output.includes("Connected")) { ++ status.connected = true; ++ status.state = "connected"; ++ } else if (output.includes("Connecting")) { ++ status.state = "connecting"; ++ } else if (output.includes("Unable") || output.includes("Registration Missing") || output.includes("registration") || output.includes("register")) { ++ status.state = "needs-auth"; ++ status.reason = "WARP registration required"; ++ } else if (!output.includes("Disconnected")) { ++ status.state = "error"; ++ status.reason = "Unknown WARP status"; ++ } ++ return status; ++ } ++ ++ function parseWireGuardStatus(output: string): var { ++ const status = { ++ connected: false, ++ state: "disconnected", ++ reason: "", ++ authUrl: "" ++ }; ++ const iface = root.currentConfig?.interface || ""; ++ ++ if (iface && output.includes(iface + ":")) { ++ status.connected = true; ++ status.state = "connected"; ++ } ++ return status; ++ } ++ ++ function parseStatusOutput(output: string): var { ++ switch (providerName) { ++ case "tailscale": ++ return parseTailscaleStatus(output); ++ case "netbird": ++ return parseNetBirdStatus(output); ++ case "warp": ++ return parseWarpStatus(output); ++ case "wireguard": ++ default: ++ return parseWireGuardStatus(output); ++ } ++ } ++ ++ function extractAuthUrl(text: string): string { ++ const urlMatch = text.match(/(https?:\/\/[^\s]+)/); ++ return urlMatch ? urlMatch[1] : ""; ++ } ++ ++ function createAuthStatus(authUrl: string): var { ++ return { ++ connected: false, ++ state: "needs-auth", ++ reason: "Authentication required", ++ authUrl: authUrl ++ }; ++ } ++ ++ function updateStatus(newStatus: var): void { ++ const oldState = status.state; ++ if (newStatus.state === "needs-auth" && !newStatus.authUrl && status.authUrl) { ++ newStatus.authUrl = status.authUrl; ++ } ++ status = newStatus; ++ root.connected = newStatus.connected; ++ ++ if (oldState !== newStatus.state) { ++ emitStatusToast(newStatus); ++ } ++ } ++ ++ function emitStatusToast(statusObj: var): void { ++ if (!GlobalConfig.utilities.toasts.vpnChanged) + return; + + const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; +- if (connected) { ++ ++ switch (statusObj.state) { ++ case "connected": + Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); +- } else { +- Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); ++ break; ++ case "disconnected": ++ if (status.connected) { ++ Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); ++ } ++ break; ++ case "needs-auth": ++ const authMsg = statusObj.reason || "Authentication required"; ++ Toaster.toast(qsTr("VPN authentication required"), qsTr("%1: %2").arg(displayName).arg(authMsg), "vpn_lock"); ++ break; ++ case "error": ++ if (status.state === "connected" || status.state === "connecting" || status.state === "needs-auth") { ++ const errMsg = statusObj.reason || "Unknown error"; ++ Toaster.toast(qsTr("VPN error"), qsTr("%1: %2").arg(displayName).arg(errMsg), "error"); ++ } ++ break; ++ } ++ } ++ ++ onStatusChanged: { ++ if (providerName === "warp" && status.state === "needs-auth" && status.reason.includes("registration")) { ++ warpRegisterProc.exec(["warp-cli", "registration", "new"]); + } + } + ++ onProviderNameChanged: { ++ status = { ++ connected: false, ++ state: "disconnected", ++ reason: "", ++ authUrl: "" ++ }; ++ root.connected = false; ++ statusCheckTimer.start(); ++ } ++ + Component.onCompleted: root.enabled && statusCheckTimer.start() + + Process { +@@ -127,15 +338,47 @@ Singleton { + Process { + id: statusProc + +- command: ["ip", "link", "show"] ++ command: root.getStatusCommand() ++ // qmllint disable incompatible-type + environment: ({ ++ // qmllint enable incompatible-type + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: StdioCollector { + onStreamFinished: { +- const iface = root.currentConfig ? root.currentConfig.interface : ""; +- root.connected = iface && text.includes(iface + ":"); ++ const newStatus = root.parseStatusOutput(text); ++ root.updateStatus(newStatus); ++ } ++ } ++ stderr: StdioCollector { ++ onStreamFinished: { ++ if (text.trim().length > 0) { ++ if (text.includes("doesn't appear to be running") || text.includes("failed to connect to local tailscaled") || text.includes("daemon is not running") || text.includes("not running") && (text.includes("netbird") || text.includes("warp"))) { ++ let cmd = "sudo systemctl start "; ++ switch (root.providerName) { ++ case "tailscale": ++ cmd += "tailscaled"; ++ break; ++ case "netbird": ++ cmd += "netbird"; ++ break; ++ case "warp": ++ cmd += "warp-svc"; ++ break; ++ default: ++ cmd += root.providerName + "d"; ++ break; ++ } ++ const errorStatus = { ++ connected: false, ++ state: "disconnected", ++ reason: `Service not running (run: ${cmd})`, ++ authUrl: "" ++ }; ++ root.updateStatus(errorStatus); ++ } ++ } + } + } + } +@@ -143,12 +386,59 @@ Singleton { + Process { + id: connectProc + +- onExited: statusCheckTimer.start() ++ onExited: exitCode => { // qmllint disable signal-handler-parameters ++ if (exitCode !== 0) { ++ return; ++ } ++ ++ if (root.providerName === "tailscale") { ++ Qt.callLater(() => { ++ if (root.status.state !== "needs-auth") { ++ statusCheckTimer.start(); ++ } ++ }); ++ } else if (root.status.state !== "needs-auth") { ++ statusCheckTimer.start(); ++ } ++ } ++ stdout: SplitParser { ++ onRead: data => { ++ const authUrl = root.extractAuthUrl(data); ++ if (authUrl) { ++ root.updateStatus(root.createAuthStatus(authUrl)); ++ } ++ } ++ } + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); +- if (error && !error.includes("[#]") && !error.includes("already exists")) { +- console.warn("VPN connection error:", error); ++ ++ if (error.includes("Access denied") || error.includes("checkprefs access denied")) { ++ const errorStatus = { ++ connected: false, ++ state: "disconnected", ++ reason: "Permission denied. Run in terminal: sudo tailscale set --operator=$USER", ++ authUrl: "" ++ }; ++ root.updateStatus(errorStatus); ++ return; ++ } ++ ++ if (error.includes("Unknown device type") || error.includes("Protocol not supported")) { ++ const errorStatus = { ++ connected: false, ++ state: "disconnected", ++ reason: "WireGuard module not loaded. Run: sudo modprobe wireguard", ++ authUrl: "" ++ }; ++ root.updateStatus(errorStatus); ++ return; ++ } ++ ++ const authUrl = root.extractAuthUrl(error); ++ ++ if (authUrl) { ++ root.updateStatus(root.createAuthStatus(authUrl)); + } else if (error.includes("already exists")) { + root.connected = true; + } +@@ -159,21 +449,38 @@ Singleton { + Process { + id: disconnectProc + +- onExited: statusCheckTimer.start() ++ onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters + stderr: StdioCollector { + onStreamFinished: { + const error = text.trim(); + if (error && !error.includes("[#]")) { +- console.warn("VPN disconnection error:", error); ++ console.warn(lc, "Disconnection error:", error); + } + } + } + } + ++ Process { ++ id: warpRegisterProc ++ ++ onExited: exitCode => { // qmllint disable signal-handler-parameters ++ if (exitCode === 0) { ++ statusCheckTimer.start(); ++ } ++ } ++ } ++ + Timer { + id: statusCheckTimer + + interval: 500 + onTriggered: root.checkStatus() + } ++ ++ LoggingCategory { ++ id: lc ++ ++ name: "caelestia.qml.services.vpn" ++ defaultLogLevel: LoggingCategory.Info ++ } + } +diff --git a/services/Visibilities.qml b/services/Visibilities.qml +index 5ddde0c9..39187050 100644 +--- a/services/Visibilities.qml ++++ b/services/Visibilities.qml +@@ -1,16 +1,18 @@ + pragma Singleton + + import Quickshell ++import qs.components ++import qs.services + + Singleton { + property var screens: new Map() + property var bars: new Map() + +- function load(screen: ShellScreen, visibilities: var): void { ++ function load(screen: ShellScreen, visibilities: DrawerVisibilities): void { + screens.set(Hypr.monitorFor(screen), visibilities); + } + +- function getForActive(): PersistentProperties { ++ function getForActive(): DrawerVisibilities { + return screens.get(Hypr.focusedMonitor); + } + } +diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml +index cb96bc56..15daf5c1 100644 +--- a/services/Wallpapers.qml ++++ b/services/Wallpapers.qml +@@ -1,17 +1,18 @@ + pragma Singleton + +-import qs.config +-import qs.utils +-import Caelestia.Models ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick ++import Caelestia.Config ++import Caelestia.Models ++import qs.services ++import qs.utils + + Searcher { + id: root + + readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt` +- readonly property list smartArg: Config.services.smartScheme ? [] : ["--no-smart"] ++ readonly property list smartArg: GlobalConfig.services.smartScheme ? [] : ["--no-smart"] + + property bool showPreview: false + readonly property string current: showPreview ? previewPath : actualCurrent +@@ -40,14 +41,12 @@ Searcher { + + list: wallpapers.entries + key: "relativePath" +- useFuzzy: Config.launcher.useFuzzy.wallpapers ++ useFuzzy: GlobalConfig.launcher.useFuzzy.wallpapers + extraOpts: useFuzzy ? ({}) : ({ + forward: false + }) + + IpcHandler { +- target: "wallpaper" +- + function get(): string { + return root.actualCurrent; + } +@@ -59,6 +58,8 @@ Searcher { + function list(): string { + return root.list.map(w => w.path).join("\n"); + } ++ ++ target: "wallpaper" + } + + FileView { +diff --git a/services/Weather.qml b/services/Weather.qml +index a3095423..9c30bf7a 100644 +--- a/services/Weather.qml ++++ b/services/Weather.qml +@@ -1,10 +1,10 @@ + pragma Singleton + +-import qs.config +-import qs.utils +-import Caelestia +-import Quickshell + import QtQuick ++import Quickshell ++import Caelestia ++import Caelestia.Config ++import qs.utils + + Singleton { + id: root +@@ -17,17 +17,17 @@ Singleton { + + readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" + readonly property string description: cc?.weatherDesc ?? qsTr("No weather") +- readonly property string temp: Config.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` +- readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` ++ readonly property string temp: GlobalConfig.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` ++ readonly property string feelsLike: GlobalConfig.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` + readonly property int humidity: cc?.humidity ?? 0 + readonly property real windSpeed: cc?.windSpeed ?? 0 +- readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" +- readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" ++ readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" ++ readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" + + readonly property var cachedCities: new Map() + + function reload(): void { +- const configLocation = Config.services.weatherLocation; ++ const configLocation = GlobalConfig.services.weatherLocation; + + if (configLocation) { + if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) { +@@ -54,18 +54,35 @@ Singleton { + return; + } + +- const [lat, lon] = coords.split(","); +- const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; +- Requests.get(url, text => { ++ const [lat, lon] = coords.split(",").map(s => s.trim()); ++ ++ const fallbackToBigDataCloud = () => { ++ const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`; ++ Requests.get(fallbackUrl, text => { ++ const geo = JSON.parse(text); ++ const geoCity = geo.city || geo.locality; ++ if (geoCity) { ++ city = geoCity; ++ cachedCities.set(coords, geoCity); ++ } else { ++ city = "Unknown City"; ++ } ++ }); ++ }; ++ ++ const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; ++ Requests.get(nominatimUrl, text => { + const geo = JSON.parse(text).features?.[0]?.properties.geocoding; + if (geo) { + const geoCity = geo.type === "city" ? geo.name : geo.city; +- city = geoCity; +- cachedCities.set(coords, geoCity); +- } else { +- city = "Unknown City"; ++ if (geoCity) { ++ city = geoCity; ++ cachedCities.set(coords, geoCity); ++ return; ++ } + } +- }); ++ fallbackToBigDataCloud(); ++ }, fallbackToBigDataCloud); + } + + function fetchCoordsFromCity(cityName: string): void { +@@ -104,14 +121,14 @@ Singleton { + humidity: json.current.relative_humidity_2m, + windSpeed: json.current.wind_speed_10m, + isDay: json.current.is_day, +- sunrise: json.daily.sunrise[0], +- sunset: json.daily.sunset[0] ++ sunrise: json.daily.sunrise[0].replace("T", " "), ++ sunset: json.daily.sunset[0].replace("T", " ") + }; + + const forecastList = []; + for (let i = 0; i < json.daily.time.length; i++) + forecastList.push({ +- date: json.daily.time[i], ++ date: json.daily.time[i].replace(/-/g, "/"), + maxTempC: Math.round(json.daily.temperature_2m_max[i]), + maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), + minTempC: Math.round(json.daily.temperature_2m_min[i]), +@@ -124,7 +141,8 @@ Singleton { + const hourlyList = []; + const now = new Date(); + for (let i = 0; i < json.hourly.time.length; i++) { +- const time = new Date(json.hourly.time[i]); ++ const time = new Date(json.hourly.time[i].replace("T", " ")); ++ + if (time < now) + continue; + +@@ -149,7 +167,7 @@ Singleton { + if (!loc || loc.indexOf(",") === -1) + return ""; + +- const [lat, lon] = loc.split(","); ++ const [lat, lon] = loc.split(",").map(s => s.trim()); + const baseUrl = "https://api.open-meteo.com/v1/forecast"; + const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; + +@@ -192,7 +210,14 @@ Singleton { + + onLocChanged: fetchWeatherData() + +- // Refresh current location hourly ++ Connections { ++ function onWeatherLocationChanged(): void { ++ root.reload(); ++ } ++ ++ target: GlobalConfig.services ++ } ++ + Timer { + interval: 3600000 // 1 hour + running: true +diff --git a/shell.qml b/shell.qml +index e93b5c52..7cb42354 100644 +--- a/shell.qml ++++ b/shell.qml +@@ -1,6 +1,8 @@ +-//@ pragma Env QS_NO_RELOAD_POPUP=1 +-//@ pragma Env QSG_RENDER_LOOP=threaded +-//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 ++//@ pragma Env QS_CRASHREPORT_URL=https://github.com/caelestia-dots/shell/issues/new?template=crash.yml ++//@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1 ++//@ pragma DefaultEnv QS_DROP_EXPENSIVE_FONTS=1 ++//@ pragma DefaultEnv QSG_RENDER_LOOP=threaded ++//@ pragma DefaultEnv QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 + + import "modules" + import "modules/drawers" +@@ -10,6 +12,8 @@ import "modules/lock" + import Quickshell + + ShellRoot { ++ settings.watchFiles: true ++ + Background {} + Drawers {} + AreaPicker {} +@@ -17,6 +21,7 @@ ShellRoot { + id: lock + } + ++ ConfigToasts {} + Shortcuts {} + BatteryMonitor {} + IdleMonitors { +diff --git a/utils/Icons.qml b/utils/Icons.qml +index c06cbf80..c864d553 100644 +--- a/utils/Icons.qml ++++ b/utils/Icons.qml +@@ -1,9 +1,9 @@ + pragma Singleton + +-import qs.config ++import QtQuick + import Quickshell + import Quickshell.Services.Notifications +-import QtQuick ++import Caelestia.Config + + Singleton { + id: root +@@ -79,6 +79,26 @@ Singleton { + Office: "content_paste" + }) + ++ // Checks if a name matches an icon config. Icon configs can have the following keys: ++ // - name: The exact name of the icon ++ // - regex: A regex to match against the name (takes priority over name) ++ // - flags: The regex flags (only used if regex is set) ++ // - icon: The icon to use ++ function matchIconConfig(name: string, iconConfig: var): bool { ++ if (!iconConfig.icon) ++ return false; ++ ++ if (iconConfig.regex) { ++ const re = new RegExp(iconConfig.regex, iconConfig.flags ?? ""); ++ if (re.test(name)) ++ return true; ++ } else if (iconConfig.name === name) { ++ return true; ++ } ++ ++ return false; ++ } ++ + function getAppIcon(name: string, fallback: string): string { + const icon = DesktopEntries.heuristicLookup(name)?.icon; + if (fallback !== "undefined") +@@ -87,6 +107,10 @@ Singleton { + } + + function getAppCategoryIcon(name: string, fallback: string): string { ++ for (const iconConfig of GlobalConfig.bar.workspaces.windowIcons) ++ if (matchIconConfig(name, iconConfig)) ++ return iconConfig.icon; ++ + const categories = DesktopEntries.heuristicLookup(name)?.categories; + + if (categories) +@@ -188,11 +212,9 @@ Singleton { + function getSpecialWsIcon(name: string): string { + name = name.toLowerCase().slice("special:".length); + +- for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) { +- if (iconConfig.name === name) { ++ for (const iconConfig of GlobalConfig.bar.workspaces.specialWorkspaceIcons) ++ if (matchIconConfig(name, iconConfig)) + return iconConfig.icon; +- } +- } + + if (name === "special") + return "star"; +@@ -208,7 +230,7 @@ Singleton { + } + + function getTrayIcon(id: string, icon: string): string { +- for (const sub of Config.bar.tray.iconSubs) ++ for (const sub of GlobalConfig.bar.tray.iconSubs) + if (sub.id === id) + return sub.image ? Qt.resolvedUrl(sub.image) : Quickshell.iconPath(sub.icon); + +@@ -218,4 +240,24 @@ Singleton { + } + return icon; + } ++ ++ function getBatteryIcon(charge: int): string { ++ if (charge > 0 && charge < 5) ++ return "battery_0_bar"; ++ if (charge >= 5 && charge < 20) ++ return "battery_1_bar"; ++ if (charge >= 20 && charge < 35) ++ return "battery_2_bar"; ++ if (charge >= 35 && charge < 50) ++ return "battery_3_bar"; ++ if (charge >= 50 && charge < 65) ++ return "battery_4_bar"; ++ if (charge >= 65 && charge < 80) ++ return "battery_5_bar"; ++ if (charge >= 80 && charge < 95) ++ return "battery_6_bar"; ++ if (charge >= 95) ++ return "battery_full"; ++ return "battery_alert"; ++ } + } +diff --git a/utils/NetworkConnection.qml b/utils/NetworkConnection.qml +index e55b87bc..8331813d 100644 +--- a/utils/NetworkConnection.qml ++++ b/utils/NetworkConnection.qml +@@ -1,7 +1,7 @@ + pragma Singleton + +-import qs.services + import QtQuick ++import qs.services + + /** + * NetworkConnection +diff --git a/utils/Paths.qml b/utils/Paths.qml +index bc89770a..97f6448a 100644 +--- a/utils/Paths.qml ++++ b/utils/Paths.qml +@@ -1,8 +1,9 @@ + pragma Singleton + +-import qs.config +-import Caelestia ++import QtQuick + import Quickshell ++import Caelestia ++import Caelestia.Config + + Singleton { + id: root +@@ -18,7 +19,7 @@ Singleton { + + readonly property string imagecache: `${cache}/imagecache` + readonly property string notifimagecache: `${imagecache}/notifs` +- readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(Config.paths.wallpaperDir) ++ readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(GlobalConfig.paths.wallpaperDir) + readonly property string recsdir: Quickshell.env("CAELESTIA_RECORDINGS_DIR") || `${videos}/Recordings` + readonly property string libdir: Quickshell.env("CAELESTIA_LIB_DIR") || "/usr/lib/caelestia" + +diff --git a/utils/Searcher.qml b/utils/Searcher.qml +index 053b73bb..102c9e76 100644 +--- a/utils/Searcher.qml ++++ b/utils/Searcher.qml +@@ -1,8 +1,7 @@ +-import Quickshell +- + import "scripts/fzf.js" as Fzf + import "scripts/fuzzysort.js" as Fuzzy + import QtQuick ++import Quickshell + + Singleton { + required property list list +diff --git a/utils/Strings.qml b/utils/Strings.qml +new file mode 100644 +index 00000000..a91a0c08 +--- /dev/null ++++ b/utils/Strings.qml +@@ -0,0 +1,26 @@ ++pragma Singleton ++ ++import Quickshell ++ ++Singleton { ++ property var _regexCache: ({}) ++ ++ function testRegexList(filterList: list, target: string): bool { ++ const regexChecker = /^\^.*\$$/; ++ for (const filter of filterList) { ++ if (regexChecker.test(filter)) { ++ let re = _regexCache[filter]; ++ if (!re) { ++ re = new RegExp(filter); ++ _regexCache[filter] = re; ++ } ++ if (re.test(target)) ++ return true; ++ } else { ++ if (filter === target) ++ return true; ++ } ++ } ++ return false; ++ } ++} +diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml +index 19aa4a7a..c715b8d2 100644 +--- a/utils/SysInfo.qml ++++ b/utils/SysInfo.qml +@@ -1,10 +1,10 @@ + pragma Singleton + +-import qs.config +-import qs.utils ++import QtQuick + import Quickshell + import Quickshell.Io +-import QtQuick ++import Caelestia.Config ++import qs.utils + + Singleton { + id: root +@@ -36,11 +36,11 @@ Singleton { + root.osIdLike = fd("ID_LIKE").split(" "); + + const logo = Quickshell.iconPath(fd("LOGO"), true); +- if (Config.general.logo === "caelestia") { ++ if (GlobalConfig.general.logo === "caelestia") { + root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`); + root.isDefaultLogo = true; +- } else if (Config.general.logo) { +- root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); ++ } else if (GlobalConfig.general.logo) { ++ root.osLogo = Quickshell.iconPath(GlobalConfig.general.logo, true) || "file://" + Paths.absolutePath(GlobalConfig.general.logo); + root.isDefaultLogo = false; + } else if (logo) { + root.osLogo = logo; +@@ -50,11 +50,11 @@ Singleton { + } + + Connections { +- target: Config.general +- + function onLogoChanged(): void { + osRelease.reload(); + } ++ ++ target: GlobalConfig.general + } + + Timer { +diff --git a/utils/scripts/lrcparser.js b/utils/scripts/lrcparser.js +new file mode 100644 +index 00000000..847779ed +--- /dev/null ++++ b/utils/scripts/lrcparser.js +@@ -0,0 +1,62 @@ ++function parseLrc(text) { ++ if (!text) return []; ++ let lines = text.split("\n"); ++ let result = []; ++ ++ let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; ++ ++ // Blacklist for credits/metadata often found in NetEase lyrics ++ const creditKeywords = [ ++ "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" ++ ]; ++ ++ for (let line of lines) { ++ ++ timeRegex.lastIndex = 0; ++ let matches = []; ++ let match; ++ ++ while ((match = timeRegex.exec(line)) !== null) { ++ matches.push(match); ++ } ++ ++ if (matches.length === 0) continue; ++ ++ let lyric = line.replace(timeRegex, "").trim(); ++ ++ let min = parseInt(matches[0][1]); ++ let sec = parseFloat(matches[0][2]); ++ let totalTime = min * 60 + sec; ++ ++ // Only filter credits if they appear in the first 20 seconds ++ if (totalTime < 20) { ++ let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); ++ if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { ++ continue; ++ } ++ } ++ ++ for (let match of matches) { ++ let min = parseInt(match[1]); ++ let sec = parseFloat(match[2]); ++ ++ result.push({ ++ time: min * 60 + sec, ++ text: lyric ++ }); ++ } ++ } ++ ++ result.sort((a, b) => a.time - b.time); ++ return result; ++} ++ ++function getCurrentLine(lyrics, position) { ++ const epsilon = 0.1; // 100ms tolerance ++ for (let i = lyrics.length - 1; i >= 0; i--) { ++ if ((position + epsilon) >= lyrics[i].time) { ++ return i; ++ } ++ } ++ return -1; ++} diff --git a/assets/logo.svg b/assets/logo.svg index 712d1e36e..6879c92b9 100644 --- a/assets/logo.svg +++ b/assets/logo.svg @@ -1,21 +1,19 @@ - - - - + inkscape:zoom="1.28" + inkscape:cx="43.359375" + inkscape:cy="183.98438" + inkscape:window-width="1824" + inkscape:window-height="1034" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="1" + inkscape:current-layer="Layer_2" /> + id="defs1"> + + - - - - - - - - - - + id="dark-4" + transform="matrix(0.08939103,0,0,0.08939103,-3.899948e-4,18.839439)"> + + + + + diff --git a/assets/shaders/fade.frag b/assets/shaders/fade.frag new file mode 100644 index 000000000..a6cdf70d0 --- /dev/null +++ b/assets/shaders/fade.frag @@ -0,0 +1,26 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float fadeMargin; +}; + +layout(binding = 1) uniform sampler2D source; + +void main() { + vec4 tex = texture(source, qt_TexCoord0); + float factor = 1.0; + float margin = 0.1; + + if (qt_TexCoord0.y < margin) { + factor = qt_TexCoord0.y / margin; + } else if (qt_TexCoord0.y > (1.0 - margin)) { + factor = (1.0 - qt_TexCoord0.y) / margin; + } + + fragColor = tex * factor * qt_Opacity; +} diff --git a/assets/shaders/fade.frag.qsb b/assets/shaders/fade.frag.qsb new file mode 100644 index 0000000000000000000000000000000000000000..888e4c10654840ca4da53af2441f2396360300b2 GIT binary patch literal 1659 zcmV->288(l02j`9ob6caZ_`#3|1}Q^hVg#0u?roL(h{7K7HFCRYqx;}>O?CxEvh2d zv4gk74z|;^LY*e<%RX)2_P0#?8`yuc?|VqorgQIcdgF#i2YYDZN{M}r&-?z)@l62i z0+0hR1mHLxAx0fbaIl6s1PBnK0TVz50RG^G0gDVAgo_Xsno_wX)s^|@s_wuFx^U%P z9aGZhZA~UC&8ztC!9t7>9@YTtW{I1c^#0`LFsm$}0}D1h#8_vSHo8ncXuy%4 zLNwvQXQlb4De1}VL4*KZgs|ZNC~)TV?4{QZzBV>l`Jb|xkzumG8#^(_+ylr+kGru$ zUO%|Ic()kEjbeF{s*Am{s4A*`7?x?#r)_zZEe-&fl*P8 zhUNG7_D9JReSmrN{4mEtI%CMm>rrX#%Nq%O9KerHo>Y$@hZCF|tudxgYW7a`+L78I zgSVMq<`x|0IEbEP-f`q`ou5&iPV-(|XFIB%IaD~WX|_T14D%?qD#td&u@OD1#dd>b z$=($5@;+%risKh#B#tV)PrI~usZK{y^f9KZ$jSZK%X>jI)%zgV`Xl6}+#!}B{w%W6 z{vp0U!gZ(nV_frF$jf@Z$#zb%ZHoUb<|j1u2kCu}Jl9;w5dUXndzk$VIr)q*eSvd6 zqxl*c;OiacC-gb?^)>R6KF>1ev|NbyD>BkA`M99@C=Bp1%KU`B$UeSeA7fk};{U)o zj85Tg*@N-euie9M>fCe#a25smYS#89l`B0UkG* zFGg0KF!BOlluXm;gpTb=>Y>+s*b#0ku;R)Vw&%wc!-(RrYsX@x>l%*@A%tjKaalZy z>-VfU^wuhpCoK?jp<~%zysmRytKrt`DWC$B#uIlb`O7S?3~tD8rxiIV^#Pt{MWIwQ#ihid^4PD|=b6R(&5WiZ z*o;DbH(T}hL#sJ&#a7K+CM#6wi)?jNT%PXlyj1)5IbjB-lcN3Wt=0A$I*EsvX zt=J|t0S~ce^ITy5o@CC=)>*4l_K-Vk$_VJeC0|T$zC-zF$ zb)8TGVp@bw(~Dv!EUcKxK8C^P^|tkC5Gq8K*UIFg-ov1n79Gp}EO6b(i3@iB+lnPQ zn3QOFi|8g=)1t-oQ3c?ocPhp#G=CUVr<2Zs4Jm5gIpJmNZ5#CEB0&^ zN*eI`vwqZbdoF%>=l+BGTo^=A{f^)1#$37q*2J_}dhg@8g@ya`rb=jljfHNUGNQQ^ z(_-;KeaQ-&PRu4`^N~QkuLD_A^`I(1QB|dos>chSH{9(XINbltiNoFItH6EbZCOvX z<<>1%6LbrlL=;<1r|!{xvblYrs_GKOe)J}hF*X!qfNNzTkc1M_IYm7=tp((SaN=(0 zrwnZ9Q-^-$CVw5!uibA)Jl}408;+Q1JF(TOM{%R&t;`ym{K#uJF4I@>;jCeFBRW_6 zR@;d>mhA|sHR#men^73(qh9?Z0kPK3-(pY*pFD#M!F+ zSC5}w+S2o;PLOKO=N<^OmJL*=Qmf|Ww(rat` F26KRpUEcrz literal 0 HcmV?d00001 diff --git a/clean-snapshot.tar b/clean-snapshot.tar new file mode 100644 index 000000000..e69de29bb diff --git a/components/AnchorAnim.qml b/components/AnchorAnim.qml new file mode 100644 index 000000000..dd62dc366 --- /dev/null +++ b/components/AnchorAnim.qml @@ -0,0 +1,51 @@ +import QtQuick +import Caelestia.Config + +AnchorAnimation { + enum Type { + StandardSmall = 0, + Standard, + StandardLarge, + StandardExtraLarge, + EmphasizedSmall, + Emphasized, + EmphasizedLarge, + EmphasizedExtraLarge, + FastSpatial, + DefaultSpatial, + SlowSpatial + } + + property int type: AnchorAnim.DefaultSpatial + + duration: { + if (type < AnchorAnim.StandardSmall || type > AnchorAnim.SlowSpatial) + return Tokens.anim.durations.expressiveDefaultSpatial; + + if (type == AnchorAnim.FastSpatial) + return Tokens.anim.durations.expressiveFastSpatial; + if (type == AnchorAnim.DefaultSpatial) + return Tokens.anim.durations.expressiveDefaultSpatial; + if (type == AnchorAnim.SlowSpatial) + return Tokens.anim.durations.expressiveSlowSpatial; + + const types = ["small", "normal", "large", "extraLarge"]; + const idx = type % 4; // 0-7 are the 4 standard types + return Tokens.anim.durations[types[idx]]; + } + easing: { + if (type == AnchorAnim.FastSpatial) + return Tokens.anim.expressiveFastSpatial; + if (type == AnchorAnim.DefaultSpatial) + return Tokens.anim.expressiveDefaultSpatial; + if (type == AnchorAnim.SlowSpatial) + return Tokens.anim.expressiveSlowSpatial; + + if (type >= AnchorAnim.StandardSmall && type <= AnchorAnim.StandardExtraLarge) + return Tokens.anim.standard; + if (type >= AnchorAnim.EmphasizedSmall && type <= AnchorAnim.EmphasizedExtraLarge) + return Tokens.anim.emphasized; + + return Tokens.anim.expressiveDefaultSpatial; + } +} diff --git a/components/Anim.qml b/components/Anim.qml index 6883a7984..8bc4468ac 100644 --- a/components/Anim.qml +++ b/components/Anim.qml @@ -1,8 +1,48 @@ -import qs.config import QtQuick +import Caelestia.Config NumberAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + enum Type { + StandardSmall = 0, + Standard, + StandardLarge, + StandardExtraLarge, + EmphasizedSmall, + Emphasized, + EmphasizedLarge, + EmphasizedExtraLarge, + FastSpatial, + DefaultSpatial, + SlowSpatial + } + + property int type: Anim.Standard + + duration: { + if (type < Anim.StandardSmall || type > Anim.SlowSpatial) + return Tokens.anim.durations.normal; + + if (type == Anim.FastSpatial) + return Tokens.anim.durations.expressiveFastSpatial; + if (type == Anim.DefaultSpatial) + return Tokens.anim.durations.expressiveDefaultSpatial; + if (type == Anim.SlowSpatial) + return Tokens.anim.durations.expressiveSlowSpatial; + + const types = ["small", "normal", "large", "extraLarge"]; + const idx = type % 4; // 0-7 are the 4 standard types + return Tokens.anim.durations[types[idx]]; + } + easing: { + if (type == Anim.FastSpatial) + return Tokens.anim.expressiveFastSpatial; + if (type == Anim.DefaultSpatial) + return Tokens.anim.expressiveDefaultSpatial; + if (type == Anim.SlowSpatial) + return Tokens.anim.expressiveSlowSpatial; + + if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge) + return Tokens.anim.emphasized; + return Tokens.anim.standard; + } } diff --git a/components/CAnim.qml b/components/CAnim.qml index 49484b789..b86fcce1f 100644 --- a/components/CAnim.qml +++ b/components/CAnim.qml @@ -1,8 +1,7 @@ -import qs.config import QtQuick +import Caelestia.Config ColorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + duration: Tokens.anim.durations.normal + easing: Tokens.anim.standard } diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml index 12b427648..691fd3c50 100644 --- a/components/ConnectionHeader.qml +++ b/components/ConnectionHeader.qml @@ -1,8 +1,7 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components ColumnLayout { id: root @@ -10,14 +9,14 @@ ColumnLayout { required property string icon required property string title - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Layout.alignment: Qt.AlignHCenter MaterialIcon { Layout.alignment: Qt.AlignHCenter animate: true text: root.icon - font.pointSize: Appearance.font.size.extraLarge * 3 + font.pointSize: Tokens.font.size.extraLarge * 3 font.bold: true } @@ -25,7 +24,7 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter animate: true text: root.title - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.bold: true } } diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml index 927ef287d..d94c3a6e8 100644 --- a/components/ConnectionInfoSection.qml +++ b/components/ConnectionInfoSection.qml @@ -1,16 +1,15 @@ -import qs.components -import qs.components.effects -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root required property var deviceDetails - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: qsTr("IP Address") @@ -19,40 +18,40 @@ ColumnLayout { StyledText { text: root.deviceDetails?.ipAddress || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Subnet Mask") } StyledText { text: root.deviceDetails?.subnet || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Gateway") } StyledText { text: root.deviceDetails?.gateway || qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("DNS Servers") } StyledText { text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.Wrap Layout.maximumWidth: parent.width } diff --git a/components/DashboardState.qml b/components/DashboardState.qml new file mode 100644 index 000000000..b4355cd7b --- /dev/null +++ b/components/DashboardState.qml @@ -0,0 +1,6 @@ +import Quickshell + +PersistentProperties { + property int currentTab + property date currentDate: new Date() +} diff --git a/components/DrawerVisibilities.qml b/components/DrawerVisibilities.qml new file mode 100644 index 000000000..3286e319f --- /dev/null +++ b/components/DrawerVisibilities.qml @@ -0,0 +1,11 @@ +import Quickshell + +PersistentProperties { + property bool bar + property bool osd + property bool session + property bool launcher + property bool dashboard + property bool utilities + property bool sidebar +} diff --git a/components/Logo.qml b/components/Logo.qml new file mode 100644 index 000000000..7cd41e17f --- /dev/null +++ b/components/Logo.qml @@ -0,0 +1,70 @@ +import QtQuick +import QtQuick.Shapes +import qs.services + +Item { + id: root + + readonly property real designWidth: 128 + readonly property real designHeight: 90.38 + + property color topColour: Colours.palette.m3primary + property color bottomColour: Colours.palette.m3onSurface + + implicitWidth: designWidth + implicitHeight: designHeight + + Shape { + anchors.centerIn: parent + width: root.designWidth + height: root.designHeight + scale: Math.min(root.width / width, root.height / height) + transformOrigin: Item.Center + preferredRendererType: Shape.CurveRenderer + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" + } + } + + ShapePath { + fillColor: root.bottomColour + strokeColor: "transparent" + + PathSvg { + path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" + } + } + + ShapePath { + fillColor: root.topColour + strokeColor: "transparent" + + PathSvg { + path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" + } + } + } +} diff --git a/components/MaterialIcon.qml b/components/MaterialIcon.qml index a1d19d3c0..739c50bae 100644 --- a/components/MaterialIcon.qml +++ b/components/MaterialIcon.qml @@ -1,12 +1,12 @@ +import Caelestia.Config import qs.services -import qs.config StyledText { property real fill property int grade: Colours.light ? 0 : -25 - font.family: Appearance.font.family.material - font.pointSize: Appearance.font.size.larger + font.family: Tokens.font.family.material + font.pointSize: Tokens.font.size.larger font.variableAxes: ({ FILL: fill.toFixed(1), GRAD: grade, diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml index 640d5f743..4d3025095 100644 --- a/components/PropertyRow.qml +++ b/components/PropertyRow.qml @@ -1,8 +1,8 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root @@ -11,16 +11,16 @@ ColumnLayout { required property string value property bool showTopMargin: false - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { - Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 + Layout.topMargin: root.showTopMargin ? Tokens.spacing.normal : 0 text: root.label } StyledText { text: root.value color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml index 2b653a5d9..7aa3f13f1 100644 --- a/components/SectionContainer.qml +++ b/components/SectionContainer.qml @@ -1,21 +1,20 @@ -import qs.components -import qs.components.effects -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root default property alias content: contentColumn.data - property real contentSpacing: Appearance.spacing.larger + property real contentSpacing: Tokens.spacing.larger property bool alignTop: false Layout.fillWidth: true - implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 + implicitHeight: contentColumn.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh ColumnLayout { @@ -25,7 +24,7 @@ StyledRect { anchors.right: parent.right anchors.top: root.alignTop ? parent.top : undefined anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large spacing: root.contentSpacing } diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml index 502e91895..09364143f 100644 --- a/components/SectionHeader.qml +++ b/components/SectionHeader.qml @@ -1,8 +1,8 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root @@ -13,9 +13,9 @@ ColumnLayout { spacing: 0 StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: root.title - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } diff --git a/components/StateLayer.qml b/components/StateLayer.qml index a20e26616..2ce8c5008 100644 --- a/components/StateLayer.qml +++ b/components/StateLayer.qml @@ -1,95 +1,186 @@ -import qs.services -import qs.config import QtQuick +import QtQuick.Shapes +import Caelestia.Config +import qs.services MouseArea { id: root property bool disabled property bool showHoverBackground: true - property color color: Colours.palette.m3onSurface - property real radius: parent?.radius ?? 0 - property alias rect: hoverLayer + readonly property alias rect: base + + property bool shapeMorph + property real stateOpacity: pressed ? 0.1 : containsMouse ? 0.08 : 0 + + property real pressX: width / 2 + property real pressY: height / 2 + property real circleRadius + + property alias color: base.color + property alias radius: base.radius + property alias topLeftRadius: base.topLeftRadius + property alias topRightRadius: base.topRightRadius + property alias bottomLeftRadius: base.bottomLeftRadius + property alias bottomRightRadius: base.bottomRightRadius + + readonly property real endRadius: { + const d1 = distSq(0, 0); + const d2 = distSq(width, 0); + const d3 = distSq(0, height); + const d4 = distSq(width, height); + return Math.sqrt(Math.max(d1, d2, d3, d4)) * (shapeMorph ? 1.16 : 1); + } + property real endRadiusAtPress - function onClicked(): void { + function distSq(x: real, y: real): real { + return (pressX - x) ** 2 + (pressY - y) ** 2; } - anchors.fill: parent + function clamp(r: real): real { + return Math.max(0, Math.min(r, width / 2, height / 2)); + } + function press(x: real, y: real): void { + pressX = x; + pressY = y; + fadeAnim.complete(); + circleRadius = 0; + circle.opacity = 0.1; + rippleAnim.restart(); + endRadiusAtPress = endRadius; + } + + anchors.fill: parent enabled: !disabled cursorShape: disabled ? undefined : Qt.PointingHandCursor hoverEnabled: true - onPressed: event => { - if (disabled) - return; + onPressed: e => press(e.x, e.y) - rippleAnim.x = event.x; - rippleAnim.y = event.y; - - const dist = (ox, oy) => ox * ox + oy * oy; - rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); - - rippleAnim.restart(); + onPressedChanged: { + if (!pressed && !rippleAnim.running && circle.opacity > 0) + fadeAnim.start(); } - onClicked: event => !disabled && onClicked(event) + onCircleRadiusChanged: { + if (!pressed && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) + fadeAnim.start(); + } - SequentialAnimation { + Anim { id: rippleAnim - property real x - property real y - property real radius + alwaysRunToEnd: true + target: root + property: "circleRadius" + to: root.endRadius + easing: Tokens.anim.expressiveSlowEffects + duration: Tokens.anim.durations.expressiveSlowEffects * 2 + } - PropertyAction { - target: ripple - property: "x" - value: rippleAnim.x - } - PropertyAction { - target: ripple - property: "y" - value: rippleAnim.y - } - PropertyAction { - target: ripple - property: "opacity" - value: 0.08 - } - Anim { - target: ripple - properties: "implicitWidth,implicitHeight" - from: 0 - to: rippleAnim.radius * 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - target: ripple - property: "opacity" - to: 0 - } + Anim { + id: fadeAnim + + target: circle + property: "opacity" + to: 0 + easing: Tokens.anim.expressiveSlowEffects + duration: Tokens.anim.durations.expressiveSlowEffects } - StyledClippingRect { - id: hoverLayer + StyledRect { + id: base anchors.fill: parent + opacity: root.stateOpacity + color: Colours.palette.m3onSurface + // Pick up radius from parent if it has one (parent can be anything with a radius property) + radius: root.parent?.radius ?? 0 // qmllint disable missing-property + } - color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) - radius: root.radius + Shape { + id: circle - StyledRect { - id: ripple + anchors.fill: parent + opacity: 0 + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 0 + strokeColor: "transparent" + fillColor: base.color + fillGradient: RadialGradient { + centerX: root.pressX + centerY: root.pressY + centerRadius: root.circleRadius + focalX: centerX + focalY: centerY + + GradientStop { + position: 0 + color: Qt.alpha(base.color, 1) + } + GradientStop { + position: 0.99 + color: Qt.alpha(base.color, 1) + } + GradientStop { + position: 1 + color: Qt.alpha(base.color, 0) + } + } - radius: Appearance.rounding.full - color: root.color - opacity: 0 + startX: root.clamp(base.topLeftRadius) + startY: 0 - transform: Translate { - x: -ripple.width / 2 - y: -ripple.height / 2 + PathLine { + x: root.width - root.clamp(base.topLeftRadius) + y: 0 + } + PathArc { + relativeX: root.clamp(base.topLeftRadius) + relativeY: root.clamp(base.topLeftRadius) + radiusX: root.clamp(base.topLeftRadius) + radiusY: root.clamp(base.topLeftRadius) + } + PathLine { + x: root.width + y: root.height - root.clamp(base.bottomRightRadius) } + PathArc { + relativeX: -root.clamp(base.bottomRightRadius) + relativeY: root.clamp(base.bottomRightRadius) + radiusX: root.clamp(base.bottomRightRadius) + radiusY: root.clamp(base.bottomRightRadius) + } + PathLine { + x: root.clamp(base.bottomLeftRadius) + y: root.height + } + PathArc { + relativeX: -root.clamp(base.bottomLeftRadius) + relativeY: -root.clamp(base.bottomLeftRadius) + radiusX: root.clamp(base.bottomLeftRadius) + radiusY: root.clamp(base.bottomLeftRadius) + } + PathLine { + x: 0 + y: root.clamp(base.topLeftRadius) + } + PathArc { + x: root.clamp(base.topLeftRadius) + y: 0 + radiusX: root.clamp(base.topLeftRadius) + radiusY: root.clamp(base.topLeftRadius) + } + } + } + + Behavior on stateOpacity { + Anim { + easing: Tokens.anim.expressiveDefaultEffects + duration: Tokens.anim.durations.expressiveDefaultEffects } } } diff --git a/components/StyledClippingRect.qml b/components/StyledClippingRect.qml index 8f2630c13..6484e0e6f 100644 --- a/components/StyledClippingRect.qml +++ b/components/StyledClippingRect.qml @@ -1,5 +1,5 @@ -import Quickshell.Widgets import QtQuick +import Quickshell.Widgets ClippingRectangle { id: root diff --git a/components/StyledText.qml b/components/StyledText.qml index ed961d26a..d3624c58c 100644 --- a/components/StyledText.qml +++ b/components/StyledText.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound -import qs.services -import qs.config import QtQuick +import Caelestia.Config +import qs.services Text { id: root @@ -11,13 +11,13 @@ Text { property string animateProp: "scale" property real animateFrom: 0 property real animateTo: 1 - property int animateDuration: Appearance.anim.durations.normal + property int animateDuration: Tokens.anim.durations.normal renderType: Text.NativeRendering textFormat: Text.PlainText color: Colours.palette.m3onSurface - font.family: Appearance.font.family.sans - font.pointSize: Appearance.font.size.smaller + font.family: Tokens.font.family.sans + font.pointSize: Tokens.font.size.smaller Behavior on color { CAnim {} @@ -29,12 +29,12 @@ Text { SequentialAnimation { Anim { to: root.animateFrom - easing.bezierCurve: Appearance.anim.curves.standardAccel + easing: Tokens.anim.standardAccel } PropertyAction {} Anim { to: root.animateTo - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } } @@ -43,6 +43,5 @@ Text { target: root property: root.animateProp duration: root.animateDuration / 2 - easing.type: Easing.BezierSpline } } diff --git a/components/containers/StyledFlickable.qml b/components/containers/StyledFlickable.qml index bc6ae0f62..b792e5833 100644 --- a/components/containers/StyledFlickable.qml +++ b/components/containers/StyledFlickable.qml @@ -1,5 +1,5 @@ -import ".." import QtQuick +import qs.components Flickable { id: root diff --git a/components/containers/StyledListView.qml b/components/containers/StyledListView.qml index 626d20635..8b5a4401e 100644 --- a/components/containers/StyledListView.qml +++ b/components/containers/StyledListView.qml @@ -1,5 +1,5 @@ -import ".." import QtQuick +import qs.components ListView { id: root diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml index 8c6e39fc8..72bbed6f1 100644 --- a/components/containers/StyledWindow.qml +++ b/components/containers/StyledWindow.qml @@ -1,9 +1,15 @@ import Quickshell import Quickshell.Wayland +import Caelestia.Config +// qmllint disable uncreatable-type PanelWindow { + // qmllint enable uncreatable-type required property string name WlrLayershell.namespace: `caelestia-${name}` color: "transparent" + + contentItem.Config.screen: screen.name + contentItem.Tokens.screen: screen.name } diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml index 957899e5c..aabda13a7 100644 --- a/components/controls/CircularIndicator.qml +++ b/components/controls/CircularIndicator.qml @@ -1,9 +1,9 @@ -import ".." -import qs.services -import qs.config -import Caelestia.Internal import QtQuick import QtQuick.Templates +import Caelestia.Config +import Caelestia.Internal +import qs.components +import qs.services BusyIndicator { id: root @@ -19,8 +19,8 @@ BusyIndicator { Completing } - property real implicitSize: Appearance.font.size.normal * 3 - property real strokeWidth: Appearance.padding.small * 0.8 + property real implicitSize: Tokens.font.size.normal * 3 + property real strokeWidth: Tokens.padding.small * 0.8 property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer @@ -64,7 +64,7 @@ BusyIndicator { transitions: Transition { Anim { properties: "opacity,internalStrokeWidth" - duration: manager.completeEndDuration * Appearance.anim.durations.scale + duration: manager.completeEndDuration * Tokens.anim.durations.scale } } @@ -90,7 +90,7 @@ BusyIndicator { property: "progress" from: 0 to: 1 - duration: manager.duration * Appearance.anim.durations.scale + duration: manager.duration * Tokens.anim.durations.scale } NumberAnimation { @@ -99,7 +99,7 @@ BusyIndicator { property: "completeEndProgress" from: 0 to: 1 - duration: manager.completeEndDuration * Appearance.anim.durations.scale + duration: manager.completeEndDuration * Tokens.anim.durations.scale onFinished: { if (root.animState === CircularIndicator.Completing) root.animState = CircularIndicator.Stopped; diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml index a15cd900b..2372c86cd 100644 --- a/components/controls/CircularProgress.qml +++ b/components/controls/CircularProgress.qml @@ -1,17 +1,17 @@ -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Shapes +import Caelestia.Config +import qs.components +import qs.services Shape { id: root property real value property int startAngle: -90 - property int strokeWidth: Appearance.padding.smaller + property int strokeWidth: Tokens.padding.smaller property int padding: 0 - property int spacing: Appearance.spacing.small + property int spacing: Tokens.spacing.small property color fgColour: Colours.palette.m3primary property color bgColour: Colours.palette.m3secondaryContainer @@ -27,7 +27,7 @@ Shape { fillColor: "transparent" strokeColor: root.bgColour strokeWidth: root.strokeWidth - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle + 360 * root.vValue + root.gapAngle @@ -40,7 +40,7 @@ Shape { Behavior on strokeColor { CAnim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } @@ -49,7 +49,7 @@ Shape { fillColor: "transparent" strokeColor: root.fgColour strokeWidth: root.strokeWidth - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { startAngle: root.startAngle @@ -62,7 +62,7 @@ Shape { Behavior on strokeColor { CAnim { - duration: Appearance.anim.durations.large + duration: Tokens.anim.durations.large } } } diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml index e3d8eefd1..80ea8fe8d 100644 --- a/components/controls/CollapsibleSection.qml +++ b/components/controls/CollapsibleSection.qml @@ -1,10 +1,8 @@ -import ".." -import qs.components -import qs.components.effects -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root @@ -15,28 +13,32 @@ ColumnLayout { property bool showBackground: false property bool nested: false + default property alias content: contentColumn.data + signal toggleRequested - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Layout.fillWidth: true Item { id: sectionHeaderItem + Layout.fillWidth: true - Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) + Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.normal * 2, 48) RowLayout { id: titleRow + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.leftMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.normal + spacing: Tokens.spacing.normal StyledText { text: root.title - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -48,11 +50,11 @@ ColumnLayout { text: "expand_more" rotation: root.expanded ? 180 : 0 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal + Behavior on rotation { Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standard + type: Anim.StandardSmall } } } @@ -61,70 +63,66 @@ ColumnLayout { StateLayer { anchors.fill: parent color: Colours.palette.m3onSurface - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal showHoverBackground: false - function onClicked(): void { + onClicked: { root.toggleRequested(); root.expanded = !root.expanded; } } } - default property alias content: contentColumn.data - Item { id: contentWrapper + Layout.fillWidth: true - Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 + Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.small * 2) : 0 clip: true Behavior on Layout.preferredHeight { - Anim { - easing.bezierCurve: Appearance.anim.curves.standard - } + Anim {} } StyledRect { id: backgroundRect + anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) opacity: root.showBackground && root.expanded ? 1.0 : 0.0 visible: root.showBackground Behavior on opacity { - Anim { - easing.bezierCurve: Appearance.anim.curves.standard - } + Anim {} } } ColumnLayout { id: contentColumn + anchors.left: parent.left anchors.right: parent.right - y: Appearance.spacing.small - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - anchors.bottomMargin: Appearance.spacing.small - spacing: Appearance.spacing.small + y: Tokens.spacing.small + anchors.leftMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.normal + anchors.bottomMargin: Tokens.spacing.small + spacing: Tokens.spacing.small opacity: root.expanded ? 1.0 : 0.0 Behavior on opacity { - Anim { - easing.bezierCurve: Appearance.anim.curves.standard - } + Anim {} } StyledText { id: descriptionText + Layout.fillWidth: true - Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 - Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 + Layout.topMargin: root.description !== "" ? Tokens.spacing.smaller : 0 + Layout.bottomMargin: root.description !== "" ? Tokens.spacing.small : 0 visible: root.description !== "" text: root.description color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.Wrap } } diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml index 438dc0806..d7885c30b 100644 --- a/components/controls/CustomSpinBox.qml +++ b/components/controls/CustomSpinBox.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services RowLayout { id: root @@ -15,13 +15,13 @@ RowLayout { property real step: 1 property alias repeatRate: timer.interval - signal valueModified(value: real) - - spacing: Appearance.spacing.small - property bool isEditing: false property string displayText: root.value.toString() + signal valueModified(value: real) + + spacing: Tokens.spacing.small + onValueChanged: { if (!root.isEditing) { root.displayText = root.value.toString(); @@ -73,23 +73,23 @@ RowLayout { root.isEditing = false; } - padding: Appearance.padding.small - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal + padding: Tokens.padding.small + leftPadding: Tokens.padding.normal + rightPadding: Tokens.padding.normal background: StyledRect { implicitWidth: 100 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainerHigh } } StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { id: upState @@ -99,7 +99,7 @@ RowLayout { onPressAndHold: timer.start() onReleased: timer.stop() - function onClicked(): void { + onClicked: { let newValue = Math.min(root.max, root.value + root.step); // Round to avoid floating point precision errors const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; @@ -120,21 +120,16 @@ RowLayout { } StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3primary implicitWidth: implicitHeight - implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: downIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { id: downState - color: Colours.palette.m3onPrimary - - onPressAndHold: timer.start() - onReleased: timer.stop() - - function onClicked(): void { + onClicked: { let newValue = Math.max(root.min, root.value - root.step); // Round to avoid floating point precision errors const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; @@ -143,6 +138,11 @@ RowLayout { root.displayText = newValue.toString(); root.valueModified(newValue); } + + color: Colours.palette.m3onPrimary + + onPressAndHold: timer.start() + onReleased: timer.stop() } MaterialIcon { @@ -162,9 +162,9 @@ RowLayout { triggeredOnStart: true onTriggered: { if (upState.pressed) - upState.onClicked(); + upState.clicked(); else if (downState.pressed) - downState.onClicked(); + downState.clicked(); } } } diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml index 80dd44c5f..1d8c85a9c 100644 --- a/components/controls/FilledSlider.qml +++ b/components/controls/FilledSlider.qml @@ -1,9 +1,9 @@ -import ".." import "../effects" -import qs.services -import qs.config import QtQuick import QtQuick.Templates +import Caelestia.Config +import qs.components +import qs.services Slider { id: root @@ -16,7 +16,7 @@ Slider { background: StyledRect { color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StyledRect { anchors.left: parent.left @@ -51,7 +51,7 @@ Slider { anchors.fill: parent color: Colours.palette.m3inverseSurface - radius: Appearance.rounding.full + radius: Tokens.rounding.full MouseArea { id: handleInteraction @@ -70,8 +70,8 @@ Slider { function update(): void { animate = !moving; binding.when = moving; - font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; - font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; + font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger; + font.family = moving ? Tokens.font.family.sans : Tokens.font.family.material; } text: root.icon @@ -96,8 +96,8 @@ Slider { target: icon property: "scale" to: 0 - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardAccel } ScriptAction { script: icon.update() @@ -106,8 +106,8 @@ Slider { target: icon property: "scale" to: 1 - duration: Appearance.anim.durations.normal / 2 - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardDecel } } } @@ -140,7 +140,7 @@ Slider { Behavior on value { Anim { - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml index ffb1d0663..58af806a6 100644 --- a/components/controls/IconButton.qml +++ b/components/controls/IconButton.qml @@ -1,7 +1,7 @@ -import ".." -import qs.services -import qs.config import QtQuick +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root @@ -15,7 +15,7 @@ StyledRect { property alias icon: label.text property bool checked property bool toggle - property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller + property real padding: type === IconButton.Text ? Tokens.padding.small / 2 : Tokens.padding.smaller property alias font: label.font property int type: IconButton.Filled property bool disabled @@ -44,7 +44,7 @@ StyledRect { onCheckedChanged: internalChecked = checked - radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour implicitWidth: implicitHeight @@ -55,8 +55,7 @@ StyledRect { color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour disabled: root.disabled - - function onClicked(): void { + onClicked: { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml index b2bb96cc0..2a1dacec7 100644 --- a/components/controls/IconTextButton.qml +++ b/components/controls/IconTextButton.qml @@ -1,8 +1,8 @@ -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root @@ -17,8 +17,8 @@ StyledRect { property alias text: label.text property bool checked property bool toggle - property real horizontalPadding: Appearance.padding.normal - property real verticalPadding: Appearance.padding.smaller + property real horizontalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.smaller property alias font: label.font property int type: IconTextButton.Filled @@ -36,7 +36,7 @@ StyledRect { onCheckedChanged: internalChecked = checked - radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour implicitWidth: row.implicitWidth + horizontalPadding * 2 @@ -46,8 +46,7 @@ StyledRect { id: stateLayer color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - - function onClicked(): void { + onClicked: { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); @@ -58,7 +57,7 @@ StyledRect { id: row anchors.centerIn: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { id: iconLabel diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index c763b54a8..482a4d409 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -1,95 +1,187 @@ pragma ComponentBehavior: Bound -import ".." -import "../effects" -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.components.effects +import qs.services +import qs.modules.drawers -Elevation { +MouseArea { id: root + enum Side { + Top, + Bottom, + Left, + Right + } + + required property Item attachTo + property int attachSideX: Menu.Right + property int attachSideY: Menu.Bottom + property int thisSideX: Menu.Right + property int thisSideY: Menu.Top + property real marginX + property real marginY + property list items property MenuItem active: items[0] ?? null property bool expanded signal itemSelected(item: MenuItem) - radius: Appearance.rounding.small / 2 - level: 2 + parent: { + const win = QsWindow.window; + const contentWin = win as ContentWindow; // If inside the drawer content window, put it inside the interaction wrapper so hover works + return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem; + } + anchors.fill: parent - implicitWidth: Math.max(200, column.implicitWidth) - implicitHeight: root.expanded ? column.implicitHeight : 0 - opacity: root.expanded ? 1 : 0 + enabled: expanded + onClicked: expanded = false - StyledClippingRect { - anchors.fill: parent - radius: parent.radius - color: Colours.palette.m3surfaceContainer + opacity: expanded ? 1 : 0 + layer.enabled: opacity < 1 - ColumnLayout { - id: column + Behavior on opacity { + Anim { + duration: Tokens.anim.durations.small + } + } - anchors.left: parent.left - anchors.right: parent.right - spacing: 0 + TransformWatcher { + id: watcher - Repeater { - model: root.items + a: root.parent + b: root.attachTo + } - StyledRect { - id: item + Elevation { + id: menu - required property int index - required property MenuItem modelData - readonly property bool active: modelData === root.active + x: { + watcher.transform; // mapToItem is not reactive so this forces updates + const item = root.attachTo; + let off = root.attachSideX === Menu.Left ? 0 : item.width; + if (root.thisSideX === Menu.Right) + off -= width; + return item.mapToItem(root.parent, off, 0).x + root.marginX; + } + y: { + watcher.transform; // mapToItem is not reactive so this forces updates + const item = root.attachTo; + let off = root.attachSideY === Menu.Top ? 0 : item.height; + if (root.thisSideY === Menu.Bottom) + off -= height; + return item.mapToItem(root.parent, 0, off).y + root.marginY; + } - Layout.fillWidth: true - implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 + radius: Tokens.rounding.normal + level: 2 - color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) + implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2) + implicitHeight: column.implicitHeight + column.anchors.margins * 2 - StateLayer { - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - disabled: !root.expanded + transform: Scale { + yScale: root.expanded ? 1 : 0.1 + origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0 - function onClicked(): void { - root.itemSelected(item.modelData); - root.active = item.modelData; - root.expanded = false; - } - } + Behavior on yScale { + Anim { + type: Anim.DefaultSpatial + } + } + } + + StyledRect { + anchors.fill: parent + radius: parent.radius + color: Colours.palette.m3surfaceContainerLow + + ColumnLayout { + id: column + + anchors.fill: parent + anchors.margins: Tokens.padding.small + spacing: 0 - RowLayout { - id: menuOptionRow + Repeater { + id: repeater - anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small + model: root.items - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - text: item.modelData.icon - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant + StyledRect { + id: item + + required property int index + required property MenuItem modelData + readonly property bool active: modelData === root.active + + Layout.fillWidth: true + implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 + + radius: active ? 12 : Tokens.rounding.extraSmall // This should use a token, but tokens are currently extremely scuffed + topLeftRadius: index === 0 ? Tokens.rounding.small : radius + topRightRadius: index === 0 ? Tokens.rounding.small : radius + bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius + bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius + + color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0) + + Behavior on radius { + Anim {} } - StyledText { - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - text: item.modelData.text - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + StateLayer { + topLeftRadius: parent.topLeftRadius + topRightRadius: parent.topRightRadius + bottomLeftRadius: parent.bottomLeftRadius + bottomRightRadius: parent.bottomRightRadius + + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface + disabled: !root.expanded + onClicked: { + root.itemSelected(item.modelData); + root.active = item.modelData; + item.modelData.clicked(); + root.expanded = false; + } } - Loader { - Layout.alignment: Qt.AlignVCenter - active: item.modelData.trailingIcon.length > 0 - visible: active + RowLayout { + id: menuOptionRow - sourceComponent: MaterialIcon { - text: item.modelData.trailingIcon - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + anchors.fill: parent + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: item.modelData.icon + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant + } + + StyledText { + Layout.alignment: Qt.AlignVCenter + Layout.fillWidth: true + text: item.modelData.text + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface + } + + Loader { + asynchronous: true + Layout.alignment: Qt.AlignVCenter + active: item.modelData.trailingIcon.length > 0 + visible: active + + sourceComponent: MaterialIcon { + text: item.modelData.trailingIcon + color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant + } } } } @@ -97,17 +189,4 @@ Elevation { } } } - - Behavior on opacity { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - } - } - - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } } diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml index fe6a19822..dc52c19c1 100644 --- a/components/controls/SpinBoxRow.qml +++ b/components/controls/SpinBoxRow.qml @@ -1,10 +1,8 @@ -import ".." -import qs.components -import qs.components.effects -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root @@ -17,8 +15,8 @@ StyledRect { property var onValueModified: function (value) {} Layout.fillWidth: true - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -31,8 +29,8 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -45,7 +43,7 @@ StyledRect { step: root.step value: root.value onValueModified: value => { - root.onValueModified(value); + root.onValueModified(value); // qmllint disable use-proper-function } } } diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml index c91474eae..4acd98cf6 100644 --- a/components/controls/SplitButton.qml +++ b/components/controls/SplitButton.qml @@ -1,8 +1,8 @@ -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services Row { id: root @@ -12,8 +12,8 @@ Row { Tonal } - property real horizontalPadding: Appearance.padding.normal - property real verticalPadding: Appearance.padding.smaller + property real horizontalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.smaller property int type: SplitButton.Filled property bool disabled property bool menuOnTop @@ -33,12 +33,12 @@ Row { property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) - spacing: Math.floor(Appearance.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.small / 2) StyledRect { - radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) - topRightRadius: Appearance.rounding.small / 2 - bottomRightRadius: Appearance.rounding.small / 2 + radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) + topRightRadius: Tokens.rounding.small / 2 + bottomRightRadius: Tokens.rounding.small / 2 color: root.disabled ? root.disabledColour : root.colour implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 @@ -51,10 +51,7 @@ Row { rect.bottomRightRadius: parent.bottomRightRadius color: root.textColour disabled: root.disabled - - function onClicked(): void { - root.active?.clicked(); - } + onClicked: root.active?.clicked() } RowLayout { @@ -62,7 +59,7 @@ Row { anchors.centerIn: parent anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { id: iconLabel @@ -86,7 +83,7 @@ Row { Behavior on Layout.preferredWidth { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } } @@ -96,9 +93,9 @@ Row { StyledRect { id: expandBtn - property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2 + property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.small / 2 - radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) topLeftRadius: rad bottomLeftRadius: rad color: root.disabled ? root.disabledColour : root.colour @@ -113,10 +110,7 @@ Row { rect.bottomLeftRadius: parent.bottomLeftRadius color: root.textColour disabled: root.disabled - - function onClicked(): void { - root.expanded = !root.expanded; - } + onClicked: root.expanded = !root.expanded } MaterialIcon { @@ -141,24 +135,14 @@ Row { Behavior on rad { Anim {} } + } - Menu { - id: menu - - states: State { - when: root.menuOnTop - - AnchorChanges { - target: menu - anchors.top: undefined - anchors.bottom: expandBtn.top - } - } + Menu { + id: menu - anchors.top: parent.bottom - anchors.right: parent.right - anchors.topMargin: Appearance.spacing.small - anchors.bottomMargin: Appearance.spacing.small - } + attachTo: expandBtn + attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom + thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top + marginY: Tokens.spacing.small * (root.menuOnTop ? -1 : 1) } } diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml index db9925ff6..bd1ecc622 100644 --- a/components/controls/SplitButtonRow.qml +++ b/components/controls/SplitButtonRow.qml @@ -1,19 +1,16 @@ pragma ComponentBehavior: Bound -import ".." -import qs.components -import qs.components.effects -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root required property string label property int expandedZ: 100 - property bool enabled: true property alias menuItems: splitButton.menuItems property alias active: splitButton.active @@ -23,8 +20,8 @@ StyledRect { signal selected(item: MenuItem) Layout.fillWidth: true - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) clip: false @@ -33,9 +30,10 @@ StyledRect { RowLayout { id: row + anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -45,6 +43,7 @@ StyledRect { SplitButton { id: splitButton + enabled: root.enabled type: SplitButton.Filled diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml index 0d199c738..ff7772cac 100644 --- a/components/controls/StyledInputField.qml +++ b/components/controls/StyledInputField.qml @@ -1,10 +1,9 @@ pragma ComponentBehavior: Bound -import ".." +import QtQuick +import Caelestia.Config import qs.components import qs.services -import qs.config -import QtQuick Item { id: root @@ -13,23 +12,23 @@ Item { property var validator: null property bool readOnly: false property int horizontalAlignment: TextInput.AlignHCenter - property int implicitWidth: 70 - property bool enabled: true // Expose activeFocus through alias to avoid FINAL property override readonly property alias hasFocus: inputField.activeFocus signal textEdited(string text) + signal editingFinished - implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 + implicitWidth: 70 + implicitHeight: inputField.implicitHeight + Tokens.padding.small * 2 StyledRect { id: container anchors.fill: parent color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) opacity: root.enabled ? 1 : 0.5 @@ -43,6 +42,7 @@ Item { MouseArea { id: inputHover + anchors.fill: parent hoverEnabled: true cursorShape: Qt.IBeamCursor @@ -52,20 +52,14 @@ Item { StyledTextField { id: inputField + anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: root.horizontalAlignment validator: root.validator readOnly: root.readOnly enabled: root.enabled - Binding { - target: inputField - property: "text" - value: root.text - when: !inputField.activeFocus - } - onTextChanged: { root.text = text; root.textEdited(text); @@ -74,6 +68,13 @@ Item { onEditingFinished: { root.editingFinished(); } + + Binding { + target: inputField + property: "text" + value: root.text + when: !inputField.activeFocus + } } } } diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml index b72fc77f3..24c7d6e8b 100644 --- a/components/controls/StyledRadioButton.qml +++ b/components/controls/StyledRadioButton.qml @@ -1,13 +1,13 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Templates +import Caelestia.Config +import qs.components +import qs.services RadioButton { id: root - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) @@ -17,20 +17,17 @@ RadioButton { implicitWidth: 20 implicitHeight: 20 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: "transparent" border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant border.width: 2 anchors.verticalCenter: parent.verticalCenter StateLayer { - anchors.margins: -Appearance.padding.smaller + anchors.margins: -Tokens.padding.smaller color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary z: -1 - - function onClicked(): void { - root.click(); - } + onClicked: root.click() } StyledRect { @@ -38,7 +35,7 @@ RadioButton { implicitWidth: 8 implicitHeight: 8 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0) } @@ -52,6 +49,6 @@ RadioButton { font.pointSize: root.font.pointSize anchors.verticalCenter: parent.verticalCenter anchors.left: outerCircle.right - anchors.leftMargin: Appearance.spacing.smaller + anchors.leftMargin: Tokens.spacing.smaller } } diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml index de8b679cd..8cfe3f8da 100644 --- a/components/controls/StyledScrollBar.qml +++ b/components/controls/StyledScrollBar.qml @@ -1,8 +1,8 @@ -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Templates +import Caelestia.Config +import qs.components +import qs.services ScrollBar { id: root @@ -11,6 +11,8 @@ ScrollBar { property bool shouldBeActive property real nonAnimPosition property bool animating + property bool _updatingFromFlickable: false + property bool _updatingFromUser: false onHoveredChanged: { if (hovered) @@ -19,9 +21,6 @@ ScrollBar { shouldBeActive = flickable.moving; } - property bool _updatingFromFlickable: false - property bool _updatingFromUser: false - // Sync nonAnimPosition with Qt's automatic position binding onPositionChanged: { if (_updatingFromUser) { @@ -37,24 +36,6 @@ ScrollBar { } } - // Sync nonAnimPosition with flickable when not animating - Connections { - target: flickable - function onContentYChanged() { - if (!animating && !fullMouse.pressed) { - _updatingFromFlickable = true; - const contentHeight = flickable.contentHeight; - const height = flickable.height; - if (contentHeight > height) { - nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); - } else { - nonAnimPosition = 0; - } - _updatingFromFlickable = false; - } - } - } - Component.onCompleted: { if (flickable) { const contentHeight = flickable.contentHeight; @@ -64,7 +45,7 @@ ScrollBar { } } } - implicitWidth: Appearance.padding.small + implicitWidth: Tokens.padding.small contentItem: StyledRect { anchors.left: parent.left @@ -80,7 +61,7 @@ ScrollBar { return 0.6; return 0; } - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3secondary MouseArea { @@ -97,15 +78,34 @@ ScrollBar { } } + // Sync nonAnimPosition with flickable when not animating Connections { + function onContentYChanged() { + if (!root.animating && !fullMouse.pressed) { + root._updatingFromFlickable = true; + const contentHeight = root.flickable.contentHeight; + const height = root.flickable.height; + if (contentHeight > height) { + root.nonAnimPosition = Math.max(0, Math.min(1, root.flickable.contentY / (contentHeight - height))); + } else { + root.nonAnimPosition = 0; + } + root._updatingFromFlickable = false; + } + } + target: root.flickable + } + Connections { function onMovingChanged(): void { if (root.flickable.moving) root.shouldBeActive = true; else hideDelay.restart(); } + + target: root.flickable } Timer { @@ -118,13 +118,14 @@ ScrollBar { CustomMouseArea { id: fullMouse - anchors.fill: parent - preventStealing: true - - onPressed: event => { + function onWheel(event: WheelEvent): void { root.animating = true; root._updatingFromUser = true; - const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); + let newPos = root.nonAnimPosition; + if (event.angleDelta.y > 0) + newPos = Math.max(0, root.nonAnimPosition - 0.1); + else if (event.angleDelta.y < 0) + newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); root.nonAnimPosition = newPos; // Update flickable position // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] @@ -140,7 +141,11 @@ ScrollBar { } } - onPositionChanged: event => { + anchors.fill: parent + preventStealing: true + + onPressed: event => { + root.animating = true; root._updatingFromUser = true; const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); root.nonAnimPosition = newPos; @@ -158,14 +163,9 @@ ScrollBar { } } - function onWheel(event: WheelEvent): void { - root.animating = true; + onPositionChanged: event => { root._updatingFromUser = true; - let newPos = root.nonAnimPosition; - if (event.angleDelta.y > 0) - newPos = Math.max(0, root.nonAnimPosition - 0.1); - else if (event.angleDelta.y < 0) - newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); + const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); root.nonAnimPosition = newPos; // Update flickable position // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml index 0ef229df2..f169691b4 100644 --- a/components/controls/StyledSlider.qml +++ b/components/controls/StyledSlider.qml @@ -1,8 +1,8 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Templates +import Caelestia.Config +import qs.components +import qs.services Slider { id: root @@ -18,7 +18,7 @@ Slider { implicitWidth: root.handle.x - root.implicitHeight / 6 color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full topRightRadius: root.implicitHeight / 15 bottomRightRadius: root.implicitHeight / 15 } @@ -33,7 +33,7 @@ Slider { implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 color: Colours.palette.m3surfaceContainerHighest - radius: Appearance.rounding.full + radius: Tokens.rounding.full topLeftRadius: root.implicitHeight / 15 bottomLeftRadius: root.implicitHeight / 15 } @@ -46,7 +46,7 @@ Slider { implicitHeight: root.implicitHeight color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full MouseArea { anchors.fill: parent diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml index ce93cd505..2e31adbff 100644 --- a/components/controls/StyledSwitch.qml +++ b/components/controls/StyledSwitch.qml @@ -1,9 +1,9 @@ -import ".." -import qs.services -import qs.config import QtQuick -import QtQuick.Templates import QtQuick.Shapes +import QtQuick.Templates +import Caelestia.Config +import qs.components +import qs.services Switch { id: root @@ -14,21 +14,21 @@ Switch { implicitHeight: implicitIndicatorHeight indicator: StyledRect { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) implicitWidth: implicitHeight * 1.7 - implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 + implicitHeight: Tokens.font.size.normal + Tokens.padding.smaller * 2 StyledRect { readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) - x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 + x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.small / 2 : Tokens.padding.small / 2 implicitWidth: nonAnimWidth - implicitHeight: parent.implicitHeight - Appearance.padding.small + implicitHeight: parent.implicitHeight - Tokens.padding.small anchors.verticalCenter: parent.verticalCenter StyledRect { @@ -83,15 +83,15 @@ Switch { anchors.centerIn: parent width: height - height: parent.implicitHeight - Appearance.padding.small * 2 + height: parent.implicitHeight - Tokens.padding.small * 2 preferredRendererType: Shape.CurveRenderer asynchronous: true ShapePath { - strokeWidth: Appearance.font.size.larger * 0.15 + strokeWidth: root.Tokens.font.size.larger * 0.15 strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest fillColor: "transparent" - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap startX: icon.start1.x startY: icon.start1.y @@ -145,8 +145,7 @@ Switch { } component PropAnim: PropertyAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + duration: Tokens.anim.durations.normal + easing: Tokens.anim.standard } } diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml index 60bcff259..4ab2f824c 100644 --- a/components/controls/StyledTextField.qml +++ b/components/controls/StyledTextField.qml @@ -1,18 +1,18 @@ pragma ComponentBehavior: Bound -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Controls +import Caelestia.Config +import qs.components +import qs.services TextField { id: root color: Colours.palette.m3onSurface placeholderTextColor: Colours.palette.m3outline - font.family: Appearance.font.family.sans - font.pointSize: Appearance.font.size.smaller + font.family: Tokens.font.family.sans + font.pointSize: Tokens.font.size.smaller renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering cursorVisible: !readOnly @@ -25,11 +25,9 @@ TextField { implicitWidth: 2 color: Colours.palette.m3primary - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Connections { - target: root - function onCursorPositionChanged(): void { if (root.activeFocus && root.cursorVisible) { cursor.opacity = 1; @@ -37,6 +35,8 @@ TextField { enableBlink.restart(); } } + + target: root } Timer { @@ -61,7 +61,7 @@ TextField { Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml index 6dda3f0cc..b909739fe 100644 --- a/components/controls/SwitchRow.qml +++ b/components/controls/SwitchRow.qml @@ -1,22 +1,20 @@ -import ".." -import qs.components -import qs.components.effects -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root required property string label required property bool checked - property bool enabled: true - property var onToggled: function (checked) {} + + signal toggled(checked: bool) Layout.fillWidth: true - implicitHeight: row.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal + implicitHeight: row.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) Behavior on implicitHeight { @@ -29,8 +27,8 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -40,9 +38,7 @@ StyledRect { StyledSwitch { checked: root.checked enabled: root.enabled - onToggled: { - root.onToggled(checked); - } + onToggled: root.toggled(checked) } } } diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml index ecf7eb133..94d76ff86 100644 --- a/components/controls/TextButton.qml +++ b/components/controls/TextButton.qml @@ -1,7 +1,7 @@ -import ".." -import qs.services -import qs.config import QtQuick +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root @@ -15,8 +15,8 @@ StyledRect { property alias text: label.text property bool checked property bool toggle - property real horizontalPadding: Appearance.padding.normal - property real verticalPadding: Appearance.padding.smaller + property real horizontalPadding: Tokens.padding.normal + property real verticalPadding: Tokens.padding.smaller property alias font: label.font property int type: TextButton.Filled @@ -47,7 +47,7 @@ StyledRect { onCheckedChanged: internalChecked = checked - radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour implicitWidth: label.implicitWidth + horizontalPadding * 2 @@ -57,8 +57,7 @@ StyledRect { id: stateLayer color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - - function onClicked(): void { + onClicked: { if (root.toggle) root.internalChecked = !root.internalChecked; root.clicked(); diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml index 98c7564f2..232426c17 100644 --- a/components/controls/ToggleButton.qml +++ b/components/controls/ToggleButton.qml @@ -1,11 +1,11 @@ -import ".." +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls -import qs.components.effects import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root @@ -14,50 +14,45 @@ StyledRect { property string icon property string label property string accent: "Secondary" - property real iconSize: Appearance.font.size.large - property real horizontalPadding: Appearance.padding.large - property real verticalPadding: Appearance.padding.normal + property real iconSize: Tokens.font.size.large + property real horizontalPadding: Tokens.padding.large + property real verticalPadding: Tokens.padding.normal property string tooltip: "" - property bool hovered: false + signal clicked - Component.onCompleted: { - hovered = toggleStateLayer.containsMouse; - } + Component.onCompleted: hovered = toggleStateLayer.containsMouse + + Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.normal * 2 : toggled ? Tokens.padding.small * 2 : 0) + implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 + implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 + radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.small : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) + color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] Connections { - target: toggleStateLayer function onContainsMouseChanged() { const newHovered = toggleStateLayer.containsMouse; - if (hovered !== newHovered) { - hovered = newHovered; + if (root.hovered !== newHovered) { + root.hovered = newHovered; } } - } - - Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) - implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 - implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 - radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) - color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] + target: toggleStateLayer + } StateLayer { id: toggleStateLayer color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] - - function onClicked(): void { - root.clicked(); - } + onClicked: root.clicked() } RowLayout { id: toggleBtnInner anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { id: toggleBtnIcon @@ -74,6 +69,7 @@ StyledRect { } Loader { + asynchronous: true active: !!root.label visible: active @@ -86,21 +82,21 @@ StyledRect { Behavior on radius { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } // Tooltip - positioned absolutely, doesn't affect layout Loader { id: tooltipLoader + + asynchronous: true active: root.tooltip !== "" z: 10000 width: 0 diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml index 269d3d6a5..beb6a15cb 100644 --- a/components/controls/ToggleRow.qml +++ b/components/controls/ToggleRow.qml @@ -1,9 +1,8 @@ -import qs.components -import qs.components.controls -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls RowLayout { id: root @@ -13,7 +12,7 @@ RowLayout { property alias toggle: toggle Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml index b129a37b9..6c62f43d5 100644 --- a/components/controls/Tooltip.qml +++ b/components/controls/Tooltip.qml @@ -1,10 +1,9 @@ -import ".." -import qs.components.effects -import qs.services -import qs.config import QtQuick import QtQuick.Controls -import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.effects +import qs.services Popup { id: root @@ -24,55 +23,6 @@ Popup { onTriggered: root.tooltipVisible = false } - // Popup properties - doesn't affect layout - parent: { - let p = target; - // Walk up to find the root Item (usually has anchors.fill: parent) - while (p && p.parent) { - const parentItem = p.parent; - // Check if this looks like a root pane Item - if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { - return parentItem; - } - p = parentItem; - } - // Fallback - return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; - } - - visible: tooltipVisible - modal: false - closePolicy: Popup.NoAutoClose - padding: 0 - margins: 0 - background: Item {} - - // Update position when target moves or tooltip becomes visible - onTooltipVisibleChanged: { - if (tooltipVisible) { - Qt.callLater(updatePosition); - } - } - Connections { - target: root.target - function onXChanged() { - if (root.tooltipVisible) - root.updatePosition(); - } - function onYChanged() { - if (root.tooltipVisible) - root.updatePosition(); - } - function onWidthChanged() { - if (root.tooltipVisible) - root.updatePosition(); - } - function onHeightChanged() { - if (root.tooltipVisible) - root.updatePosition(); - } - } - function updatePosition() { if (!target || !parent) return; @@ -94,10 +44,10 @@ Popup { let newX = targetCenterX - tooltipWidth / 2; // Position tooltip above target - let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; + let newY = targetPos.y - tooltipHeight - Tokens.spacing.small; // Keep within bounds - const padding = Appearance.padding.normal; + const padding = Tokens.padding.normal; if (newX < padding) { newX = padding; } else if (newX + tooltipWidth > (parent.width - padding)) { @@ -110,13 +60,47 @@ Popup { }); } + // Popup properties - doesn't affect layout + parent: { + let p = target; + // Walk up to find the root Item (usually has anchors.fill: parent) + while (p && p.parent) { + const parentItem = p.parent; + // Check if this looks like a root pane Item + if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { + return parentItem; + } + p = parentItem; + } + // Fallback + return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; + } + + visible: tooltipVisible + modal: false + closePolicy: Popup.NoAutoClose + padding: 0 + margins: 0 + background: Item {} + + // Update position when target moves or tooltip becomes visible + onTooltipVisibleChanged: { + if (tooltipVisible) { + Qt.callLater(updatePosition); + } + } + Component.onCompleted: { + if (tooltipVisible) { + updatePosition(); + } + } + enter: Transition { Anim { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } @@ -125,37 +109,18 @@ Popup { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial - } - } - - // Monitor hover state - Connections { - target: root.target - function onHoveredChanged() { - if (target.hovered) { - showTimer.start(); - if (timeout > 0) { - hideTimer.stop(); - hideTimer.start(); - } - } else { - showTimer.stop(); - hideTimer.stop(); - tooltipVisible = false; - } + type: Anim.FastSpatial } } contentItem: StyledRect { id: tooltipRect - implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: tooltipText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: tooltipText.implicitHeight + Tokens.padding.smaller * 2 color: Colours.palette.m3surfaceContainerHighest - radius: Appearance.rounding.small + radius: Tokens.rounding.small antialiasing: true // Add elevation for depth @@ -173,13 +138,47 @@ Popup { text: root.text color: Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } - Component.onCompleted: { - if (tooltipVisible) { - updatePosition(); + Connections { + function onXChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onYChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onWidthChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + function onHeightChanged() { + if (root.tooltipVisible) + root.updatePosition(); + } + + target: root.target + } + + // Monitor hover state + Connections { + function onHoveredChanged() { + if (target.hovered) { + showTimer.start(); + if (timeout > 0) { + hideTimer.stop(); + hideTimer.start(); + } + } else { + showTimer.stop(); + hideTimer.stop(); + tooltipVisible = false; + } } + + target: root.target } } diff --git a/components/effects/ColouredIcon.qml b/components/effects/ColouredIcon.qml index 5ef4d4cc0..570246bf0 100644 --- a/components/effects/ColouredIcon.qml +++ b/components/effects/ColouredIcon.qml @@ -1,8 +1,8 @@ pragma ComponentBehavior: Bound -import Caelestia -import Quickshell.Widgets import QtQuick +import Quickshell.Widgets +import Caelestia IconImage { id: root diff --git a/components/effects/Colouriser.qml b/components/effects/Colouriser.qml index 2948155d6..b8abebb14 100644 --- a/components/effects/Colouriser.qml +++ b/components/effects/Colouriser.qml @@ -1,6 +1,6 @@ -import ".." import QtQuick import QtQuick.Effects +import qs.components MultiEffect { property color sourceColor: "black" diff --git a/components/effects/Elevation.qml b/components/effects/Elevation.qml index fb29f16e8..9e0962dad 100644 --- a/components/effects/Elevation.qml +++ b/components/effects/Elevation.qml @@ -1,7 +1,7 @@ -import ".." -import qs.services import QtQuick import QtQuick.Effects +import qs.components +import qs.services RectangularShadow { property int level diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml index d4a751f84..1b502e299 100644 --- a/components/effects/InnerBorder.qml +++ b/components/effects/InnerBorder.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Effects +import Caelestia.Config +import qs.components +import qs.services StyledRect { property alias innerRadius: maskInner.radius @@ -37,8 +37,8 @@ StyledRect { id: maskInner anchors.fill: parent - anchors.margins: Appearance.padding.normal - radius: Appearance.rounding.small + anchors.margins: Tokens.padding.normal + radius: Tokens.rounding.normal } } } diff --git a/components/effects/OpacityMask.qml b/components/effects/OpacityMask.qml index 22e424960..8e625034e 100644 --- a/components/effects/OpacityMask.qml +++ b/components/effects/OpacityMask.qml @@ -1,9 +1,9 @@ -import Quickshell import QtQuick +import Quickshell ShaderEffect { required property Item source required property Item maskSource - fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) + fragmentShader: Quickshell.shellPath("assets/shaders/opacitymask.frag.qsb") } diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml index bb87133c7..83cb4a922 100644 --- a/components/filedialog/CurrentItem.qml +++ b/components/filedialog/CurrentItem.qml @@ -1,16 +1,16 @@ -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Shapes +import Caelestia.Config +import qs.components +import qs.services Item { id: root required property var currentItem - implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin - implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 + implicitWidth: content.implicitWidth + Tokens.padding.larger + content.anchors.rightMargin + implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.normal + content.anchors.bottomMargin : 0 Shape { preferredRendererType: Shape.CurveRenderer @@ -18,7 +18,7 @@ Item { ShapePath { id: path - readonly property real rounding: Appearance.rounding.small + readonly property real rounding: Tokens.rounding.small readonly property bool flatten: root.implicitHeight < rounding * 2 readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding @@ -76,16 +76,16 @@ Item { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small - anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small + anchors.rightMargin: Tokens.padding.larger - Tokens.padding.small + anchors.bottomMargin: Tokens.padding.normal - Tokens.padding.small Connections { - target: root - function onCurrentItemChanged(): void { if (root.currentItem) content.text = qsTr(`"%1" selected`).arg(root.currentItem.modelData.name); } + + target: root } } } diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml index bde9ac277..b46b4f721 100644 --- a/components/filedialog/DialogButtons.qml +++ b/components/filedialog/DialogButtons.qml @@ -1,7 +1,7 @@ -import ".." -import qs.services -import qs.config import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root @@ -9,7 +9,7 @@ StyledRect { required property var dialog required property FolderContents folder - implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer @@ -17,9 +17,9 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Filter:") @@ -28,14 +28,14 @@ StyledRect { StyledRect { Layout.fillWidth: true Layout.fillHeight: true - Layout.rightMargin: Appearance.spacing.normal + Layout.rightMargin: Tokens.spacing.normal color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.small + radius: Tokens.rounding.small StyledText { anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` } @@ -43,24 +43,21 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 StateLayer { disabled: !root.dialog.selectionValid - - function onClicked(): void { - root.dialog.accepted(root.folder.currentItem.modelData.path); - } + onClicked: root.dialog.accepted(root.folder.currentItem.modelData.path) } StyledText { id: selectText anchors.centerIn: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal text: qsTr("Select") color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -69,13 +66,13 @@ StyledRect { StyledRect { color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { root.dialog.rejected(); } } @@ -84,7 +81,7 @@ StyledRect { id: cancelText anchors.centerIn: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal text: qsTr("Cancel") } diff --git a/components/filedialog/FileDialog.qml b/components/filedialog/FileDialog.qml index f3187a55b..90dd2bc20 100644 --- a/components/filedialog/FileDialog.qml +++ b/components/filedialog/FileDialog.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import qs.components +import qs.services LazyLoader { id: loader diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml index e16c7a15c..d8869b83f 100644 --- a/components/filedialog/FolderContents.qml +++ b/components/filedialog/FolderContents.qml @@ -1,22 +1,23 @@ pragma ComponentBehavior: Bound -import ".." -import "../controls" -import "../images" -import qs.services -import qs.config -import qs.utils -import Caelestia.Models -import Quickshell import QtQuick -import QtQuick.Layouts import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import Caelestia.Models +import qs.components +import qs.components.controls +import qs.components.filedialog +import qs.components.images +import qs.services +import qs.utils Item { id: root required property var dialog - property alias currentItem: view.currentItem + readonly property FileEntry currentItem: view.currentItem as FileEntry StyledRect { anchors.fill: parent @@ -41,12 +42,13 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: Appearance.padding.small - radius: Appearance.rounding.small + anchors.margins: Tokens.padding.small + radius: Tokens.rounding.small } } Loader { + asynchronous: true anchors.centerIn: parent opacity: view.count === 0 ? 1 : 0 @@ -57,14 +59,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 font.weight: 500 } StyledText { text: qsTr("This folder is empty") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } } @@ -78,10 +80,10 @@ Item { id: view anchors.fill: parent - anchors.margins: Appearance.padding.small + Appearance.padding.normal + anchors.margins: Tokens.padding.small + Tokens.padding.normal - cellWidth: Sizes.itemWidth + Appearance.spacing.small - cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 + cellWidth: Sizes.itemWidth + Tokens.spacing.small + cellHeight: Sizes.itemWidth + Tokens.spacing.small * 2 + Tokens.padding.normal * 2 + 1 clip: true focus: true @@ -90,11 +92,11 @@ Item { Keys.onReturnPressed: { if (root.dialog.selectionValid) - root.dialog.accepted(currentItem.modelData.path); + root.dialog.accepted((currentItem as FileEntry).modelData.path); } Keys.onEnterPressed: { if (root.dialog.selectionValid) - root.dialog.accepted(currentItem.modelData.path); + root.dialog.accepted((currentItem as FileEntry).modelData.path); } StyledScrollBar.vertical: StyledScrollBar { @@ -104,92 +106,21 @@ Item { model: FileSystemModel { path: { if (root.dialog.cwd[0] === "Home") - return `${Paths.home}/${root.dialog.cwd.slice(1).join("/")}`; + return Paths.home + `/${root.dialog.cwd.slice(1).join("/")}`; else return root.dialog.cwd.join("/"); } onPathChanged: view.currentIndex = -1 } - delegate: StyledRect { - id: item - - required property int index - required property FileSystemEntry modelData - - readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 - - implicitWidth: Sizes.itemWidth - implicitHeight: nonAnimHeight - - radius: Appearance.rounding.normal - color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) - z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 - clip: true - - StateLayer { - onDoubleClicked: { - if (item.modelData.isDir) - root.dialog.cwd.push(item.modelData.name); - else if (root.dialog.selectionValid) - root.dialog.accepted(item.modelData.path); - } - - function onClicked(): void { - view.currentIndex = item.index; - } - } - - CachingIconImage { - id: icon - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: Appearance.padding.normal - - implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 - - Component.onCompleted: { - const file = item.modelData; - if (file.isImage) - source = Qt.resolvedUrl(file.path); - else if (!file.isDir) - source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); - else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) - source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); - else - source = Quickshell.iconPath("inode-directory"); - } - } - - StyledText { - id: name - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small - anchors.margins: Appearance.padding.normal - - horizontalAlignment: Text.AlignHCenter - elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight - wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap - - Component.onCompleted: text = item.modelData.name - } - - Behavior on implicitHeight { - Anim {} - } - } + delegate: FileEntry {} add: Transition { Anim { properties: "opacity,scale" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -208,12 +139,11 @@ Item { Anim { properties: "opacity,scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } @@ -221,8 +151,77 @@ Item { CurrentItem { anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small currentItem: view.currentItem } + + component FileEntry: StyledRect { + id: item + + required property int index + required property FileSystemEntry modelData + + readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.normal * 2 + + implicitWidth: Sizes.itemWidth + implicitHeight: nonAnimHeight + + radius: Tokens.rounding.normal + color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) + z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 + clip: true + + StateLayer { + onClicked: view.currentIndex = item.index + onDoubleClicked: { + if (item.modelData.isDir) + root.dialog.cwd.push(item.modelData.name); + else if (root.dialog.selectionValid) + root.dialog.accepted(item.modelData.path); + } + } + + CachingIconImage { + id: icon + + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: Tokens.padding.normal + + implicitSize: Sizes.itemWidth - Tokens.padding.normal * 2 + + Component.onCompleted: { + const file = item.modelData; + if (file.isImage) + source = Qt.resolvedUrl(file.path); + else if (!file.isDir) + source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); + else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) + source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); + else + source = Quickshell.iconPath("inode-directory"); + } + } + + StyledText { + id: name + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: icon.bottom + anchors.topMargin: Tokens.spacing.small + anchors.margins: Tokens.padding.normal + + horizontalAlignment: Text.AlignHCenter + elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight + wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap + + Component.onCompleted: text = item.modelData.name + } + + Behavior on implicitHeight { + Anim {} + } + } } diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml index c9a3feb53..c53d8f765 100644 --- a/components/filedialog/HeaderBar.qml +++ b/components/filedialog/HeaderBar.qml @@ -1,18 +1,18 @@ pragma ComponentBehavior: Bound -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root required property var dialog - implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: inner.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer @@ -20,20 +20,17 @@ StyledRect { id: inner anchors.fill: parent - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small Item { implicitWidth: implicitHeight - implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 StateLayer { - radius: Appearance.rounding.small + radius: Tokens.rounding.small disabled: root.dialog.cwd.length === 1 - - function onClicked(): void { - root.dialog.cwd.pop(); - } + onClicked: root.dialog.cwd.pop() } MaterialIcon { @@ -49,7 +46,7 @@ StyledRect { StyledRect { Layout.fillWidth: true - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainerHigh implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 @@ -58,10 +55,10 @@ StyledRect { id: pathComponents anchors.fill: parent - anchors.margins: Appearance.padding.small / 2 + anchors.margins: Tokens.padding.small / 2 anchors.leftMargin: 0 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: root.dialog.cwd @@ -75,7 +72,8 @@ StyledRect { spacing: 0 Loader { - Layout.rightMargin: Appearance.spacing.small + asynchronous: true + Layout.rightMargin: Tokens.spacing.small active: folder.index > 0 sourceComponent: StyledText { text: "/" @@ -85,27 +83,30 @@ StyledRect { } Item { - implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 + implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.small : 0) + folderName.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: folderName.implicitHeight + Tokens.padding.small * 2 Loader { + asynchronous: true anchors.fill: parent active: folder.index < root.dialog.cwd.length - 1 sourceComponent: StateLayer { - radius: Appearance.rounding.small - - function onClicked(): void { + onClicked: { root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); } + + radius: Tokens.rounding.small } } Loader { id: homeIcon + asynchronous: true + anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Tokens.padding.normal active: folder.index === 0 && folder.modelData === "Home" sourceComponent: MaterialIcon { @@ -120,7 +121,7 @@ StyledRect { anchors.left: homeIcon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 + anchors.leftMargin: homeIcon.active ? Tokens.padding.small : 0 text: folder.modelData color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml index b55d7b379..e9c0918bf 100644 --- a/components/filedialog/Sidebar.qml +++ b/components/filedialog/Sidebar.qml @@ -1,10 +1,11 @@ pragma ComponentBehavior: Bound -import ".." -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.filedialog +import qs.services StyledRect { id: root @@ -12,7 +13,7 @@ StyledRect { required property var dialog implicitWidth: Sizes.sidebarWidth - implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 color: Colours.tPalette.m3surfaceContainer @@ -22,16 +23,16 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.small / 2 + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small / 2 StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.padding.small / 2 - Layout.bottomMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.padding.small / 2 + Layout.bottomMargin: Tokens.spacing.normal text: qsTr("Files") color: Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.bold: true } @@ -45,15 +46,14 @@ StyledRect { readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] Layout.fillWidth: true - implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: placeInner.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) StateLayer { color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - - function onClicked(): void { + onClicked: { if (place.modelData === "Home") root.dialog.cwd = ["Home"]; else @@ -65,11 +65,11 @@ StyledRect { id: placeInner anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + anchors.margins: Tokens.padding.normal + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: { @@ -91,7 +91,7 @@ StyledRect { return "folder"; } color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: place.selected ? 1 : 0 Behavior on fill { @@ -103,7 +103,7 @@ StyledRect { Layout.fillWidth: true text: place.modelData color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } } diff --git a/components/images/CachingIconImage.qml b/components/images/CachingIconImage.qml index 1acc6a181..001e95de4 100644 --- a/components/images/CachingIconImage.qml +++ b/components/images/CachingIconImage.qml @@ -1,13 +1,14 @@ pragma ComponentBehavior: Bound -import qs.utils -import Quickshell.Widgets import QtQuick +import Quickshell.Widgets +import qs.utils Item { id: root - readonly property int status: loader.item?.status ?? Image.Null + // Easier (and more efficient) to ignore it than to check type and cast + readonly property int status: loader.item?.status ?? Image.Null // qmllint disable missing-property readonly property real actualSize: Math.min(width, height) property real implicitSize property url source @@ -18,6 +19,7 @@ Item { Loader { id: loader + asynchronous: true anchors.fill: parent sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null } diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml index e8f957a7d..1e932dbad 100644 --- a/components/images/CachingImage.qml +++ b/components/images/CachingImage.qml @@ -1,28 +1,17 @@ -import qs.utils -import Caelestia.Internal -import Quickshell import QtQuick +import Quickshell +import Caelestia.Images Image { id: root - property alias path: manager.path + property string path asynchronous: true fillMode: Image.PreserveAspectCrop - - Connections { - target: QsWindow.window - - function onDevicePixelRatioChanged(): void { - manager.updateSource(); - } - } - - CachingImageManager { - id: manager - - item: root - cacheDir: Qt.resolvedUrl(Paths.imagecache) + source: IUtils.urlForPath(path, fillMode) + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); } } diff --git a/components/misc/CustomShortcut.qml b/components/misc/CustomShortcut.qml index aa35ed8f5..9bbcf1f76 100644 --- a/components/misc/CustomShortcut.qml +++ b/components/misc/CustomShortcut.qml @@ -1,5 +1,7 @@ import Quickshell.Hyprland +// qmllint disable unresolved-type GlobalShortcut { + // qmllint enable unresolved-type appid: "caelestia" } diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml index db73ea08f..d426eed88 100644 --- a/components/widgets/ExtraIndicator.qml +++ b/components/widgets/ExtraIndicator.qml @@ -1,20 +1,20 @@ -import ".." import "../effects" -import qs.services -import qs.config import QtQuick +import Caelestia.Config +import qs.components +import qs.services StyledRect { required property int extra anchors.right: parent.right - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal color: Colours.palette.m3tertiary - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: count.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: count.implicitHeight + Appearance.padding.small * 2 + implicitWidth: count.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: count.implicitHeight + Tokens.padding.small * 2 opacity: extra > 0 ? 1 : 0 scale: extra > 0 ? 1 : 0.5 @@ -38,14 +38,13 @@ StyledRect { Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/extras/version.cpp b/extras/version.cpp index e1a0cf302..d63434170 100644 --- a/extras/version.cpp +++ b/extras/version.cpp @@ -10,7 +10,8 @@ int main(int argc, char* argv[]) { std::cout << GIT_REVISION << std::endl; std::cout << DISTRIBUTOR << std::endl; } else if (arg == "-s" || arg == "--short") { - std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION << ", distrubuted by: " << DISTRIBUTOR << std::endl; + std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR << std::endl; } else { std::cout << "Usage: " << argv[0] << " [-t | --terse] [-s | --short]" << std::endl; return arg != "-h" && arg != "--help"; diff --git a/flake.lock b/flake.lock index 5b7fbd218..0721599d2 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ ] }, "locked": { - "lastModified": 1769226332, - "narHash": "sha256-JKD9M2+/J4e6nRtcY2XRfpLlOHaGXT4aUHyIG/20qlw=", + "lastModified": 1778644647, + "narHash": "sha256-iQIu4b5by8B3qKeigungSIgRoJg9HS1NiIZwqIbCGYk=", "owner": "caelestia-dots", "repo": "cli", - "rev": "52a3a3c50ef55e3561057e8a74c85cf16f83039f", + "rev": "2ce6213698d1cfa15b4b067d35c3cda634f443dd", "type": "github" }, "original": { @@ -23,11 +23,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1769018530, - "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", + "lastModified": 1778869304, + "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", "owner": "nixos", "repo": "nixpkgs", - "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", + "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", "type": "github" }, "original": { @@ -44,11 +44,11 @@ ] }, "locked": { - "lastModified": 1768985439, - "narHash": "sha256-qkU4r+l+UPz4dutMMRZSin64HuVZkEv9iFpu9yMWVY0=", + "lastModified": 1778488696, + "narHash": "sha256-QSWgYuZUCNUJ/cxmaq83WkcT7lHQDDfsPVgH+96kIl0=", "ref": "refs/heads/master", - "rev": "191085a8821b35680bba16ce5411fc9dbe912237", - "revCount": 731, + "rev": "7d1c9a9c6721606b129829134d6f614f015621e2", + "revCount": 818, "type": "git", "url": "https://git.outfoxxed.me/outfoxxed/quickshell" }, diff --git a/flake.nix b/flake.nix index 5c884115d..0d7ebb8c2 100644 --- a/flake.nix +++ b/flake.nix @@ -36,7 +36,6 @@ withX11 = false; withI3 = false; }; - app2unit = pkgs.callPackage ./nix/app2unit.nix {inherit pkgs;}; caelestia-cli = inputs.caelestia-cli.packages.${pkgs.stdenv.hostPlatform.system}.default; }; with-cli = caelestia-shell.override {withCli = true;}; diff --git a/last-50-commits.txt b/last-50-commits.txt new file mode 100644 index 000000000..f19ac693c --- /dev/null +++ b/last-50-commits.txt @@ -0,0 +1,50 @@ +8686ac04 fix: update imports and improve layout properties in MonitorIdentifier and Picker components +bf2d52d6 Improve shell panels, monitors, and recording +b3cebc2f [CI] chore: update flake +f1c69bd1 [CI] chore: update flake +7c981749 fix: lock screencopy not working +1b8df3da chore: set crash report url to ours +5c6db1a2 chore: add crash issue template +d9957810 fix: add Global to app properties (#1447) +8934df23 [CI] chore: update flake +8f8c8aaa ci: update action versions +c1d0fd97 [CI] chore: update flake +dc5fc0ca ci: fix update flake workflow +b19fc142 fix: allow non-ascii chars in passwords +d8488a1b feat: add showOverFullscreen option + disable by default +1e0138af fix: close detached popout on fs +cd386820 fix: allow interacting with notifs and osd in fs +d4b5814a feat: display on overlay layer only when fs +6d2598bc fix: slight border showing when fullscreen +5ff6ac61 feat: calculate size from source when single dim requested +178df6f0 fix: handle hidpi for rest of sourceSize uses +7a46c13e fix: handle hidpi properly +ec5c3d57 chore: remove old CachingImageManager +1ab02bcb fix: initial 0 size on launcher wallpaper item +6dfe017c feat: split caching into separate service +4a9ceb48 fix: use original image if requested size is invalid +ebaa9c70 fix: specify sourceSize as a single prop +84a90271 fix: use QSaveFile for atomic writes +66befc1a feat: use image provider for caching +d91a5164 fix: blob exclusion being ignored at corner blends +4bd8dce9 feat: always expire notifs if fullscreen +9e267e17 fix: interaction blocking at edges when fullscreen +0e005f78 feat: use default env + drop expensive fonts +c7847f12 fix: unstretch kuru +84b56dce fix: control center outer rounding +88b8b7e4 chore: add deformScale to example config +d66f633f fix: clamp blob radii at half min dimension +f4067bcb feat: add deform scale config option +886b5ed7 fix: don't cache icons + fix bg colour when transparent image +601b0177 feat: improve sidebar notif expand anim +aedb8551 fix: sidebar notif group height anim +0d04de66 ci: update stable branch on release (#1414) +4bbbf7f7 fix: wrap media position by length (#1410) +7dc4cdbe fix: state layer ripple +0b6ccda8 chore: format +0029add4 feat: m3 expressive menus +a10c89e5 fix: don't use easingCurve type +397b6a9b fix: dash tab hover +3128159b feat: use pch to speed up builds +d177551b feat: forward declare where possible +9c5e424d chore: .. imports -> qs.components diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml index d24cff274..2d13b5a32 100644 --- a/modules/BatteryMonitor.qml +++ b/modules/BatteryMonitor.qml @@ -1,33 +1,31 @@ -import qs.config -import Caelestia +import QtQuick import Quickshell import Quickshell.Services.UPower -import QtQuick +import Caelestia +import Caelestia.Config Scope { id: root - readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) + readonly property list warnLevels: [...GlobalConfig.general.battery.warnLevels].sort((a, b) => b.level - a.level) Connections { - target: UPower - function onOnBatteryChanged(): void { if (UPower.onBattery) { - if (Config.utilities.toasts.chargingChanged) + if (GlobalConfig.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); } else { - if (Config.utilities.toasts.chargingChanged) + if (GlobalConfig.utilities.toasts.chargingChanged) Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); for (const level of root.warnLevels) level.warned = false; } } + + target: UPower } Connections { - target: UPower.displayDevice - function onPercentageChanged(): void { if (!UPower.onBattery) return; @@ -40,11 +38,13 @@ Scope { } } - if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) { + if (!hibernateTimer.running && p <= GlobalConfig.general.battery.criticalLevel) { Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error); hibernateTimer.start(); } } + + target: UPower.displayDevice } Timer { diff --git a/modules/ConfigToasts.qml b/modules/ConfigToasts.qml new file mode 100644 index 000000000..81463b8d8 --- /dev/null +++ b/modules/ConfigToasts.qml @@ -0,0 +1,39 @@ +import QtQuick +import Quickshell +import Caelestia +import Caelestia.Config + +Scope { + Connections { + function onLoaded(): void { + if (GlobalConfig.utilities.toasts.configLoaded) + Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded successfully!"), "rule_settings"); + } + + function onLoadFailed(error: string, screen: string): void { + Toaster.toast(qsTr("Failed to parse config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Warning); + } + + function onSaveFailed(error: string, screen: string): void { + Toaster.toast(qsTr("Failed to save config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Error); + } + + function onUnknownOption(key: string, screen: string): void { + Toaster.toast(qsTr("Unknown option in%1 config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); + } + + target: GlobalConfig + } + + Connections { + function onLoadFailed(error: string, screen: string): void { + Toaster.toast(qsTr("Failed to parse token config%1").arg(screen ? "for " + screen : ""), error, "settings_alert", Toast.Warning); + } + + function onUnknownOption(key: string, screen: string): void { + Toaster.toast(qsTr("Unknown option in%1 token config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); + } + + target: TokenConfig + } +} diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml index b7ce05843..417b0e8ae 100644 --- a/modules/IdleMonitors.qml +++ b/modules/IdleMonitors.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound import "lock" -import qs.config -import qs.services -import Caelestia.Internal import Quickshell import Quickshell.Wayland +import Caelestia.Config +import Caelestia.Internal +import qs.services Scope { id: root required property Lock lock - readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) + readonly property bool enabled: !GlobalConfig.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) function handleIdleAction(action: var): void { if (!action) @@ -29,7 +29,7 @@ Scope { LogindManager { onAboutToSleep: { - if (Config.general.idle.lockBeforeSleep) + if (GlobalConfig.general.idle.lockBeforeSleep) root.lock.lock.locked = true; } onLockRequested: root.lock.lock.locked = true @@ -37,7 +37,7 @@ Scope { } Variants { - model: Config.general.idle.timeouts + model: GlobalConfig.general.idle.timeouts IdleMonitor { required property var modelData diff --git a/modules/MonitorIdentifier.qml b/modules/MonitorIdentifier.qml index 096660a42..a618c6b52 100644 --- a/modules/MonitorIdentifier.qml +++ b/modules/MonitorIdentifier.qml @@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound import qs.components import qs.components.containers import qs.services -import qs.config +import Caelestia.Config import Quickshell import Quickshell.Wayland import Quickshell.Hyprland @@ -43,9 +43,9 @@ Variants { StyledRect { id: identifierRect anchors.centerIn: parent - implicitWidth: Appearance.padding.large * 14 - implicitHeight: Appearance.padding.large * 14 - radius: Appearance.rounding.large + implicitWidth: Tokens.padding.large * 14 + implicitHeight: Tokens.padding.large * 14 + radius: Tokens.rounding.large color: Colours.tPalette.m3surfaceContainer opacity: root.active ? 0.92 : 0 @@ -57,7 +57,7 @@ Variants { ColumnLayout { anchors.centerIn: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.alignment: Qt.AlignHCenter @@ -70,7 +70,7 @@ Variants { StyledText { Layout.alignment: Qt.AlignHCenter text: win.monitor?.name ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurfaceVariant } diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index a62b827eb..9ee796769 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -1,23 +1,28 @@ -import qs.components.misc -import qs.modules.controlcenter -import qs.services -import Caelestia +import QtQuick import Quickshell import Quickshell.Io +import Caelestia +import qs.components.misc +import qs.services +import qs.modules.controlcenter Scope { id: root property bool launcherInterrupted - readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false + readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "controlCenter" description: "Open control center" onPressed: WindowFactory.create() } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "showall" description: "Toggle launcher, dashboard and osd" onPressed: { @@ -28,7 +33,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "dashboard" description: "Toggle dashboard" onPressed: { @@ -39,7 +46,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "session" description: "Toggle session menu" onPressed: { @@ -50,7 +59,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "launcher" description: "Toggle launcher" onPressed: root.launcherInterrupted = false @@ -63,15 +74,41 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "launcherInterrupt" description: "Interrupt launcher keybind" onPressed: root.launcherInterrupted = true } - IpcHandler { - target: "drawers" + // qmllint disable unresolved-type + CustomShortcut { + // qmllint enable unresolved-type + name: "sidebar" + description: "Toggle sidebar" + onPressed: { + if (root.hasFullscreen) + return; + const visibilities = Visibilities.getForActive(); + visibilities.sidebar = !visibilities.sidebar; + } + } + // qmllint disable unresolved-type + CustomShortcut { + // qmllint enable unresolved-type + name: "utilities" + description: "Toggle utilities" + onPressed: { + if (root.hasFullscreen) + return; + const visibilities = Visibilities.getForActive(); + visibilities.utilities = !visibilities.utilities; + } + } + + IpcHandler { function toggle(drawer: string): void { if (list().split("\n").includes(drawer)) { if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer)) @@ -79,7 +116,7 @@ Scope { const visibilities = Visibilities.getForActive(); visibilities[drawer] = !visibilities[drawer]; } else { - console.warn(`[IPC] Drawer "${drawer}" does not exist`); + console.warn(lc, `Drawer "${drawer}" does not exist`); } } @@ -87,19 +124,19 @@ Scope { const visibilities = Visibilities.getForActive(); return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n"); } + + target: "drawers" } IpcHandler { - target: "controlCenter" - function open(): void { WindowFactory.create(); } + + target: "controlCenter" } IpcHandler { - target: "toaster" - function info(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Info); } @@ -115,5 +152,14 @@ Scope { function error(title: string, message: string, icon: string): void { Toaster.toast(title, message, icon, Toast.Error); } + + target: "toaster" + } + + LoggingCategory { + id: lc + + name: "caelestia.qml.shortcuts" + defaultLogLevel: LoggingCategory.Info } } diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml index 0d8b2fe13..76cc10399 100644 --- a/modules/areapicker/AreaPicker.qml +++ b/modules/areapicker/AreaPicker.qml @@ -1,10 +1,11 @@ pragma ComponentBehavior: Bound -import qs.components.containers -import qs.components.misc import Quickshell -import Quickshell.Wayland import Quickshell.Io +import Quickshell.Wayland +import qs.components.containers +import qs.components.misc +import qs.services Scope { LazyLoader { @@ -15,7 +16,7 @@ Scope { property bool clipboardOnly Variants { - model: Quickshell.screens + model: Screens.screens StyledWindow { id: win @@ -47,8 +48,6 @@ Scope { } IpcHandler { - target: "picker" - function open(): void { root.freeze = false; root.closing = false; @@ -76,9 +75,13 @@ Scope { root.clipboardOnly = true; root.activeAsync = true; } + + target: "picker" } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshot" description: "Open screenshot tool" onPressed: { @@ -89,7 +92,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshotFreeze" description: "Open screenshot tool (freeze mode)" onPressed: { @@ -100,7 +105,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshotClip" description: "Open screenshot tool (clipboard)" onPressed: { @@ -111,7 +118,9 @@ Scope { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "screenshotFreezeClip" description: "Open screenshot tool (freeze mode, clipboard)" onPressed: { diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml index 3be2bf308..75c45ab93 100644 --- a/modules/areapicker/Picker.qml +++ b/modules/areapicker/Picker.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Caelestia -import Quickshell -import Quickshell.Wayland import QtQuick import QtQuick.Effects +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Caelestia +import qs.components +import qs.services MouseArea { id: root @@ -80,9 +80,12 @@ MouseArea { } else { Quickshell.execDetached(["swappy", "-f", path]); } - closeAnim.start(); + closeAnim.start(); + }, () => { + console.error("Failed to save screenshot"); + closeAnim.start(); }) -} +} onClientsChanged: checkClientRects(mouseX, mouseY) @@ -166,7 +169,7 @@ onClientsChanged: checkClientRects(mouseX, mouseY) target: root property: "opacity" to: 0 - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } ExAnim { target: root @@ -191,9 +194,21 @@ onClientsChanged: checkClientRects(mouseX, mouseY) } } + Process { + running: true + command: ["hyprctl", "cursorpos", "-j"] + stdout: StdioCollector { + onStreamFinished: { + const pos = JSON.parse(text); + root.checkClientRects(pos.x - root.screen.x, pos.y - root.screen.y); + } + } + } + Loader { id: screencopy + asynchronous: true anchors.fill: parent active: root.loader.freeze @@ -207,6 +222,13 @@ onClientsChanged: checkClientRects(mouseX, mouseY) root.save(); } } + + Component.onCompleted: { + if (hasContent && !root.loader.freeze) { + overlay.visible = border.visible = true; + root.save(); + } + } } } @@ -265,7 +287,7 @@ onClientsChanged: checkClientRects(mouseX, mouseY) Behavior on opacity { Anim { - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } } @@ -294,7 +316,6 @@ onClientsChanged: checkClientRects(mouseX, mouseY) } component ExAnim: Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } diff --git a/modules/background/Background.qml b/modules/background/Background.qml index f8484e167..70f6914dd 100644 --- a/modules/background/Background.qml +++ b/modules/background/Background.qml @@ -1,146 +1,158 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Wayland +import Caelestia.Config import qs.components import qs.components.containers import qs.services -import qs.config -import Quickshell -import Quickshell.Wayland -import QtQuick -Loader { - active: Config.background.enabled +Variants { + model: Screens.screens.filter(s => GlobalConfig.forScreen(s.name).background.enabled) - sourceComponent: Variants { - model: Quickshell.screens + StyledWindow { + id: win - StyledWindow { - id: win + required property ShellScreen modelData - required property ShellScreen modelData + screen: modelData + name: "background" + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: contentItem.Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom + color: contentItem.Config.background.wallpaperEnabled ? "black" : "transparent" + surfaceFormat.opaque: false - screen: modelData - name: "background" - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.layer: WlrLayer.Background - color: "black" + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true - anchors.top: true - anchors.bottom: true - anchors.left: true - anchors.right: true + Item { + id: behindClock - Item { - id: behindClock + anchors.fill: parent + + Loader { + id: wallpaper + + asynchronous: true anchors.fill: parent + active: Config.background.wallpaperEnabled - Wallpaper { - id: wallpaper - } + sourceComponent: Wallpaper {} + } - Visualiser { - anchors.fill: parent - screen: win.modelData - wallpaper: wallpaper - } + Visualiser { + anchors.fill: parent + screen: win.modelData + wallpaper: wallpaper } + } - Loader { - id: clockLoader - active: Config.background.desktopClock.enabled - - anchors.margins: Appearance.padding.large * 2 - anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) - - state: Config.background.desktopClock.position - states: [ - State { - name: "top-left" - AnchorChanges { - target: clockLoader - anchors.top: parent.top - anchors.left: parent.left - } - }, - State { - name: "top-center" - AnchorChanges { - target: clockLoader - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - } - }, - State { - name: "top-right" - AnchorChanges { - target: clockLoader - anchors.top: parent.top - anchors.right: parent.right - } - }, - State { - name: "middle-left" - AnchorChanges { - target: clockLoader - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - } - }, - State { - name: "middle-center" - AnchorChanges { - target: clockLoader - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - } - }, - State { - name: "middle-right" - AnchorChanges { - target: clockLoader - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - } - }, - State { - name: "bottom-left" - AnchorChanges { - target: clockLoader - anchors.bottom: parent.bottom - anchors.left: parent.left - } - }, - State { - name: "bottom-center" - AnchorChanges { - target: clockLoader - anchors.bottom: parent.bottom - anchors.horizontalCenter: parent.horizontalCenter - } - }, - State { - name: "bottom-right" - AnchorChanges { - target: clockLoader - anchors.bottom: parent.bottom - anchors.right: parent.right - } - } - ] + Loader { + id: clockLoader + + asynchronous: true + active: Config.background.desktopClock.enabled + + anchors.margins: Tokens.padding.large * 2 + anchors.leftMargin: Tokens.padding.large * 2 + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) + + state: Config.background.desktopClock.position + states: [ + State { + name: "top-left" - transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + AnchorChanges { + target: clockLoader + anchors.top: parent.top + anchors.left: parent.left + } + }, + State { + name: "top-center" + + AnchorChanges { + target: clockLoader + anchors.top: parent.top + anchors.horizontalCenter: parent.horizontalCenter + } + }, + State { + name: "top-right" + + AnchorChanges { + target: clockLoader + anchors.top: parent.top + anchors.right: parent.right + } + }, + State { + name: "middle-left" + + AnchorChanges { + target: clockLoader + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + } + }, + State { + name: "middle-center" + + AnchorChanges { + target: clockLoader + anchors.verticalCenter: parent.verticalCenter + anchors.horizontalCenter: parent.horizontalCenter + } + }, + State { + name: "middle-right" + + AnchorChanges { + target: clockLoader + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } + }, + State { + name: "bottom-left" + + AnchorChanges { + target: clockLoader + anchors.bottom: parent.bottom + anchors.left: parent.left + } + }, + State { + name: "bottom-center" + + AnchorChanges { + target: clockLoader + anchors.bottom: parent.bottom + anchors.horizontalCenter: parent.horizontalCenter + } + }, + State { + name: "bottom-right" + + AnchorChanges { + target: clockLoader + anchors.bottom: parent.bottom + anchors.right: parent.right } } + ] - sourceComponent: DesktopClock { - wallpaper: behindClock - absX: clockLoader.x - absY: clockLoader.y - } + transitions: Transition { + AnchorAnim {} + } + + sourceComponent: DesktopClock { + wallpaper: behindClock + absX: clockLoader.x + absY: clockLoader.y } } } diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml index 77fe447fe..c7415be8b 100644 --- a/modules/background/DesktopClock.qml +++ b/modules/background/DesktopClock.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config import QtQuick -import QtQuick.Layouts import QtQuick.Effects +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services Item { id: root @@ -14,17 +14,17 @@ Item { required property real absX required property real absY - property real scale: Config.background.desktopClock.scale + property real clockScale: Config.background.desktopClock.scale readonly property bool bgEnabled: Config.background.desktopClock.background.enabled - readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur + readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled readonly property bool invertColors: Config.background.desktopClock.invertColors readonly property bool useLightSet: Colours.light ? !invertColors : invertColors readonly property color safePrimary: useLightSet ? Colours.palette.m3primaryContainer : Colours.palette.m3primary readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary - implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.scale) - implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.scale) + implicitWidth: layout.implicitWidth + (Tokens.padding.large * 4 * root.clockScale) + implicitHeight: layout.implicitHeight + (Tokens.padding.large * 2 * root.clockScale) Item { id: clockContainer @@ -40,6 +40,7 @@ Item { } Loader { + asynchronous: true anchors.fill: parent active: root.blurEnabled @@ -62,7 +63,7 @@ Item { visible: root.bgEnabled anchors.fill: parent - radius: Appearance.rounding.large * root.scale + radius: Tokens.rounding.large * root.clockScale opacity: Config.background.desktopClock.background.opacity color: Colours.palette.m3surface @@ -73,43 +74,44 @@ Item { id: layout anchors.centerIn: parent - spacing: Appearance.spacing.larger * root.scale + spacing: Tokens.spacing.larger * root.clockScale RowLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: Time.hourStr - font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale + font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale font.weight: Font.Bold color: root.safePrimary } StyledText { text: ":" - font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale + font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale color: root.safeTertiary opacity: 0.8 - Layout.topMargin: -Appearance.padding.large * 1.5 * root.scale + Layout.topMargin: -Tokens.padding.large * 1.5 * root.clockScale } StyledText { text: Time.minuteStr - font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale + font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale font.weight: Font.Bold color: root.safeSecondary } Loader { + asynchronous: true Layout.alignment: Qt.AlignTop - Layout.topMargin: Appearance.padding.large * 1.4 * root.scale + Layout.topMargin: Tokens.padding.large * 1.4 * root.clockScale - active: Config.services.useTwelveHourClock + active: GlobalConfig.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr - font.pointSize: Appearance.font.size.large * root.scale + font.pointSize: Tokens.font.size.large * root.clockScale color: root.safeSecondary } } @@ -117,10 +119,10 @@ Item { StyledRect { Layout.fillHeight: true - Layout.preferredWidth: 4 * root.scale - Layout.topMargin: Appearance.spacing.larger * root.scale - Layout.bottomMargin: Appearance.spacing.larger * root.scale - radius: Appearance.rounding.full + Layout.preferredWidth: 4 * root.clockScale + Layout.topMargin: Tokens.spacing.larger * root.clockScale + Layout.bottomMargin: Tokens.spacing.larger * root.clockScale + radius: Tokens.rounding.full color: root.safePrimary opacity: 0.8 } @@ -130,7 +132,7 @@ Item { StyledText { text: Time.format("MMMM").toUpperCase() - font.pointSize: Appearance.font.size.large * root.scale + font.pointSize: Tokens.font.size.large * root.clockScale font.letterSpacing: 4 font.weight: Font.Bold color: root.safeSecondary @@ -138,7 +140,7 @@ Item { StyledText { text: Time.format("dd") - font.pointSize: Appearance.font.size.extraLarge * root.scale + font.pointSize: Tokens.font.size.extraLarge * root.clockScale font.letterSpacing: 2 font.weight: Font.Medium color: root.safePrimary @@ -146,7 +148,7 @@ Item { StyledText { text: Time.format("dddd") - font.pointSize: Appearance.font.size.larger * root.scale + font.pointSize: Tokens.font.size.larger * root.clockScale font.letterSpacing: 2 color: root.safeSecondary } @@ -154,16 +156,15 @@ Item { } } - Behavior on scale { + Behavior on clockScale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on implicitWidth { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml index c9bb9efb0..450558792 100644 --- a/modules/background/Visualiser.qml +++ b/modules/background/Visualiser.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Caelestia.Services -import Quickshell -import Quickshell.Widgets import QtQuick import QtQuick.Effects +import Quickshell +import Caelestia.Config +import Caelestia.Internal +import Caelestia.Services +import qs.components +import qs.services Item { id: root required property ShellScreen screen - required property Wallpaper wallpaper + required property Item wallpaper readonly property bool shouldBeActive: Config.background.visualiser.enabled && (!Config.background.visualiser.autoHide || (Hypr.monitorFor(screen)?.activeWorkspace?.toplevels?.values.every(t => t.lastIpcObject?.floating) ?? true)) property real offset: shouldBeActive ? 0 : screen.height * 0.2 @@ -21,6 +21,7 @@ Item { opacity: shouldBeActive ? 1 : 0 Loader { + asynchronous: true anchors.fill: parent active: root.opacity > 0 && Config.background.visualiser.blur @@ -42,6 +43,7 @@ Item { layer.enabled: true Loader { + asynchronous: true anchors.fill: parent anchors.topMargin: root.offset anchors.bottomMargin: -root.offset @@ -53,25 +55,29 @@ Item { service: Audio.cava } - Item { - id: content + VisualiserBars { + id: bars anchors.fill: parent anchors.margins: Config.border.thickness - anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing + anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Tokens.spacing.small * Config.background.visualiser.spacing - Side { - content: content - } - Side { - content: content - isRight: true - } + values: Audio.cava.values + primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) + secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) + rounding: Tokens.rounding.small * Config.background.visualiser.rounding + spacing: Tokens.spacing.small * Config.background.visualiser.spacing + animationDuration: Tokens.anim.durations.normal Behavior on anchors.leftMargin { Anim {} } } + + FrameAnimation { + running: root.opacity > 0 && !bars.settled + onTriggered: bars.advance(frameTime) + } } } } @@ -83,69 +89,4 @@ Item { Behavior on opacity { Anim {} } - - component Side: Repeater { - id: side - - required property Item content - property bool isRight - - model: Config.services.visualiserBars - - ClippingRectangle { - id: bar - - required property int modelData - property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1])) - - clip: true - - x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0) - implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing - - y: side.content.height - height - implicitHeight: bar.value * side.content.height * 0.4 - - color: "transparent" - topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding - topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding - - Rectangle { - topLeftRadius: parent.topLeftRadius - topRightRadius: parent.topRightRadius - - gradient: Gradient { - orientation: Gradient.Vertical - - GradientStop { - position: 0 - color: Qt.alpha(Colours.palette.m3primary, 0.7) - - Behavior on color { - CAnim {} - } - } - GradientStop { - position: 1 - color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) - - Behavior on color { - CAnim {} - } - } - } - - anchors.left: parent.left - anchors.right: parent.right - y: parent.height - height - implicitHeight: side.content.height * 0.4 - } - - Behavior on value { - Anim { - duration: Appearance.anim.durations.small - } - } - } - } } diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml index b5d7d4af4..8cc2df9d3 100644 --- a/modules/background/Wallpaper.qml +++ b/modules/background/Wallpaper.qml @@ -1,20 +1,19 @@ pragma ComponentBehavior: Bound +import QtQuick +import Caelestia.Config import qs.components -import qs.components.images import qs.components.filedialog +import qs.components.images import qs.services -import qs.config import qs.utils -import QtQuick Item { id: root property string source: Wallpapers.current property Image current: one - - anchors.fill: parent + property bool completed onSourceChanged: { if (!source) @@ -27,43 +26,47 @@ Item { Component.onCompleted: { if (source) - Qt.callLater(() => one.update()); + Qt.callLater(() => { + one.update(); + completed = true; + }); } Loader { + asynchronous: true anchors.fill: parent - active: !root.source + active: root.completed && !root.source sourceComponent: StyledRect { color: Colours.palette.m3surfaceContainer Row { anchors.centerIn: parent - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large MaterialIcon { text: "sentiment_stressed" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraLarge * 5 + font.pointSize: Tokens.font.size.extraLarge * 5 } Column { anchors.verticalCenter: parent.verticalCenter - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Wallpaper missing?") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 font.bold: true } StyledRect { - implicitWidth: selectWallText.implicitWidth + Appearance.padding.large * 2 - implicitHeight: selectWallText.implicitHeight + Appearance.padding.small * 2 + implicitWidth: selectWallText.implicitWidth + Tokens.padding.large * 2 + implicitHeight: selectWallText.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary FileDialog { @@ -78,10 +81,7 @@ Item { StateLayer { radius: parent.radius color: Colours.palette.m3onPrimary - - function onClicked(): void { - dialog.open(); - } + onClicked: dialog.open() } StyledText { @@ -91,7 +91,7 @@ Item { text: qsTr("Set it now!") color: Colours.palette.m3onPrimary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index cb384e393..a2ed060e7 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -1,30 +1,32 @@ pragma ComponentBehavior: Bound -import qs.services -import qs.config import "popouts" as BarPopouts import "components" import "components/workspaces" -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root required property ShellScreen screen - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts - readonly property int vPadding: Appearance.padding.large + required property bool fullscreen + readonly property int vPadding: Tokens.padding.large function closeTray(): void { if (!Config.bar.tray.compact) return; for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i); - if (item?.enabled && item.id === "tray") { - item.item.expanded = false; + const loader = repeater.itemAt(i) as WrappedLoader; + if (loader?.enabled && loader.id === "tray") { + (loader.item as Tray).expanded = false; } } } @@ -42,11 +44,9 @@ ColumnLayout { const id = ch.id; const top = ch.y; - const item = ch.item; - const itemHeight = item.implicitHeight; if (id === "statusIcons" && Config.bar.popouts.statusIcons) { - const items = item.items; + const items = (ch.item as StatusIcons).items; const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); if (icon) { popouts.currentName = icon.name; @@ -54,9 +54,10 @@ ColumnLayout { popouts.hasCurrent = true; } } else if (id === "tray" && Config.bar.popouts.tray) { - if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) { - const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count); - const trayItem = item.items.itemAt(index); + const tray = ch.item as Tray; + if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { + const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); + const trayItem = tray.items.itemAt(index); if (trayItem) { popouts.currentName = `traymenu${index}`; popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); @@ -66,11 +67,11 @@ ColumnLayout { } } else { popouts.hasCurrent = false; - item.expanded = true; + tray.expanded = true; } - } else if (id === "activeWindow" && Config.bar.popouts.activeWindow) { + } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { popouts.currentName = id.toLowerCase(); - popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y; + popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; popouts.hasCurrent = true; } } @@ -79,11 +80,11 @@ ColumnLayout { const ch = childAt(width / 2, y) as WrappedLoader; if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) { // Workspace scroll - const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); + const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); const specialWs = mon?.lastIpcObject.specialWorkspace.name; if (specialWs?.length > 0) Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`); - else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) + else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`); } else if (y < screen.height / 2 && Config.bar.scrollActions.volume) { // Volume scroll on top half @@ -95,13 +96,13 @@ ColumnLayout { // Brightness scroll on bottom half const monitor = Brightness.getMonitorForScreen(screen); if (angleDelta.y > 0) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); else if (angleDelta.y < 0) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); } } - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Repeater { id: repeater @@ -128,12 +129,15 @@ ColumnLayout { delegate: WrappedLoader { sourceComponent: Workspaces { screen: root.screen + fullscreen: root.fullscreen } } } DelegateChoice { roleValue: "activeWindow" delegate: WrappedLoader { + Layout.fillWidth: true + visible: !root.fullscreen sourceComponent: ActiveWindow { bar: root monitor: Brightness.getMonitorForScreen(root.screen) @@ -143,18 +147,21 @@ ColumnLayout { DelegateChoice { roleValue: "tray" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: Tray {} } } DelegateChoice { roleValue: "clock" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: Clock {} } } DelegateChoice { roleValue: "statusIcons" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: StatusIcons {} } } @@ -170,7 +177,7 @@ ColumnLayout { } component WrappedLoader: Loader { - required property bool enabled + required enabled required property string id required property int index @@ -193,6 +200,7 @@ ColumnLayout { return null; } + asynchronous: true Layout.alignment: Qt.AlignHCenter // Cursed ahh thing to add padding to first and last enabled components diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index 29961b62c..c57dcd647 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -1,39 +1,44 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config -import "popouts" as BarPopouts -import Quickshell import QtQuick +import Quickshell +import Caelestia.Config +import qs.components +import qs.utils +import qs.modules.bar.popouts as BarPopouts Item { id: root required property ShellScreen screen - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts - required property bool disabled + required property bool fullscreen + + readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) - readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) - readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 - readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness - readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered) + readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) + readonly property int padding: Math.max(Tokens.padding.smaller, Config.border.thickness) + readonly property int contentWidth: Tokens.sizes.bar.innerWidth + padding * 2 + readonly property int exclusiveZone: shouldBeVisible ? contentWidth : Config.border.thickness + readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) property bool isHovered function closeTray(): void { - content.item?.closeTray(); + (content.item as Bar)?.closeTray(); } function checkPopout(y: real): void { - content.item?.checkPopout(y); + (content.item as Bar)?.checkPopout(y); } function handleWheel(y: real, angleDelta: point): void { - content.item?.handleWheel(y, angleDelta); + (content.item as Bar)?.handleWheel(y, angleDelta); } - visible: width > Config.border.thickness - implicitWidth: Config.border.thickness + clip: true + visible: width > 0 + implicitWidth: fullscreen ? 0 : Config.border.thickness states: State { name: "visible" @@ -52,8 +57,7 @@ Item { Anim { target: root property: "implicitWidth" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } }, Transition { @@ -63,7 +67,7 @@ Item { Anim { target: root property: "implicitWidth" - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } ] @@ -81,7 +85,8 @@ Item { width: root.contentWidth screen: root.screen visibilities: root.visibilities - popouts: root.popouts + popouts: root.popouts // qmllint disable incompatible-type + fullscreen: root.fullscreen } } } diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml index 0c9b21e6f..f39aa4c39 100644 --- a/modules/bar/components/ActiveWindow.qml +++ b/modules/bar/components/ActiveWindow.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Caelestia.Config import qs.components import qs.services import qs.utils -import qs.config -import QtQuick Item { id: root @@ -13,6 +13,19 @@ Item { required property Brightness.Monitor monitor property color colour: Colours.palette.m3primary + readonly property string windowTitle: { + const title = Hypr.activeToplevel?.title; + if (!title) + return qsTr("Desktop"); + if (Config.bar.activeWindow.compact) { + // " - " (standard hyphen), " — " (em dash), " – " (en dash) + const parts = title.split(/\s+[\-\u2013\u2014]\s+/); + if (parts.length > 1) + return parts[parts.length - 1].trim(); + } + return title; + } + readonly property int maxHeight: { const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer"); const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0); @@ -25,6 +38,32 @@ Item { implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight) implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin + Loader { + asynchronous: true + anchors.fill: parent + active: !Config.bar.activeWindow.showOnHover + + sourceComponent: MouseArea { + cursorShape: Qt.PointingHandCursor + hoverEnabled: true + onPositionChanged: { + const popouts = root.bar.popouts; + if (popouts.hasCurrent && popouts.currentName !== "activewindow") + popouts.hasCurrent = false; + } + onClicked: { + const popouts = root.bar.popouts; + if (popouts.hasCurrent) { + popouts.hasCurrent = false; + } else { + popouts.currentName = "activewindow"; + popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y; + popouts.hasCurrent = true; + } + } + } + } + MaterialIcon { id: icon @@ -46,9 +85,9 @@ Item { TextMetrics { id: metrics - text: Hypr.activeToplevel?.title ?? qsTr("Desktop") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.mono + text: root.windowTitle + font.pointSize: root.Tokens.font.size.smaller + font.family: root.Tokens.font.family.mono elide: Qt.ElideRight elideWidth: root.maxHeight - icon.height @@ -62,8 +101,7 @@ Item { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -72,7 +110,7 @@ Item { anchors.horizontalCenter: icon.horizontalCenter anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small font.pointSize: metrics.font.pointSize font.family: metrics.font.family @@ -81,10 +119,10 @@ Item { transform: [ Translate { - x: Config.bar.activeWindow.inverted ? -implicitWidth + text.implicitHeight : 0 + x: root.Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0 }, Rotation { - angle: Config.bar.activeWindow.inverted ? 270 : 90 + angle: root.Config.bar.activeWindow.inverted ? 270 : 90 origin.x: text.implicitHeight / 2 origin.y: text.implicitHeight / 2 } diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml index 801e93d77..ffe599afd 100644 --- a/modules/bar/components/Clock.qml +++ b/modules/bar/components/Clock.qml @@ -1,38 +1,71 @@ pragma ComponentBehavior: Bound +import QtQuick +import Caelestia.Config import qs.components import qs.services -import qs.config -import QtQuick -Column { +StyledRect { id: root - property color colour: Colours.palette.m3tertiary + readonly property color colour: Colours.palette.m3tertiary + readonly property int padding: Config.bar.clock.background ? Tokens.padding.normal : Tokens.padding.small + + implicitWidth: Tokens.sizes.bar.innerWidth + implicitHeight: layout.implicitHeight + root.padding * 2 + + color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) + radius: Tokens.rounding.full - spacing: Appearance.spacing.small + Column { + id: layout - Loader { - anchors.horizontalCenter: parent.horizontalCenter + anchors.centerIn: parent + spacing: Tokens.spacing.small + + Loader { + asynchronous: true + anchors.horizontalCenter: parent.horizontalCenter + + active: Config.bar.clock.showIcon + visible: active + + sourceComponent: MaterialIcon { + text: "calendar_month" + color: root.colour + } + } - active: Config.bar.clock.showIcon - visible: active + StyledText { + anchors.horizontalCenter: parent.horizontalCenter - sourceComponent: MaterialIcon { - text: "calendar_month" + visible: Config.bar.clock.showDate + + horizontalAlignment: StyledText.AlignHCenter + text: Time.format("ddd\nd") + font.pointSize: Tokens.font.size.smaller + font.family: Tokens.font.family.sans color: root.colour } - } - StyledText { - id: text + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + visible: Config.bar.clock.showDate + height: visible ? 1 : 0 - anchors.horizontalCenter: parent.horizontalCenter + width: parent.width * 0.8 + color: root.colour + opacity: 0.2 + } - horizontalAlignment: StyledText.AlignHCenter - text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") - font.pointSize: Appearance.font.size.smaller - font.family: Appearance.font.family.mono - color: root.colour + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + horizontalAlignment: StyledText.AlignHCenter + text: Time.format(GlobalConfig.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") + font.pointSize: Tokens.font.size.smaller + font.family: Tokens.font.family.mono + color: root.colour + } } } diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml index 2bc386441..94d1a1c4d 100644 --- a/modules/bar/components/OsIcon.qml +++ b/modules/bar/components/OsIcon.qml @@ -1,12 +1,16 @@ +import QtQuick +import Caelestia.Config +import qs.components import qs.components.effects import qs.services -import qs.config import qs.utils -import QtQuick Item { id: root + implicitWidth: Math.round(Tokens.font.size.large * 1.2) + implicitHeight: Math.round(Tokens.font.size.large * 1.2) + MouseArea { anchors.fill: parent cursorShape: Qt.PointingHandCursor @@ -16,13 +20,28 @@ Item { } } - ColouredIcon { + Loader { + asynchronous: true anchors.centerIn: parent - source: SysInfo.osLogo - implicitSize: Appearance.font.size.large * 1.2 - colour: Colours.palette.m3tertiary + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon + } + + Component { + id: caelestiaLogo + + Logo { + implicitWidth: Math.round(Tokens.font.size.large * 1.6) + implicitHeight: Math.round(Tokens.font.size.large * 1.6) + } } - implicitWidth: Appearance.font.size.large * 1.2 - implicitHeight: Appearance.font.size.large * 1.2 + Component { + id: distroIcon + + ColouredIcon { + source: SysInfo.osLogo + implicitSize: Math.round(Tokens.font.size.large * 1.2) + colour: Colours.palette.m3tertiary + } + } } diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml index 917bdf7fd..681a805d7 100644 --- a/modules/bar/components/Power.qml +++ b/modules/bar/components/Power.qml @@ -1,15 +1,14 @@ +import QtQuick +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell -import QtQuick Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities - implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 implicitHeight: icon.implicitHeight StateLayer { @@ -17,13 +16,9 @@ Item { anchors.fill: undefined anchors.centerIn: parent implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 - - radius: Appearance.rounding.full - - function onClicked(): void { - root.visibilities.session = !root.visibilities.session; - } + implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 + radius: Tokens.rounding.full + onClicked: root.visibilities.session = !root.visibilities.session } MaterialIcon { @@ -35,6 +30,6 @@ Item { text: "power_settings_new" color: Colours.palette.m3error font.bold: true - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml index ca7dc2e3a..900e55747 100644 --- a/modules/bar/components/StatusIcons.qml +++ b/modules/bar/components/StatusIcons.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.utils -import qs.config +import QtQuick +import QtQuick.Layouts import Quickshell import Quickshell.Bluetooth import Quickshell.Services.UPower -import QtQuick -import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services +import qs.utils StyledRect { id: root @@ -17,11 +17,11 @@ StyledRect { readonly property alias items: iconColumn color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full clip: true - implicitWidth: Config.bar.sizes.innerWidth - implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) + implicitWidth: Tokens.sizes.bar.innerWidth + implicitHeight: iconColumn.implicitHeight + Tokens.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) ColumnLayout { id: iconColumn @@ -29,9 +29,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.bottomMargin: Appearance.padding.normal + anchors.bottomMargin: Tokens.padding.normal - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 // Lock keys status WrappedLoader { @@ -136,7 +136,7 @@ StyledRect { animate: true text: Hypr.kbLayout color: root.colour - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono } } @@ -172,15 +172,15 @@ StyledRect { active: Config.bar.status.showBluetooth sourceComponent: ColumnLayout { - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 // Bluetooth icon MaterialIcon { animate: true text: { - if (!Bluetooth.defaultAdapter?.enabled) + if (!Bluetooth.defaultAdapter?.enabled) // qmllint disable unresolved-type return "bluetooth_disabled"; - if (Bluetooth.devices.values.some(d => d.connected)) + if (Bluetooth.devices.values.some(d => d.connected)) // qmllint disable unresolved-type return "bluetooth_connected"; return "bluetooth"; } @@ -190,7 +190,7 @@ StyledRect { // Connected bluetooth devices Repeater { model: ScriptModel { - values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) + values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) // qmllint disable unresolved-type } MaterialIcon { @@ -204,21 +204,21 @@ StyledRect { fill: 1 SequentialAnimation on opacity { - running: device.modelData?.state !== BluetoothDeviceState.Connected + running: device.modelData?.state !== BluetoothDeviceState.Connected // qmllint disable unresolved-type alwaysRunToEnd: true loops: Animation.Infinite Anim { from: 1 to: 0 - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.large + easing: Tokens.anim.standardAccel } Anim { from: 0 to: 1 - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.large + easing: Tokens.anim.standardDecel } } } @@ -264,6 +264,7 @@ StyledRect { component WrappedLoader: Loader { required property string name + asynchronous: true Layout.alignment: Qt.AlignHCenter visible: active } diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml index 96956f6f4..85a920c13 100644 --- a/modules/bar/components/Tray.qml +++ b/modules/bar/components/Tray.qml @@ -1,10 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Services.SystemTray +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell.Services.SystemTray -import QtQuick StyledRect { id: root @@ -13,8 +14,8 @@ StyledRect { readonly property alias items: items readonly property alias expandIcon: expandIcon - readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small - readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0 + readonly property int padding: Config.bar.tray.background ? Tokens.padding.normal : Tokens.padding.small + readonly property int spacing: Config.bar.tray.background ? Tokens.spacing.small : 0 property bool expanded @@ -27,11 +28,11 @@ StyledRect { clip: true visible: height > 0 - implicitWidth: Config.bar.sizes.innerWidth + implicitWidth: Tokens.sizes.bar.innerWidth implicitHeight: nonAnimHeight color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.full + radius: Tokens.rounding.full Column { id: layout @@ -39,7 +40,7 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.topMargin: root.padding - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0 @@ -48,7 +49,7 @@ StyledRect { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -56,7 +57,7 @@ StyledRect { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" @@ -66,7 +67,9 @@ StyledRect { Repeater { id: items - model: SystemTray.items + model: ScriptModel { + values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) + } TrayItem {} } @@ -79,23 +82,25 @@ StyledRect { Loader { id: expandIcon + asynchronous: true + anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - active: Config.bar.tray.compact + active: Config.bar.tray.compact && items.count > 0 sourceComponent: Item { implicitWidth: expandIconInner.implicitWidth - implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2 + implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small * 2 MaterialIcon { id: expandIconInner anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small + anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.small : -Tokens.padding.small text: "expand_less" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large rotation: root.expanded ? 180 : 0 Behavior on rotation { @@ -111,8 +116,7 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml index 99119073d..fefb532c9 100644 --- a/modules/bar/components/TrayItem.qml +++ b/modules/bar/components/TrayItem.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell.Services.SystemTray +import Caelestia.Config import qs.components.effects import qs.services -import qs.config import qs.utils -import Quickshell.Services.SystemTray -import QtQuick MouseArea { id: root @@ -13,8 +13,8 @@ MouseArea { required property SystemTrayItem modelData acceptedButtons: Qt.LeftButton | Qt.RightButton - implicitWidth: Appearance.font.size.small * 2 - implicitHeight: Appearance.font.size.small * 2 + implicitWidth: Tokens.font.size.small * 2 + implicitHeight: Tokens.font.size.small * 2 onClicked: event => { if (event.button === Qt.LeftButton) diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index dae54b371..ebc0caf26 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -1,8 +1,10 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import qs.config -import QtQuick StyledRect { id: root @@ -10,6 +12,7 @@ StyledRect { required property int activeWsId required property Repeater workspaces required property Item mask + required property bool fullscreen readonly property int currentWsIdx: { let i = activeWsId - 1; @@ -20,13 +23,12 @@ StyledRect { property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 - property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0 + property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0 property real offset: Math.min(leading, trailing) property real size: { const s = Math.abs(leading - trailing) + currentSize; if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) { - const ws = workspaces.itemAt(lastWs); - // console.log(ws, lastWs); + const ws = workspaces.itemAt(lastWs) as Workspace; return ws ? Math.min(ws.y + ws.size - offset, s) : 0; } return s; @@ -42,9 +44,9 @@ StyledRect { clip: true y: offset + mask.y - implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 implicitHeight: size - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary Colouriser { @@ -61,38 +63,38 @@ StyledRect { } Behavior on leading { - enabled: Config.bar.workspaces.activeTrail + enabled: root.Config.bar.workspaces.activeTrail EAnim {} } Behavior on trailing { - enabled: Config.bar.workspaces.activeTrail + enabled: root.Config.bar.workspaces.activeTrail EAnim { - duration: Appearance.anim.durations.normal * 2 + duration: Tokens.anim.durations.normal * 2 } } Behavior on currentSize { - enabled: Config.bar.workspaces.activeTrail + enabled: root.Config.bar.workspaces.activeTrail EAnim {} } Behavior on offset { - enabled: !Config.bar.workspaces.activeTrail + enabled: !root.Config.bar.workspaces.activeTrail EAnim {} } Behavior on size { - enabled: !Config.bar.workspaces.activeTrail + enabled: !root.Config.bar.workspaces.activeTrail EAnim {} } component EAnim: Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml index 56b215e67..2bd3c8cb8 100644 --- a/modules/bar/components/workspaces/OccupiedBg.qml +++ b/modules/bar/components/workspaces/OccupiedBg.qml @@ -1,10 +1,10 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell -import QtQuick Item { id: root @@ -52,8 +52,8 @@ Item { required property var modelData - readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null - readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null + readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null // qmllint disable incompatible-type + readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null // qmllint disable incompatible-type function getWsIdx(ws: int): int { let i = ws - 1; @@ -65,18 +65,18 @@ Item { anchors.horizontalCenter: root.horizontalCenter y: (start?.y ?? 0) - 1 - implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2 + implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + 2 implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full scale: 0 Component.onCompleted: scale = 1 Behavior on scale { Anim { - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -90,14 +90,14 @@ Item { } } - component Pill: QtObject { - property int start - property int end - } - Component { id: pillComp Pill {} } + + component Pill: QtObject { + property int start + property int end + } } diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml index ad85af89e..3ed382e90 100644 --- a/modules/bar/components/workspaces/SpecialWorkspaces.qml +++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Caelestia.Config import qs.components import qs.components.effects import qs.services import qs.utils -import qs.config -import Quickshell -import Quickshell.Hyprland -import QtQuick -import QtQuick.Layouts Item { id: root required property ShellScreen screen readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) - readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? "" + readonly property string activeSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" layer.enabled: true layer.effect: OpacityMask { @@ -31,7 +31,7 @@ Item { Rectangle { anchors.fill: parent - radius: Appearance.rounding.full + radius: Tokens.rounding.full gradient: Gradient { orientation: Gradient.Vertical @@ -60,7 +60,7 @@ Item { anchors.left: parent.left anchors.right: parent.right - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitHeight: parent.height / 2 opacity: view.contentY > 0 ? 0 : 1 @@ -74,9 +74,9 @@ Item { anchors.left: parent.left anchors.right: parent.right - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitHeight: parent.height / 2 - opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1 + opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.small ? 0 : 1 Behavior on opacity { Anim {} @@ -88,14 +88,14 @@ Item { id: view anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal interactive: false currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial)) model: ScriptModel { - values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) + values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!GlobalConfig.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) } preferredHighlightBegin: 0 @@ -105,150 +105,21 @@ Item { highlightFollowsCurrentItem: false highlight: Item { y: view.currentItem?.y ?? 0 - implicitHeight: view.currentItem?.size ?? 0 + implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 Behavior on y { Anim {} } } - delegate: ColumnLayout { - id: ws - - required property HyprlandWorkspace modelData - readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) - property int wsId - property string icon - property bool hasWindows - - anchors.left: view.contentItem.left - anchors.right: view.contentItem.right - - spacing: 0 - - Component.onCompleted: { - wsId = modelData.id; - icon = Icons.getSpecialWsIcon(modelData.name); - hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; - } - - // Hacky thing cause modelData gets destroyed before the remove anim finishes - Connections { - target: ws.modelData - - function onIdChanged(): void { - if (ws.modelData) - ws.wsId = ws.modelData.id; - } - - function onNameChanged(): void { - if (ws.modelData) - ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); - } - - function onLastIpcObjectChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - } - - Connections { - target: Config.bar.workspaces - - function onShowWindowsOnSpecialWorkspacesChanged(): void { - if (ws.modelData) - ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; - } - } - - Loader { - id: label - - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 - - sourceComponent: ws.icon.length === 1 ? letterComp : iconComp - - Component { - id: iconComp - - MaterialIcon { - fill: 1 - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - - Component { - id: letterComp - - StyledText { - text: ws.icon - verticalAlignment: Qt.AlignVCenter - } - } - } - - Loader { - id: windows - - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true - Layout.preferredHeight: implicitHeight - - visible: active - active: ws.hasWindows - - sourceComponent: Column { - spacing: 0 - - add: Transition { - Anim { - properties: "scale" - from: 0 - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - } - - move: Transition { - Anim { - properties: "scale" - to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel - } - Anim { - properties: "x,y" - } - } - - Repeater { - model: ScriptModel { - values: Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId) - } - - MaterialIcon { - required property var modelData - - grade: 0 - text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") - color: Colours.palette.m3onSurfaceVariant - } - } - } - - Behavior on Layout.preferredHeight { - Anim {} - } - } - } + delegate: SpecialWsDelegate {} add: Transition { Anim { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -256,12 +127,12 @@ Item { Anim { property: "scale" to: 0.5 - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } Anim { property: "opacity" to: 0 - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } @@ -269,7 +140,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" @@ -280,7 +151,7 @@ Item { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" @@ -289,6 +160,7 @@ Item { } Loader { + asynchronous: true active: Config.bar.workspaces.activeIndicator anchors.fill: parent @@ -300,10 +172,10 @@ Item { anchors.right: parent.right y: (view.currentItem?.y ?? 0) - view.contentY - implicitHeight: view.currentItem?.size ?? 0 + implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 color: Colours.palette.m3tertiary - radius: Appearance.rounding.full + radius: Tokens.rounding.full Colouriser { source: view @@ -320,13 +192,13 @@ Item { Behavior on y { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } Behavior on implicitHeight { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } } @@ -341,7 +213,7 @@ Item { drag.target: view.contentItem drag.axis: Drag.YAxis drag.maximumY: 0 - drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) + drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.small) onPressed: event => startY = event.y @@ -349,11 +221,150 @@ Item { if (Math.abs(event.y - startY) > drag.threshold) return; - const ws = view.itemAt(event.x, event.y); + const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; if (ws?.modelData) Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); else Hypr.dispatch("togglespecialworkspace special"); } } + + component SpecialWsDelegate: ColumnLayout { + id: ws + + required property HyprlandWorkspace modelData + readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.small : 0) + property int wsId + property string icon + property bool hasWindows + + anchors.left: view.contentItem.left + anchors.right: view.contentItem.right + + spacing: 0 + + Component.onCompleted: { + wsId = modelData.id; + icon = Icons.getSpecialWsIcon(modelData.name); + hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; + } + + // Hacky thing cause modelData gets destroyed before the remove anim finishes + Connections { + function onIdChanged(): void { + if (ws.modelData) + ws.wsId = ws.modelData.id; + } + + function onNameChanged(): void { + if (ws.modelData) + ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); + } + + function onLastIpcObjectChanged(): void { + if (ws.modelData) + ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + + target: ws.modelData + } + + Connections { + function onShowWindowsOnSpecialWorkspacesChanged(): void { + if (ws.modelData) + ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; + } + + target: root.Config.bar.workspaces + } + + Loader { + id: label + + asynchronous: true + + Layout.alignment: Qt.AlignHCenter | Qt.AlignTop + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + + sourceComponent: ws.icon.length === 1 ? letterComp : iconComp + + Component { + id: iconComp + + MaterialIcon { + fill: 1 + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + + Component { + id: letterComp + + StyledText { + text: ws.icon + verticalAlignment: Qt.AlignVCenter + } + } + } + + Loader { + id: windows + + asynchronous: true + + Layout.alignment: Qt.AlignHCenter + Layout.fillHeight: true + Layout.preferredHeight: implicitHeight + + visible: active + active: ws.hasWindows + + sourceComponent: Column { + spacing: 0 + + add: Transition { + Anim { + properties: "scale" + from: 0 + to: 1 + easing: Tokens.anim.standardDecel + } + } + + move: Transition { + Anim { + properties: "scale" + to: 1 + easing: Tokens.anim.standardDecel + } + Anim { + properties: "x,y" + } + } + + Repeater { + model: ScriptModel { + values: { + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); + const maxIcons = root.Config.bar.workspaces.maxWindowIcons; + return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; + } + } + + MaterialIcon { + required property var modelData + + grade: 0 + text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") + color: Colours.palette.m3onSurfaceVariant + } + } + } + + Behavior on Layout.preferredHeight { + Anim {} + } + } + } } diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml index 3c8238b57..bd581aab1 100644 --- a/modules/bar/components/workspaces/Workspace.qml +++ b/modules/bar/components/workspaces/Workspace.qml @@ -1,10 +1,12 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components import qs.services import qs.utils -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root @@ -16,7 +18,7 @@ ColumnLayout { readonly property bool isWorkspace: true // Flag for finding workspace children // Unanimated prop for others to use as reference - readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0) + readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.small : 0) readonly property int ws: groupOffset + index + 1 readonly property bool isOccupied: occupied[ws] ?? false @@ -31,7 +33,7 @@ ColumnLayout { id: indicator Layout.alignment: Qt.AlignHCenter | Qt.AlignTop - Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 animate: true text: { @@ -55,9 +57,11 @@ ColumnLayout { Loader { id: windows + asynchronous: true + Layout.alignment: Qt.AlignHCenter Layout.fillHeight: true - Layout.topMargin: -Config.bar.sizes.innerWidth / 10 + Layout.topMargin: -Tokens.sizes.bar.innerWidth / 10 visible: active active: root.hasWindows @@ -70,7 +74,7 @@ ColumnLayout { properties: "scale" from: 0 to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } } @@ -78,7 +82,7 @@ ColumnLayout { Anim { properties: "scale" to: 1 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { properties: "x,y" @@ -87,7 +91,12 @@ ColumnLayout { Repeater { model: ScriptModel { - values: Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws) + values: { + const ws = root.ws; + const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); + const maxIcons = root.Config.bar.workspaces.maxWindowIcons; + return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; + } } MaterialIcon { diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index bfa80ab68..2030bf80e 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -1,39 +1,43 @@ pragma ComponentBehavior: Bound -import qs.services -import qs.config -import qs.components -import Quickshell import QtQuick -import QtQuick.Layouts import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.services StyledClippingRect { id: root required property ShellScreen screen + required property bool fullscreen - readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== "" - readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId + readonly property bool onSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" + readonly property int activeWsId: GlobalConfig.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId - readonly property var occupied: Hypr.workspaces.values.reduce((acc, curr) => { - acc[curr.id] = curr.lastIpcObject.windows > 0; - return acc; - }, {}) + readonly property var occupied: { + const occ = {}; + for (const ws of Hypr.workspaces.values) + occ[ws.id] = ws.lastIpcObject.windows > 0; + return occ; + } readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown property real blur: onSpecial ? 1 : 0 - implicitWidth: Config.bar.sizes.innerWidth - implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 + implicitWidth: Tokens.sizes.bar.innerWidth + implicitHeight: layout.implicitHeight + Tokens.padding.small * 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full Item { anchors.fill: parent scale: root.onSpecial ? 0.8 : 1 opacity: root.onSpecial ? 0.5 : 1 + visible: !root.fullscreen layer.enabled: root.blur > 0 layer.effect: MultiEffect { @@ -43,10 +47,11 @@ StyledClippingRect { } Loader { + asynchronous: true active: Config.bar.workspaces.occupiedBg anchors.fill: parent - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small sourceComponent: OccupiedBg { workspaces: workspaces @@ -59,7 +64,7 @@ StyledClippingRect { id: layout anchors.centerIn: parent - spacing: Math.floor(Appearance.spacing.small / 2) + spacing: Math.floor(Tokens.spacing.small / 2) Repeater { id: workspaces @@ -75,6 +80,7 @@ StyledClippingRect { } Loader { + asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: Config.bar.workspaces.activeIndicator @@ -82,13 +88,14 @@ StyledClippingRect { activeWsId: root.activeWsId workspaces: workspaces mask: layout + fullscreen: root.fullscreen } } MouseArea { anchors.fill: layout onClicked: event => { - const ws = layout.childAt(event.x, event.y).ws; + const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws; if (Hypr.activeWsId !== ws) Hypr.dispatch(`workspace ${ws}`); else @@ -108,8 +115,10 @@ StyledClippingRect { Loader { id: specialWs + asynchronous: true + anchors.fill: parent - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small active: opacity > 0 @@ -131,7 +140,7 @@ StyledClippingRect { Behavior on blur { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml index adf7b7740..122466451 100644 --- a/modules/bar/popouts/ActiveWindow.qml +++ b/modules/bar/popouts/ActiveWindow.qml @@ -1,36 +1,37 @@ +import QtQuick +import QtQuick.Layouts +import Quickshell.Wayland +import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.services import qs.utils -import qs.config -import Quickshell.Widgets -import Quickshell.Wayland -import QtQuick -import QtQuick.Layouts Item { id: root - required property Item wrapper + required property PopoutState popouts - implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 + implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.large * 2 implicitHeight: child.implicitHeight Column { id: child anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { id: detailsRow anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IconImage { id: icon + asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: details.implicitHeight source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing") @@ -45,7 +46,7 @@ Item { StyledText { Layout.fillWidth: true text: Hypr.activeToplevel?.title ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } @@ -58,17 +59,14 @@ Item { } Item { - implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2 - implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: expandIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: expandIcon.implicitHeight + Tokens.padding.small * 2 Layout.alignment: Qt.AlignVCenter StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { - root.wrapper.detach("winfo"); - } + radius: Tokens.rounding.normal + onClicked: root.popouts.detachRequested("winfo") } MaterialIcon { @@ -79,23 +77,23 @@ Item { text: "chevron_right" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } ClippingWrapperRectangle { color: "transparent" - radius: Appearance.rounding.small + radius: Tokens.rounding.small ScreencopyView { id: preview - captureSource: Hypr.activeToplevel?.wayland ?? null + captureSource: Hypr.activeToplevel?.wayland ?? null // qmllint disable unresolved-type live: visible - constraintSize.width: Config.bar.sizes.windowPreviewSize - constraintSize.height: Config.bar.sizes.windowPreviewSize + constraintSize.width: Tokens.sizes.bar.windowPreviewSize + constraintSize.height: Tokens.sizes.bar.windowPreviewSize } } } diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml index 58b29ba8d..e8dc92032 100644 --- a/modules/bar/popouts/Audio.qml +++ b/modules/bar/popouts/Audio.qml @@ -1,23 +1,21 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.Pipewire +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config -import Quickshell -import Quickshell.Services.Pipewire -import QtQuick -import QtQuick.Layouts -import QtQuick.Controls -import "../../controlcenter/network" Item { id: root - required property var wrapper + required property PopoutState popouts - implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.normal * 2 ButtonGroup { id: sinks @@ -32,7 +30,7 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Output device") @@ -55,7 +53,7 @@ Item { } StyledText { - Layout.topMargin: Appearance.spacing.smaller + Layout.topMargin: Tokens.spacing.smaller text: qsTr("Input device") font.weight: 500 } @@ -74,15 +72,15 @@ Item { } StyledText { - Layout.topMargin: Appearance.spacing.smaller - Layout.bottomMargin: -Appearance.spacing.small / 2 + Layout.topMargin: Tokens.spacing.smaller + Layout.bottomMargin: -Tokens.spacing.small / 2 text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) font.weight: 500 } CustomMouseArea { Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 onWheel: event => { if (event.angleDelta.y > 0) @@ -107,14 +105,14 @@ Item { IconTextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Appearance.padding.small + verticalPadding: Tokens.padding.small text: qsTr("Open settings") icon: "settings" - onClicked: root.wrapper.detach("audio") + onClicked: root.popouts.detachRequested("audio") } } } diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml index ac975e1b7..93d2012bd 100644 --- a/modules/bar/popouts/Battery.qml +++ b/modules/bar/popouts/Battery.qml @@ -1,16 +1,16 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell.Services.UPower +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell.Services.UPower -import QtQuick Column { id: root - spacing: Appearance.spacing.normal - width: Config.bar.sizes.batteryWidth + spacing: Tokens.spacing.normal + width: Tokens.sizes.bar.batteryWidth StyledText { text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected") @@ -37,18 +37,19 @@ Column { } Loader { + asynchronous: true anchors.horizontalCenter: parent.horizontalCenter active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None - height: active ? (item?.implicitHeight ?? 0) : 0 + height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 sourceComponent: StyledRect { - implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: child.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: child.implicitHeight + Tokens.padding.smaller * 2 color: Colours.palette.m3error - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Column { id: child @@ -57,7 +58,7 @@ Column { Row { anchors.horizontalCenter: parent.horizontalCenter - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { anchors.verticalCenter: parent.verticalCenter @@ -71,7 +72,7 @@ Column { anchors.verticalCenter: parent.verticalCenter text: qsTr("Performance Degraded") color: Colours.palette.m3onError - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono font.weight: 500 } @@ -108,17 +109,17 @@ Column { anchors.horizontalCenter: parent.horizontalCenter - implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2 - implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2 + implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.normal * 2 + Tokens.spacing.large * 2 + implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small * 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full StyledRect { id: indicator color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full state: profiles.current states: [ @@ -146,10 +147,8 @@ Column { ] transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.emphasized + AnchorAnim { + type: AnchorAnim.Emphasized } } } @@ -159,7 +158,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Appearance.padding.small + anchors.leftMargin: Tokens.padding.small profile: PowerProfile.PowerSaver icon: "energy_savings_leaf" @@ -179,7 +178,7 @@ Column { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Appearance.padding.small + anchors.rightMargin: Tokens.padding.small profile: PowerProfile.Performance icon: "rocket_launch" @@ -200,16 +199,13 @@ Column { required property string icon required property int profile - implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 - implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 StateLayer { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - function onClicked(): void { - PowerProfiles.profile = parent.profile; - } + onClicked: PowerProfiles.profile = parent.profile } MaterialIcon { @@ -218,7 +214,7 @@ Column { anchors.centerIn: parent text: parent.icon - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface fill: profiles.current === text ? 1 : 0 diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml index 676da82f5..baca115ea 100644 --- a/modules/bar/popouts/Bluetooth.qml +++ b/modules/bar/popouts/Bluetooth.qml @@ -1,35 +1,35 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config import qs.utils -import Quickshell -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts -import "../../controlcenter/network" ColumnLayout { id: root - required property Item wrapper + required property PopoutState popouts - spacing: Appearance.spacing.small + width: 300 + spacing: Tokens.spacing.small StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Tokens.padding.normal + Layout.rightMargin: Tokens.padding.small text: qsTr("Bluetooth") font.weight: 500 } Toggle { label: qsTr("Enabled") - checked: Bluetooth.defaultAdapter?.enabled ?? false + checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; + const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type if (adapter) adapter.enabled = checked; } @@ -37,19 +37,19 @@ ColumnLayout { Toggle { label: qsTr("Discovering") - checked: Bluetooth.defaultAdapter?.discovering ?? false + checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type toggle.onToggled: { - const adapter = Bluetooth.defaultAdapter; + const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type if (adapter) adapter.discovering = checked; } } StyledText { - Layout.topMargin: Appearance.spacing.small - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Tokens.spacing.small + Layout.rightMargin: Tokens.padding.small text: { - const devices = Bluetooth.devices.values; + const devices = Bluetooth.devices.values; // qmllint disable unresolved-type let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); const connected = devices.filter(d => d.connected).length; if (connected > 0) @@ -57,23 +57,23 @@ ColumnLayout { return available; } color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Repeater { model: ScriptModel { - values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) + values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type } RowLayout { id: device required property BluetoothDevice modelData - readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting + readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.small opacity: 0 scale: 0.7 @@ -96,20 +96,27 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 Layout.fillWidth: true text: device.modelData.name + elide: Text.ElideRight + } + + MaterialIcon { + visible: device.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type + text: Icons.getBatteryIcon(device.modelData.batteryAvailable ? device.modelData.battery * 100 : -1) + color: device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant } StyledRect { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.small - radius: Appearance.rounding.full - color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) + radius: Tokens.rounding.full + color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type CircularIndicator { anchors.fill: parent @@ -117,12 +124,9 @@ ColumnLayout { } StateLayer { - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type disabled: device.loading - - function onClicked(): void { - device.modelData.connected = !device.modelData.connected; - } + onClicked: device.modelData.connected = !device.modelData.connected } MaterialIcon { @@ -131,7 +135,7 @@ ColumnLayout { anchors.centerIn: parent animate: true text: device.modelData.connected ? "link_off" : "link" - color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface + color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type opacity: device.loading ? 0 : 1 @@ -142,17 +146,16 @@ ColumnLayout { } Loader { + visible: status === Loader.Ready + asynchronous: true active: device.modelData.bonded sourceComponent: Item { implicitWidth: connectBtn.implicitWidth implicitHeight: connectBtn.implicitHeight StateLayer { - radius: Appearance.rounding.full - - function onClicked(): void { - device.modelData.forget(); - } + radius: Tokens.rounding.full + onClicked: device.modelData.forget() } MaterialIcon { @@ -166,14 +169,14 @@ ColumnLayout { IconTextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer - verticalPadding: Appearance.padding.small + verticalPadding: Tokens.padding.small text: qsTr("Open settings") icon: "settings" - onClicked: root.wrapper.detach("bluetooth") + onClicked: root.popouts.detachRequested("bluetooth") } component Toggle: RowLayout { @@ -182,8 +185,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.normal + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml new file mode 100644 index 000000000..ab80d2cc9 --- /dev/null +++ b/modules/bar/popouts/ClipWrapper.qml @@ -0,0 +1,67 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import qs.components +import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others + +Item { + id: root + + required property ShellScreen screen + required property real borderThickness + + readonly property alias content: content + property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 + + visible: width > 0 && height > 0 + clip: true + + implicitWidth: content.implicitWidth * (1 - offsetScale) + implicitHeight: content.implicitHeight + + x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0 + y: { + if (content.isDetached) + return (parent.height - content.nonAnimHeight) / 2; + + const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2; + const diff = parent.height - Math.floor(off + content.nonAnimHeight); + if (diff < 0) + return off + diff; + return Math.max(off, 0); + } + + Behavior on offsetScale { + Anim { + type: Anim.DefaultSpatial + } + } + + Behavior on x { + Anim { + duration: content.animLength + easing: content.animCurve + } + } + + Behavior on y { + enabled: root.offsetScale < 1 + + Anim { + duration: content.animLength + easing: content.animCurve + } + } + + Wrapper { + id: content + + screen: root.screen + offsetScale: root.offsetScale + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale + } +} diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml index 40768444f..42c5a7667 100644 --- a/modules/bar/popouts/Content.qml +++ b/modules/bar/popouts/Content.qml @@ -1,43 +1,41 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config +import "./kblayout" +import QtQuick import Quickshell import Quickshell.Services.SystemTray -import QtQuick - -import "./kblayout" +import Caelestia.Config +import qs.components Item { id: root - required property Item wrapper + required property PopoutState popouts readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null readonly property Item current: currentPopout?.item ?? null - anchors.centerIn: parent - - implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2 - implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2 + implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.large * 2 + implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.large * 2 Item { id: content anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large Popout { name: "activewindow" sourceComponent: ActiveWindow { - wrapper: root.wrapper + popouts: root.popouts } } Popout { id: networkPopout + name: "network" sourceComponent: Network { - wrapper: root.wrapper + popouts: root.popouts view: "wireless" } } @@ -45,60 +43,64 @@ Item { Popout { name: "ethernet" sourceComponent: Network { - wrapper: root.wrapper + popouts: root.popouts view: "ethernet" } } Popout { id: passwordPopout + name: "wirelesspassword" sourceComponent: WirelessPassword { id: passwordComponent - wrapper: root.wrapper - network: networkPopout.item?.passwordNetwork ?? null + + popouts: root.popouts + network: (networkPopout.item as Network)?.passwordNetwork ?? null } Connections { - target: root.wrapper function onCurrentNameChanged() { // Update network immediately when password popout becomes active - if (root.wrapper.currentName === "wirelesspassword") { + if (root.popouts.currentName === "wirelesspassword") { // Set network immediately if available - if (networkPopout.item && networkPopout.item.passwordNetwork) { + if ((networkPopout.item as Network)?.passwordNetwork) { if (passwordPopout.item) { - passwordPopout.item.network = networkPopout.item.passwordNetwork; + (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } } // Also try after a short delay in case networkPopout.item wasn't ready Qt.callLater(() => { - if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { - passwordPopout.item.network = networkPopout.item.passwordNetwork; + if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) { + (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } }, 100); } } + + target: root.popouts } Connections { - target: networkPopout function onItemChanged() { // When network popout loads, update password popout if it's active - if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { + if (root.popouts.currentName === "wirelesspassword" && passwordPopout.item) { Qt.callLater(() => { - if (networkPopout.item && networkPopout.item.passwordNetwork) { - passwordPopout.item.network = networkPopout.item.passwordNetwork; + if ((networkPopout.item as Network)?.passwordNetwork) { + (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; } }); } } + + target: networkPopout } } Popout { name: "bluetooth" sourceComponent: Bluetooth { - wrapper: root.wrapper + popouts: root.popouts } } @@ -110,15 +112,13 @@ Item { Popout { name: "audio" sourceComponent: Audio { - wrapper: root.wrapper + popouts: root.popouts } } Popout { name: "kblayout" - sourceComponent: KbLayout { - wrapper: root.wrapper - } + sourceComponent: KbLayout {} } Popout { @@ -128,7 +128,7 @@ Item { Repeater { model: ScriptModel { - values: [...SystemTray.items.values] + values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) } Popout { @@ -141,22 +141,22 @@ Item { sourceComponent: trayMenuComp Connections { - target: root.wrapper - function onHasCurrentChanged(): void { - if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { + if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { trayMenu.sourceComponent = null; trayMenu.sourceComponent = trayMenuComp; } } + + target: root.popouts } Component { id: trayMenuComp TrayMenu { - popouts: root.wrapper - trayItem: trayMenu.modelData.menu + popouts: root.popouts + trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type } } } @@ -167,10 +167,9 @@ Item { id: popout required property string name - readonly property bool shouldBeActive: root.wrapper.currentName === name + readonly property bool shouldBeActive: root.popouts.currentName === name - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right + anchors.centerIn: parent opacity: 0 scale: 0.8 @@ -195,7 +194,7 @@ Item { SequentialAnimation { Anim { properties: "opacity,scale" - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } PropertyAction { target: popout diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml index 7d74530e3..caab7487f 100644 --- a/modules/bar/popouts/LockStatus.qml +++ b/modules/bar/popouts/LockStatus.qml @@ -1,10 +1,10 @@ +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.services -import qs.config -import QtQuick.Layouts ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Capslock: %1").arg(Hypr.capsLock ? "Enabled" : "Disabled") diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml index 5b32e4a6e..63b69b571 100644 --- a/modules/bar/popouts/Network.qml +++ b/modules/bar/popouts/Network.qml @@ -1,33 +1,33 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root - required property Item wrapper + required property PopoutState popouts property string connectingToSsid: "" property string view: "wireless" // "wireless" or "ethernet" property var passwordNetwork: null property bool showPasswordDialog: false - spacing: Appearance.spacing.small - width: Config.bar.sizes.networkWidth + spacing: Tokens.spacing.small + width: Tokens.sizes.bar.networkWidth // Wireless section StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.padding.normal : 0 - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: visible ? Tokens.padding.normal : 0 + Layout.rightMargin: Tokens.padding.small text: qsTr("Wireless") font.weight: 500 } @@ -43,11 +43,11 @@ ColumnLayout { StyledText { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.spacing.small : 0 - Layout.rightMargin: Appearance.padding.small - text: qsTr("%1 networks available").arg(Nmcli.networks.length) + Layout.topMargin: visible ? Tokens.spacing.small : 0 + Layout.rightMargin: Tokens.padding.small + text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Repeater { @@ -70,8 +70,8 @@ ColumnLayout { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.small opacity: 0 scale: 0.7 @@ -97,12 +97,12 @@ ColumnLayout { MaterialIcon { visible: networkItem.modelData.isSecure text: "lock" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 Layout.fillWidth: true text: networkItem.modelData.ssid elide: Text.ElideRight @@ -112,9 +112,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small + implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.small - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) CircularIndicator { @@ -126,7 +126,7 @@ ColumnLayout { color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface disabled: networkItem.loading || !Nmcli.wifiEnabled - function onClicked(): void { + onClicked: { if (networkItem.modelData.active) { Nmcli.disconnectFromNetwork(); } else { @@ -135,7 +135,7 @@ ColumnLayout { // Password is required - show password dialog root.passwordNetwork = network; root.showPasswordDialog = true; - root.wrapper.currentName = "wirelesspassword"; + root.popouts.currentName = "wirelesspassword"; }); // Clear connecting state if connection succeeds immediately (saved profile) @@ -165,27 +165,24 @@ ColumnLayout { StyledRect { visible: root.view === "wireless" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.spacing.small : 0 + Layout.topMargin: visible ? Tokens.spacing.small : 0 Layout.fillWidth: true - implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2 + implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer StateLayer { color: Colours.palette.m3onPrimaryContainer disabled: Nmcli.scanning || !Nmcli.wifiEnabled - - function onClicked(): void { - Nmcli.rescanWifi(); - } + onClicked: Nmcli.rescanWifi() } RowLayout { id: rescanBtn anchors.centerIn: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small opacity: Nmcli.scanning ? 0 : 1 MaterialIcon { @@ -210,9 +207,9 @@ ColumnLayout { CircularIndicator { anchors.centerIn: parent - strokeWidth: Appearance.padding.small / 2 + strokeWidth: Tokens.padding.small / 2 bgColour: "transparent" - implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2 + implicitSize: parent.implicitHeight - Tokens.padding.smaller * 2 running: Nmcli.scanning } } @@ -221,8 +218,8 @@ ColumnLayout { StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.padding.normal : 0 - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: visible ? Tokens.padding.normal : 0 + Layout.rightMargin: Tokens.padding.small text: qsTr("Ethernet") font.weight: 500 } @@ -230,11 +227,11 @@ ColumnLayout { StyledText { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.topMargin: visible ? Appearance.spacing.small : 0 - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: visible ? Tokens.spacing.small : 0 + Layout.rightMargin: Tokens.padding.small text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Repeater { @@ -256,8 +253,8 @@ ColumnLayout { visible: root.view === "ethernet" Layout.preferredHeight: visible ? implicitHeight : 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.small opacity: 0 scale: 0.7 @@ -281,8 +278,8 @@ ColumnLayout { } StyledText { - Layout.leftMargin: Appearance.spacing.small / 2 - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.leftMargin: Tokens.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 Layout.fillWidth: true text: ethernetItem.modelData.interface || qsTr("Unknown") elide: Text.ElideRight @@ -292,9 +289,9 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.small + implicitHeight: connectIcon.implicitHeight + Tokens.padding.small - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) CircularIndicator { @@ -306,7 +303,7 @@ ColumnLayout { color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface disabled: ethernetItem.loading - function onClicked(): void { + onClicked: { if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); } else { @@ -334,8 +331,6 @@ ColumnLayout { } Connections { - target: Nmcli - function onActiveChanged(): void { if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { root.connectingToSsid = ""; @@ -343,8 +338,8 @@ ColumnLayout { if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { root.showPasswordDialog = false; root.passwordNetwork = null; - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; + if (root.popouts.currentName === "wirelesspassword") { + root.popouts.currentName = "network"; } } } @@ -354,17 +349,20 @@ ColumnLayout { if (!Nmcli.scanning) scanIcon.rotation = 0; } + + target: Nmcli } Connections { - target: root.wrapper function onCurrentNameChanged(): void { // Clear password network when leaving password dialog - if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { + if (root.popouts.currentName !== "wirelesspassword" && root.showPasswordDialog) { root.showPasswordDialog = false; root.passwordNetwork = null; } } + + target: root.popouts } component Toggle: RowLayout { @@ -373,8 +371,8 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - spacing: Appearance.spacing.normal + Layout.rightMargin: Tokens.padding.small + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/bar/popouts/PopoutState.qml b/modules/bar/popouts/PopoutState.qml new file mode 100644 index 000000000..6be8169b4 --- /dev/null +++ b/modules/bar/popouts/PopoutState.qml @@ -0,0 +1,8 @@ +import QtQuick + +QtObject { + property string currentName + property bool hasCurrent + + signal detachRequested(mode: string) +} diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml index 9b743db19..7975d1bf4 100644 --- a/modules/bar/popouts/TrayMenu.qml +++ b/modules/bar/popouts/TrayMenu.qml @@ -1,21 +1,21 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Quickshell -import Quickshell.Widgets import QtQuick import QtQuick.Controls +import Quickshell +import Quickshell.Widgets +import Caelestia.Config +import qs.components +import qs.services StackView { id: root - required property Item popouts + required property PopoutState popouts required property QsMenuHandle trayItem - implicitWidth: currentItem.implicitWidth - implicitHeight: currentItem.implicitHeight + implicitWidth: currentItem?.implicitWidth ?? 0 + implicitHeight: currentItem?.implicitHeight ?? 0 initialItem: SubMenu { handle: root.trayItem @@ -26,6 +26,12 @@ StackView { popEnter: NoAnim {} popExit: NoAnim {} + Component { + id: subMenuComp + + SubMenu {} + } + component NoAnim: Transition { NumberAnimation { duration: 0 @@ -39,8 +45,8 @@ StackView { property bool isSubMenu property bool shown - padding: Appearance.padding.smaller - spacing: Appearance.spacing.small + padding: Tokens.padding.smaller + spacing: Tokens.spacing.small opacity: shown ? 1 : 0 scale: shown ? 1 : 0.8 @@ -72,15 +78,16 @@ StackView { required property QsMenuEntry modelData - implicitWidth: Config.bar.sizes.trayMenuWidth + implicitWidth: Tokens.sizes.bar.trayMenuWidth implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent" Loader { id: children + asynchronous: true anchors.left: parent.left anchors.right: parent.right @@ -90,14 +97,14 @@ StackView { implicitHeight: label.implicitHeight StateLayer { - anchors.margins: -Appearance.padding.small / 2 - anchors.leftMargin: -Appearance.padding.smaller - anchors.rightMargin: -Appearance.padding.smaller + anchors.margins: -Tokens.padding.small / 2 + anchors.leftMargin: -Tokens.padding.smaller + anchors.rightMargin: -Tokens.padding.smaller radius: item.radius disabled: !item.modelData.enabled - function onClicked(): void { + onClicked: { const entry = item.modelData; if (entry.hasChildren) root.push(subMenuComp.createObject(null, { @@ -114,11 +121,13 @@ StackView { Loader { id: icon + asynchronous: true anchors.left: parent.left active: item.modelData.icon !== "" sourceComponent: IconImage { + asynchronous: true implicitSize: label.implicitHeight source: item.modelData.icon @@ -129,7 +138,7 @@ StackView { id: label anchors.left: icon.right - anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0 + anchors.leftMargin: icon.active ? Tokens.spacing.smaller : 0 text: labelMetrics.elidedText color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline @@ -143,12 +152,13 @@ StackView { font.family: label.font.family elide: Text.ElideRight - elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0) + elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.normal : 0) } Loader { id: expand + asynchronous: true anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right @@ -165,11 +175,12 @@ StackView { } Loader { + asynchronous: true active: menu.isSubMenu sourceComponent: Item { implicitWidth: back.implicitWidth - implicitHeight: back.implicitHeight + Appearance.spacing.small / 2 + implicitHeight: back.implicitHeight + Tokens.spacing.small / 2 Item { anchors.bottom: parent.bottom @@ -178,20 +189,17 @@ StackView { StyledRect { anchors.fill: parent - anchors.margins: -Appearance.padding.small / 2 - anchors.leftMargin: -Appearance.padding.smaller - anchors.rightMargin: -Appearance.padding.smaller * 2 + anchors.margins: -Tokens.padding.small / 2 + anchors.leftMargin: -Tokens.padding.smaller + anchors.rightMargin: -Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3secondaryContainer StateLayer { radius: parent.radius color: Colours.palette.m3onSecondaryContainer - - function onClicked(): void { - root.pop(); - } + onClicked: root.pop() } } @@ -216,10 +224,4 @@ StackView { } } } - - Component { - id: subMenuComp - - SubMenu {} - } } diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml index 96639e711..f6a635fed 100644 --- a/modules/bar/popouts/WirelessPassword.qml +++ b/modules/bar/popouts/WirelessPassword.qml @@ -1,27 +1,100 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root - required property Item wrapper + required property PopoutState popouts property var network: null property bool isClosing: false - readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" + readonly property bool shouldBeVisible: root.popouts.currentName === "wirelesspassword" + + function checkConnectionStatus(): void { + if (!root.shouldBeVisible || !connectButton.connecting) { + return; + } + + // Check if we're connected to the target network (case-insensitive SSID comparison) + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + // Successfully connected - give it a moment for network list to update + // Use Timer for actual delay + connectionSuccessTimer.start(); + return; + } + + // Check for connection failures - if pending connection was cleared but we're not connected + if (Nmcli.pendingConnection === null && connectButton.connecting) { + // Wait a bit more before giving up (allow time for connection to establish) + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + // Delete the failed connection + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + + // Return to network popout + if (root.popouts.currentName === "wirelesspassword") { + root.popouts.currentName = "network"; + } + } + + spacing: Tokens.spacing.normal + implicitWidth: 400 + implicitHeight: content.implicitHeight + Tokens.padding.large * 2 + visible: shouldBeVisible || isClosing + enabled: shouldBeVisible && !isClosing + focus: enabled + + Component.onCompleted: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + onShouldBeVisibleChanged: { + if (shouldBeVisible) { + // Use Timer for actual delay to ensure dialog is fully rendered + focusTimer.start(); + } + } + + Keys.onEscapePressed: closeDialog() Connections { - target: root.wrapper function onCurrentNameChanged() { - if (root.wrapper.currentName === "wirelesspassword") { + if (root.popouts.currentName === "wirelesspassword") { // Update network when popout becomes active Qt.callLater(() => { // Try to get network from parent Content's networkPopout @@ -38,10 +111,13 @@ ColumnLayout { }); } } + + target: root.popouts } Timer { id: focusTimer + interval: 150 onTriggered: { root.forceActiveFocus(); @@ -49,41 +125,16 @@ ColumnLayout { } } - spacing: Appearance.spacing.normal - - implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - visible: shouldBeVisible || isClosing - enabled: shouldBeVisible && !isClosing - focus: enabled - - Component.onCompleted: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - onShouldBeVisibleChanged: { - if (shouldBeVisible) { - // Use Timer for actual delay to ensure dialog is fully rendered - focusTimer.start(); - } - } - - Keys.onEscapePressed: closeDialog() - StyledRect { Layout.fillWidth: true Layout.preferredWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 - - radius: Appearance.rounding.normal + implicitHeight: content.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer visible: root.shouldBeVisible || root.isClosing opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 + Keys.onEscapePressed: root.closeDialog() Behavior on opacity { Anim {} @@ -113,33 +164,32 @@ ColumnLayout { } } - Keys.onEscapePressed: root.closeDialog() - ColumnLayout { id: content anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } StyledText { id: networkNameText + Layout.alignment: Qt.AlignHCenter text: { if (root.network) { @@ -151,14 +201,15 @@ ColumnLayout { return qsTr("Network: Unknown"); } color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Timer { + property int attempts: 0 + interval: 50 running: root.shouldBeVisible && (!root.network || !root.network.ssid) repeat: true - property int attempts: 0 onTriggered: { attempts++; // Keep trying to get network from Network component @@ -186,7 +237,7 @@ ColumnLayout { id: statusText Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small + Layout.topMargin: Tokens.spacing.small visible: connectButton.connecting || connectButton.hasError text: { if (connectButton.hasError) { @@ -198,23 +249,30 @@ ColumnLayout { return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: 400 wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + Layout.maximumWidth: parent.width - Tokens.padding.large * 2 } FocusScope { id: passwordContainer + + property string passwordBuffer: "" + objectName: "passwordContainer" - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) - + implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) focus: true activeFocusOnTab: true - property string passwordBuffer: "" + Component.onCompleted: { + if (root.shouldBeVisible) { + // Use Timer for actual delay to ensure focus works correctly + passwordFocusTimer.start(); + } + } Keys.onPressed: event => { // Ensure we have focus when receiving keyboard input @@ -222,6 +280,11 @@ ColumnLayout { forceActiveFocus(); } + if (event.key === Qt.Key_Escape) { + event.accepted = false; + closeDialog(); + } + // Clear error when user starts typing if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; @@ -240,13 +303,16 @@ ColumnLayout { } event.accepted = true; } else if (event.text && event.text.length > 0) { + if (event.key === Qt.Key_Tab) { + event.accepted = false; + return; + } passwordBuffer += event.text; event.accepted = true; } } Connections { - target: root function onShouldBeVisibleChanged(): void { if (root.shouldBeVisible) { // Use Timer for actual delay to ensure focus works correctly @@ -255,26 +321,22 @@ ColumnLayout { connectButton.hasError = false; } } + + target: root } Timer { id: passwordFocusTimer + interval: 50 onTriggered: { passwordContainer.forceActiveFocus(); } } - Component.onCompleted: { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly - passwordFocusTimer.start(); - } - } - StyledRect { anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) border.color: { @@ -303,11 +365,8 @@ ColumnLayout { StateLayer { hoverEnabled: false cursorShape: Qt.IBeamCursor - radius: Appearance.rounding.normal - - function onClicked(): void { - passwordContainer.forceActiveFocus(); - } + radius: Tokens.rounding.normal + onClicked: passwordContainer.forceActiveFocus() } StyledText { @@ -316,8 +375,8 @@ ColumnLayout { anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { @@ -332,10 +391,10 @@ ColumnLayout { anchors.centerIn: parent implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal + implicitHeight: Tokens.font.size.normal orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false model: ScriptModel { @@ -349,7 +408,7 @@ ColumnLayout { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 + radius: Tokens.rounding.small / 2 opacity: 0 scale: 0 @@ -392,8 +451,7 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } @@ -405,15 +463,15 @@ ColumnLayout { } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { id: cancelButton Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -428,7 +486,7 @@ ColumnLayout { property bool hasError: false Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") @@ -491,45 +549,14 @@ ColumnLayout { } } - function checkConnectionStatus(): void { - if (!root.shouldBeVisible || !connectButton.connecting) { - return; - } - - // Check if we're connected to the target network (case-insensitive SSID comparison) - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - // Successfully connected - give it a moment for network list to update - // Use Timer for actual delay - connectionSuccessTimer.start(); - return; - } - - // Check for connection failures - if pending connection was cleared but we're not connected - if (Nmcli.pendingConnection === null && connectButton.connecting) { - // Wait a bit more before giving up (allow time for connection to establish) - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - // Delete the failed connection - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - Timer { id: connectionMonitor + + property int repeatCount: 0 + interval: 1000 repeat: true triggeredOnStart: false - property int repeatCount: 0 onTriggered: { repeatCount++; @@ -545,6 +572,7 @@ ColumnLayout { Timer { id: connectionSuccessTimer + interval: 500 onTriggered: { // Double-check connection is still active @@ -555,8 +583,8 @@ ColumnLayout { connectButton.connecting = false; connectButton.text = qsTr("Connect"); // Return to network popout on successful connection - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; + if (root.popouts.currentName === "wirelesspassword") { + root.popouts.currentName = "network"; } closeDialog(); } @@ -565,12 +593,12 @@ ColumnLayout { } Connections { - target: Nmcli function onActiveChanged() { if (root.shouldBeVisible) { root.checkConnectionStatus(); } } + function onConnectionFailed(ssid: string) { if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { connectionMonitor.stop(); @@ -583,23 +611,7 @@ ColumnLayout { Nmcli.forgetNetwork(ssid); } } - } - - function closeDialog(): void { - if (isClosing) { - return; - } - - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); - // Return to network popout - if (root.wrapper.currentName === "wirelesspassword") { - root.wrapper.currentName = "network"; - } + target: Nmcli } } diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml index 05a1d3c9e..7a66d3b97 100644 --- a/modules/bar/popouts/Wrapper.qml +++ b/modules/bar/popouts/Wrapper.qml @@ -1,57 +1,66 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Caelestia.Config import qs.components import qs.services -import qs.config -import qs.modules.windowinfo import qs.modules.controlcenter -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland -import QtQuick +import qs.modules.windowinfo Item { id: root required property ShellScreen screen + required property real offsetScale + + readonly property alias content: content + readonly property alias winfo: winfo + readonly property alias controlCenter: controlCenter - readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 + readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight - readonly property Item current: content.item?.current ?? null + readonly property Item current: (content.item as Content)?.current ?? null + readonly property bool isDetached: detachedMode.length > 0 - property string currentName + property alias currentName: popoutState.currentName + property alias hasCurrent: popoutState.hasCurrent property real currentCenter - property bool hasCurrent property string detachedMode property string queuedMode - readonly property bool isDetached: detachedMode.length > 0 - property int animLength: Appearance.anim.durations.normal - property list animCurve: Appearance.anim.curves.emphasized + // Dummy object so Tokens attached prop resolves to global config + // Anim configs are not per-monitor + readonly property QtObject dummy: QtObject {} + property int animLength: dummy.Tokens.anim.durations.expressiveDefaultSpatial + property var animCurve: dummy.Tokens.anim.expressiveDefaultSpatial // The easingCurve type is Qt 6.11+ so we gotta use var for now + + function setAnims(detach: bool): void { + const type = `expressive${detach ? "Slow" : "Default"}Spatial`; + animLength = dummy.Tokens.anim.durations[type]; + animCurve = dummy.Tokens.anim[type]; + } function detach(mode: string): void { - animLength = Appearance.anim.durations.large; + setAnims(true); if (mode === "winfo") { detachedMode = mode; } else { queuedMode = mode; detachedMode = "any"; } + setAnims(false); focus = true; } function close(): void { hasCurrent = false; - animCurve = Appearance.anim.curves.emphasizedAccel; - animLength = Appearance.anim.durations.normal; detachedMode = ""; - animCurve = Appearance.anim.curves.emphasized; } - visible: width > 0 && height > 0 - clip: true - implicitWidth: nonAnimWidth implicitHeight: nonAnimHeight @@ -59,7 +68,7 @@ Item { Keys.onEscapePressed: { // Forward escape to password popout if active, otherwise close if (currentName === "wirelesspassword" && content.item) { - const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); + const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); if (passwordPopout && passwordPopout.item) { passwordPopout.item.closeDialog(); return; @@ -75,6 +84,12 @@ Item { } } + PopoutState { + id: popoutState + + onDetachRequested: mode => root.detach(mode) + } + HyprlandFocusGrab { active: root.isDetached windows: [QsWindow.window] @@ -82,15 +97,7 @@ Item { } Binding { - when: root.isDetached - - target: QsWindow.window - property: "WlrLayershell.keyboardFocus" - value: WlrKeyboardFocus.OnDemand - } - - Binding { - when: root.hasCurrent && root.currentName === "wirelesspassword" + when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword") target: QsWindow.window property: "WlrLayershell.keyboardFocus" @@ -101,15 +108,16 @@ Item { id: content shouldBeActive: root.hasCurrent && !root.detachedMode - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter + anchors.fill: parent sourceComponent: Content { - wrapper: root + popouts: popoutState } } Comp { + id: winfo + shouldBeActive: root.detachedMode === "winfo" anchors.centerIn: parent @@ -120,48 +128,31 @@ Item { } Comp { + id: controlCenter + shouldBeActive: root.detachedMode === "any" anchors.centerIn: parent sourceComponent: ControlCenter { screen: root.screen active: root.queuedMode - - function close(): void { - root.close(); - } - } - } - - Behavior on x { - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve - } - } - - Behavior on y { - enabled: root.implicitWidth > 0 - - Anim { - duration: root.animLength - easing.bezierCurve: root.animCurve + onClose: root.close() } } Behavior on implicitWidth { Anim { duration: root.animLength - easing.bezierCurve: root.animCurve + easing: root.animCurve } } Behavior on implicitHeight { - enabled: root.implicitWidth > 0 + enabled: root.offsetScale < 1 Anim { duration: root.animLength - easing.bezierCurve: root.animCurve + easing: root.animCurve } } @@ -173,6 +164,7 @@ Item { active: false opacity: 0 + // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set states: State { name: "active" when: comp.shouldBeActive diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml index 94b6f7ec5..d8f9a462d 100644 --- a/modules/bar/popouts/kblayout/KbLayout.qml +++ b/modules/bar/popouts/kblayout/KbLayout.qml @@ -3,51 +3,47 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.services -import qs.config -import qs.utils - -import "." ColumnLayout { id: root - required property Item wrapper + function refresh() { + kb.refresh(); + } + + spacing: Tokens.spacing.small + width: Tokens.sizes.bar.kbLayoutWidth - spacing: Appearance.spacing.small - width: Config.bar.sizes.kbLayoutWidth + Component.onCompleted: kb.start() KbLayoutModel { id: kb } - function refresh() { - kb.refresh(); - } - Component.onCompleted: kb.start() - StyledText { - Layout.topMargin: Appearance.padding.normal - Layout.rightMargin: Appearance.padding.small + Layout.topMargin: Tokens.padding.normal + Layout.rightMargin: Tokens.padding.small text: qsTr("Keyboard Layouts") font.weight: 500 } ListView { id: list + model: kb.visibleModel Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - Layout.topMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.spacing.small clip: true interactive: true implicitHeight: Math.min(contentHeight, 320) visible: kb.visibleModel.count > 0 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small add: Transition { NumberAnimation { @@ -85,54 +81,55 @@ ColumnLayout { } delegate: Item { + id: kbDelegate + required property int layoutIndex required property string label + readonly property bool isDisabled: layoutIndex > 3 width: list.width - height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) - - readonly property bool isDisabled: layoutIndex > 3 + height: Math.max(36, rowText.implicitHeight + Tokens.padding.small * 2) + ToolTip.visible: isDisabled && layer.containsMouse + ToolTip.text: "XKB limitation: maximum 4 layouts allowed" StateLayer { id: layer + + onClicked: { + if (!kbDelegate.isDisabled) + kb.switchTo(kbDelegate.layoutIndex); + } + anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter implicitHeight: parent.height - 4 - - radius: Appearance.rounding.full - enabled: !isDisabled - - function onClicked(): void { - if (!isDisabled) - kb.switchTo(layoutIndex); - } + radius: Tokens.rounding.full + enabled: !kbDelegate.isDisabled } StyledText { id: rowText + anchors.verticalCenter: layer.verticalCenter anchors.left: layer.left anchors.right: layer.right - anchors.leftMargin: Appearance.padding.small - anchors.rightMargin: Appearance.padding.small - text: label + anchors.leftMargin: Tokens.padding.small + anchors.rightMargin: Tokens.padding.small + text: kbDelegate.label elide: Text.ElideRight - opacity: isDisabled ? 0.4 : 1.0 + opacity: kbDelegate.isDisabled ? 0.4 : 1.0 } - - ToolTip.visible: isDisabled && layer.containsMouse - ToolTip.text: "XKB limitation: maximum 4 layouts allowed" } } Rectangle { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - Layout.topMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.spacing.small - height: 1 + implicitHeight: 1 color: Colours.palette.m3onSurfaceVariant opacity: 0.35 } @@ -142,9 +139,9 @@ ColumnLayout { visible: kb.activeLabel.length > 0 Layout.fillWidth: true - Layout.rightMargin: Appearance.padding.small - Layout.topMargin: Appearance.spacing.small - spacing: Appearance.spacing.small + Layout.rightMargin: Tokens.padding.small + Layout.topMargin: Tokens.spacing.small + spacing: Tokens.spacing.small opacity: 1 scale: 1 @@ -163,16 +160,18 @@ ColumnLayout { } Connections { - target: kb function onActiveLabelChanged() { if (!activeRow.visible) return; popIn.restart(); } + + target: kb } SequentialAnimation { id: popIn + running: false ParallelAnimation { diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml index 437109530..f62d0b989 100644 --- a/modules/bar/popouts/kblayout/KbLayoutModel.qml +++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml @@ -1,24 +1,20 @@ pragma ComponentBehavior: Bound import QtQuick - -import Quickshell import Quickshell.Io - -import qs.config import Caelestia +import Caelestia.Config + +// TODO: handle this better later Item { id: model - visible: false - ListModel { - id: _visibleModel - } property alias visibleModel: _visibleModel - property string activeLabel: "" property int activeIndex: -1 + property var _xkbMap: ({}) + property bool _notifiedLimit: false function start() { _xkbXmlBase.running = true; @@ -35,31 +31,6 @@ Item { _switchProc.running = true; } - ListModel { - id: _layoutsModel - } - - property var _xkbMap: ({}) - property bool _notifiedLimit: false - - Process { - id: _xkbXmlBase - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] - stdout: StdioCollector { - onStreamFinished: _buildXmlMap(text) - } - onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) - _xkbXmlEvdev.running = true - } - - Process { - id: _xkbXmlEvdev - command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] - stdout: StdioCollector { - onStreamFinished: _buildXmlMap(text) - } - } - function _buildXmlMap(xml) { const map = {}; @@ -105,8 +76,84 @@ Item { return `${lang} (${code})`; } + function _setLayouts(raw) { + const parts = raw.split(",").map(s => s.trim()).filter(Boolean); + _layoutsModel.clear(); + + const seen = new Set(); + let idx = 0; + + for (const p of parts) { + if (seen.has(p)) + continue; + seen.add(p); + _layoutsModel.append({ + layoutIndex: idx, + token: p, + label: _pretty(p) + }); + idx++; + } + } + + function _rebuildVisible() { + _visibleModel.clear(); + + let arr = []; + for (let i = 0; i < _layoutsModel.count; i++) + arr.push(_layoutsModel.get(i)); + + arr = arr.filter(i => i.layoutIndex !== activeIndex); + arr.forEach(i => _visibleModel.append(i)); + + if (!GlobalConfig.utilities.toasts.kbLimit) + return; + + if (_layoutsModel.count > 4) { + Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); + } + } + + function _pretty(token) { + const code = token.replace(/\(.*\)$/, "").trim(); + if (_xkbMap[code]) + return code.toUpperCase() + " - " + _xkbMap[code]; + return code.toUpperCase() + " - " + code; + } + + visible: false + + ListModel { + id: _visibleModel + } + + ListModel { + id: _layoutsModel + } + + Process { + id: _xkbXmlBase + + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] + stdout: StdioCollector { + onStreamFinished: model._buildXmlMap(text) + } + onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property + _xkbXmlEvdev.running = true + } + + Process { + id: _xkbXmlEvdev + + command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] + stdout: StdioCollector { + onStreamFinished: model._buildXmlMap(text) + } + } + Process { id: _getKbLayoutOpt + command: ["hyprctl", "-j", "getoption", "input:kb_layout"] stdout: StdioCollector { onStreamFinished: { @@ -114,7 +161,7 @@ Item { const j = JSON.parse(text); const raw = (j?.str || j?.value || "").toString().trim(); if (raw.length) { - _setLayouts(raw); + model._setLayouts(raw); _fetchActiveLayouts.running = true; return; } @@ -126,6 +173,7 @@ Item { Process { id: _fetchLayoutsFromDevices + command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -134,7 +182,7 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const raw = (kb?.layout || "").trim(); if (raw.length) - _setLayouts(raw); + model._setLayouts(raw); } catch (e) {} _fetchActiveLayouts.running = true; } @@ -143,6 +191,7 @@ Item { Process { id: _fetchActiveLayouts + command: ["hyprctl", "-j", "devices"] stdout: StdioCollector { onStreamFinished: { @@ -151,66 +200,22 @@ Item { const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; const idx = kb?.active_layout_index ?? -1; - activeIndex = idx >= 0 ? idx : -1; - activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; + model.activeIndex = idx >= 0 ? idx : -1; + model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; } catch (e) { - activeIndex = -1; - activeLabel = ""; + model.activeIndex = -1; + model.activeLabel = ""; } - _rebuildVisible(); + model._rebuildVisible(); } } } Process { id: _switchProc + onRunningChanged: if (!running) _fetchActiveLayouts.running = true } - - function _setLayouts(raw) { - const parts = raw.split(",").map(s => s.trim()).filter(Boolean); - _layoutsModel.clear(); - - const seen = new Set(); - let idx = 0; - - for (const p of parts) { - if (seen.has(p)) - continue; - seen.add(p); - _layoutsModel.append({ - layoutIndex: idx, - token: p, - label: _pretty(p) - }); - idx++; - } - } - - function _rebuildVisible() { - _visibleModel.clear(); - - let arr = []; - for (let i = 0; i < _layoutsModel.count; i++) - arr.push(_layoutsModel.get(i)); - - arr = arr.filter(i => i.layoutIndex !== activeIndex); - arr.forEach(i => _visibleModel.append(i)); - - if (!Config.utilities.toasts.kbLimit) - return; - - if (_layoutsModel.count > 4) { - Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); - } - } - - function _pretty(token) { - const code = token.replace(/\(.*\)$/, "").trim(); - if (_xkbMap[code]) - return code.toUpperCase() + " - " + _xkbMap[code]; - return code.toUpperCase() + " - " + code; - } } diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml index 4aacfad99..c8b435caa 100644 --- a/modules/controlcenter/ControlCenter.qml +++ b/modules/controlcenter/ControlCenter.qml @@ -1,34 +1,34 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root required property ShellScreen screen - readonly property int rounding: floating ? 0 : Appearance.rounding.normal + readonly property int rounding: floating ? 0 : Tokens.rounding.large property alias floating: session.floating property alias active: session.active property alias navExpanded: session.navExpanded + readonly property bool initialOpeningComplete: panes.initialOpeningComplete readonly property Session session: Session { id: session root: root } - function close(): void { - } + signal close - implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio - implicitHeight: screen.height * Config.controlCenter.sizes.heightMult + implicitWidth: implicitHeight * Tokens.sizes.controlCenter.ratio + implicitHeight: screen.height * Tokens.sizes.controlCenter.heightMult GridLayout { anchors.fill: parent @@ -42,6 +42,7 @@ Item { Layout.fillWidth: true Layout.columnSpan: 2 + asynchronous: true active: root.floating visible: active @@ -60,8 +61,6 @@ Item { color: Colours.tPalette.m3surfaceContainer CustomMouseArea { - anchors.fill: parent - function onWheel(event: WheelEvent): void { // Prevent tab switching during initial opening animation to avoid blank pages if (!panes.initialOpeningComplete) { @@ -73,6 +72,8 @@ Item { else if (event.angleDelta.y > 0) root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); } + + anchors.fill: parent } NavRail { @@ -95,6 +96,4 @@ Item { session: root.session } } - - readonly property bool initialOpeningComplete: panes.initialOpeningComplete } diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml index e61a741a3..a126c8009 100644 --- a/modules/controlcenter/NavRail.qml +++ b/modules/controlcenter/NavRail.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components import qs.services -import qs.config import qs.modules.controlcenter -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root @@ -15,23 +15,23 @@ Item { required property Session session required property bool initialOpeningComplete - implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.larger * 4 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 ColumnLayout { id: layout anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.larger * 2 - spacing: Appearance.spacing.normal + anchors.leftMargin: Tokens.padding.larger * 2 + spacing: Tokens.spacing.normal states: State { name: "expanded" when: root.session.navExpanded PropertyChanges { - layout.spacing: Appearance.spacing.small + layout.spacing: root.Tokens.spacing.small } } @@ -42,7 +42,8 @@ Item { } Loader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large + asynchronous: true active: !root.session.floating visible: active @@ -50,23 +51,23 @@ Item { readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 implicitWidth: nonAnimWidth - implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth + implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Tokens.padding.normal * 2 : nonAnimWidth color: Colours.palette.m3primaryContainer - radius: Appearance.rounding.small + radius: Tokens.rounding.small StateLayer { id: normalWinState - color: Colours.palette.m3onPrimaryContainer - - function onClicked(): void { + onClicked: { root.session.root.close(); WindowFactory.create(null, { active: root.session.active, navExpanded: root.session.navExpanded }); } + + color: Colours.palette.m3onPrimaryContainer } MaterialIcon { @@ -74,11 +75,11 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large + anchors.leftMargin: Tokens.padding.large text: "select_window" color: Colours.palette.m3onPrimaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: 1 } @@ -87,7 +88,7 @@ Item { anchors.left: normalWinIcon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal text: qsTr("Float window") color: Colours.palette.m3onPrimaryContainer @@ -95,22 +96,20 @@ Item { Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } Behavior on implicitWidth { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } @@ -121,7 +120,8 @@ Item { NavItem { required property int index - Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 + + Layout.topMargin: index === 0 ? Tokens.spacing.large * 2 : 0 icon: PaneRegistry.getByIndex(index).icon label: PaneRegistry.getByIndex(index).label } @@ -146,7 +146,7 @@ Item { expandedLabel.opacity: 1 smallLabel.opacity: 0 background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth - background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + background.implicitHeight: icon.implicitHeight + root.Tokens.padding.normal * 2 item.implicitHeight: background.implicitHeight } } @@ -154,35 +154,34 @@ Item { transitions: Transition { Anim { property: "opacity" - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } Anim { properties: "implicitWidth,implicitHeight" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } StyledRect { id: background - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 - implicitHeight: icon.implicitHeight + Appearance.padding.small + implicitHeight: icon.implicitHeight + Tokens.padding.small StateLayer { - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - - function onClicked(): void { + onClicked: { // Prevent tab switching during initial opening animation to avoid blank pages if (!root.initialOpeningComplete) { return; } root.session.active = item.label; } + + color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface } MaterialIcon { @@ -190,11 +189,11 @@ Item { anchors.left: parent.left anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.padding.large + anchors.leftMargin: Tokens.padding.large text: item.icon color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: item.active ? 1 : 0 Behavior on fill { @@ -207,7 +206,7 @@ Item { anchors.left: icon.right anchors.verticalCenter: parent.verticalCenter - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal opacity: 0 text: item.label @@ -220,10 +219,10 @@ Item { anchors.horizontalCenter: icon.horizontalCenter anchors.top: icon.bottom - anchors.topMargin: Appearance.spacing.small / 2 + anchors.topMargin: Tokens.spacing.small / 2 text: item.label - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.capitalization: Font.Capitalize } } diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml index b3e19c81f..f26a2aade 100644 --- a/modules/controlcenter/PaneRegistry.qml +++ b/modules/controlcenter/PaneRegistry.qml @@ -36,6 +36,12 @@ QtObject { readonly property string icon: "task_alt" readonly property string component: "taskbar/TaskbarPane.qml" }, + QtObject { + readonly property string id: "notifications" + readonly property string label: "notifications" + readonly property string icon: "notifications" + readonly property string component: "notifications/NotificationsPane.qml" + }, QtObject { readonly property string id: "launcher" readonly property string label: "launcher" @@ -47,6 +53,12 @@ QtObject { readonly property string label: "monitors" readonly property string icon: "monitor" readonly property string component: "monitors/MonitorsPane.qml" + }, + QtObject { + readonly property string id: "dashboard" + readonly property string label: "dashboard" + readonly property string icon: "dashboard" + readonly property string component: "dashboard/DashboardPane.qml" } ] @@ -60,7 +72,7 @@ QtObject { return result; } - function getByIndex(index: int): QtObject { + function getByIndex(index: int): var { if (index >= 0 && index < panes.length) { return panes[index]; } @@ -76,12 +88,12 @@ QtObject { return -1; } - function getByLabel(label: string): QtObject { + function getByLabel(label: string): var { const index = getIndexByLabel(label); return getByIndex(index); } - function getById(id: string): QtObject { + function getById(id: string): var { for (let i = 0; i < panes.length; i++) { if (panes[i].id === id) { return panes[i]; diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 90a369a95..e8803011f 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -5,15 +5,17 @@ import "network" import "audio" import "appearance" import "taskbar" +import "notifications" import "launcher" import "monitors" +import "dashboard" +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.services -import qs.config import qs.modules.controlcenter -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts ClippingRectangle { id: root @@ -37,26 +39,27 @@ ClippingRectangle { } Connections { - target: root.session - function onActiveIndexChanged(): void { root.focus = true; } + + target: root.session } ColumnLayout { id: layout + property bool animationComplete: true + property bool initialOpeningComplete: false + spacing: 0 y: -root.session.activeIndex * root.height clip: true - property bool animationComplete: true - property bool initialOpeningComplete: false - Timer { id: animationDelayTimer - interval: Appearance.anim.durations.normal + + interval: Tokens.anim.durations.normal onTriggered: { layout.animationComplete = true; } @@ -64,7 +67,8 @@ ClippingRectangle { Timer { id: initialOpeningTimer - interval: Appearance.anim.durations.large + + interval: Tokens.anim.durations.large running: true onTriggered: { layout.initialOpeningComplete = true; @@ -76,6 +80,7 @@ ClippingRectangle { Pane { required property int index + paneIndex: index componentPath: PaneRegistry.getByIndex(index).component } @@ -86,11 +91,12 @@ ClippingRectangle { } Connections { - target: root.session function onActiveIndexChanged(): void { layout.animationComplete = false; animationDelayTimer.restart(); } + + target: root.session } } @@ -99,10 +105,6 @@ ClippingRectangle { required property int paneIndex required property string componentPath - - implicitWidth: root.width - implicitHeight: root.height - property bool hasBeenLoaded: false function updateActive(): void { @@ -125,10 +127,14 @@ ClippingRectangle { loader.active = shouldBeActive; } + implicitWidth: root.width + implicitHeight: root.height + Loader { id: loader anchors.fill: parent + asynchronous: true clip: false active: false @@ -156,20 +162,22 @@ ClippingRectangle { } Connections { - target: root.session function onActiveIndexChanged(): void { pane.updateActive(); } + + target: root.session } Connections { - target: layout function onInitialOpeningCompleteChanged(): void { pane.updateActive(); } function onAnimationCompleteChanged(): void { pane.updateActive(); } + + target: layout } } } diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml index 3678093b5..33805609d 100644 --- a/modules/controlcenter/Session.qml +++ b/modules/controlcenter/Session.qml @@ -1,5 +1,5 @@ -import QtQuick import "./state" +import QtQuick import qs.modules.controlcenter QtObject { diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml index abcf5df19..dc0dc4a07 100644 --- a/modules/controlcenter/WindowFactory.qml +++ b/modules/controlcenter/WindowFactory.qml @@ -1,9 +1,9 @@ pragma Singleton +import QtQuick +import Quickshell import qs.components import qs.services -import Quickshell -import QtQuick Singleton { id: root @@ -47,11 +47,8 @@ Singleton { anchors.fill: parent screen: win.screen + onClose: win.destroy() floating: true - - function close(): void { - win.destroy(); - } } Behavior on color { diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml index fb7160893..8f66968ba 100644 --- a/modules/controlcenter/WindowTitle.qml +++ b/modules/controlcenter/WindowTitle.qml @@ -1,8 +1,8 @@ +import QtQuick +import Quickshell +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell -import QtQuick StyledRect { id: root @@ -10,7 +10,7 @@ StyledRect { required property ShellScreen screen required property Session session - implicitHeight: text.implicitHeight + Appearance.padding.normal + implicitHeight: text.implicitHeight + Tokens.padding.normal color: Colours.tPalette.m3surfaceContainer StyledText { @@ -21,24 +21,24 @@ StyledRect { text: qsTr("Caelestia Settings - %1").arg(root.session.active) font.capitalization: Font.Capitalize - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } Item { anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal implicitWidth: implicitHeight - implicitHeight: closeIcon.implicitHeight + Appearance.padding.small + implicitHeight: closeIcon.implicitHeight + Tokens.padding.small StateLayer { - radius: Appearance.rounding.full - - function onClicked(): void { + onClicked: { QsWindow.window.destroy(); } + + radius: Tokens.rounding.full } MaterialIcon { diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml index 42511677a..a1e0e42c6 100644 --- a/modules/controlcenter/appearance/AppearancePane.qml +++ b/modules/controlcenter/appearance/AppearancePane.qml @@ -4,26 +4,26 @@ import ".." import "../components" import "./sections" import "../../launcher/services" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config +import Caelestia.Models import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.components.images import qs.services -import qs.config import qs.utils -import Caelestia.Models -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root required property Session session - property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 + property real animDurationsScale: GlobalConfig.appearance.anim.durations.scale ?? 1 property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" @@ -31,9 +31,9 @@ Item { property real paddingScale: Config.appearance.padding.scale ?? 1 property real roundingScale: Config.appearance.rounding.scale ?? 1 property real spacingScale: Config.appearance.spacing.scale ?? 1 - property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false - property real transparencyBase: Config.appearance.transparency.base ?? 0.85 - property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4 + property bool transparencyEnabled: GlobalConfig.appearance.transparency.enabled ?? false + property real transparencyBase: GlobalConfig.appearance.transparency.base ?? 0.85 + property real transparencyLayers: GlobalConfig.appearance.transparency.layers ?? 0.4 property real borderRounding: Config.border.rounding ?? 1 property real borderThickness: Config.border.thickness ?? 1 @@ -48,52 +48,53 @@ Item { property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false property bool backgroundEnabled: Config.background.enabled ?? true + property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true property bool visualiserEnabled: Config.background.visualiser.enabled ?? false property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true property real visualiserRounding: Config.background.visualiser.rounding ?? 1 property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 - anchors.fill: parent - function saveConfig() { - Config.appearance.anim.durations.scale = root.animDurationsScale; - - Config.appearance.font.family.material = root.fontFamilyMaterial; - Config.appearance.font.family.mono = root.fontFamilyMono; - Config.appearance.font.family.sans = root.fontFamilySans; - Config.appearance.font.size.scale = root.fontSizeScale; - - Config.appearance.padding.scale = root.paddingScale; - Config.appearance.rounding.scale = root.roundingScale; - Config.appearance.spacing.scale = root.spacingScale; - - Config.appearance.transparency.enabled = root.transparencyEnabled; - Config.appearance.transparency.base = root.transparencyBase; - Config.appearance.transparency.layers = root.transparencyLayers; - - Config.background.desktopClock.enabled = root.desktopClockEnabled; - Config.background.enabled = root.backgroundEnabled; - Config.background.desktopClock.scale = root.desktopClockScale; - Config.background.desktopClock.position = root.desktopClockPosition; - Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; - Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; - Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; - Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; - Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; - Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; - Config.background.desktopClock.invertColors = root.desktopClockInvertColors; - - Config.background.visualiser.enabled = root.visualiserEnabled; - Config.background.visualiser.autoHide = root.visualiserAutoHide; - Config.background.visualiser.rounding = root.visualiserRounding; - Config.background.visualiser.spacing = root.visualiserSpacing; - - Config.border.rounding = root.borderRounding; - Config.border.thickness = root.borderThickness; - - Config.save(); + GlobalConfig.appearance.anim.durations.scale = root.animDurationsScale; + + GlobalConfig.appearance.font.family.material = root.fontFamilyMaterial; + GlobalConfig.appearance.font.family.mono = root.fontFamilyMono; + GlobalConfig.appearance.font.family.sans = root.fontFamilySans; + GlobalConfig.appearance.font.size.scale = root.fontSizeScale; + + GlobalConfig.appearance.padding.scale = root.paddingScale; + GlobalConfig.appearance.rounding.scale = root.roundingScale; + GlobalConfig.appearance.spacing.scale = root.spacingScale; + + GlobalConfig.appearance.transparency.enabled = root.transparencyEnabled; + GlobalConfig.appearance.transparency.base = root.transparencyBase; + GlobalConfig.appearance.transparency.layers = root.transparencyLayers; + + GlobalConfig.background.desktopClock.enabled = root.desktopClockEnabled; + GlobalConfig.background.enabled = root.backgroundEnabled; + GlobalConfig.background.desktopClock.scale = root.desktopClockScale; + GlobalConfig.background.desktopClock.position = root.desktopClockPosition; + GlobalConfig.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; + GlobalConfig.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; + GlobalConfig.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; + GlobalConfig.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; + GlobalConfig.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; + GlobalConfig.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; + GlobalConfig.background.desktopClock.invertColors = root.desktopClockInvertColors; + + GlobalConfig.background.wallpaperEnabled = root.wallpaperEnabled; + + GlobalConfig.background.visualiser.enabled = root.visualiserEnabled; + GlobalConfig.background.visualiser.autoHide = root.visualiserAutoHide; + GlobalConfig.background.visualiser.rounding = root.visualiserRounding; + GlobalConfig.background.visualiser.spacing = root.visualiserSpacing; + + GlobalConfig.border.rounding = root.borderRounding; + GlobalConfig.border.thickness = root.borderThickness; } + anchors.fill: parent + Component { id: appearanceRightContentComponent @@ -108,9 +109,9 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter - Layout.bottomMargin: Appearance.spacing.normal + Layout.bottomMargin: Tokens.spacing.normal text: qsTr("Wallpaper") - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 600 } @@ -119,8 +120,9 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - Layout.bottomMargin: -Appearance.padding.large * 2 + Layout.bottomMargin: -Tokens.padding.large * 2 + asynchronous: true active: { const isActive = root.session.activeIndex === 3; const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; @@ -132,7 +134,7 @@ Item { onStatusChanged: { if (status === Loader.Error) { - console.error("[AppearancePane] Wallpaper loader error!"); + console.error(lc, "Wallpaper loader error!"); } } @@ -148,10 +150,11 @@ Item { anchors.fill: parent leftContent: Component { - StyledFlickable { id: sidebarFlickable + readonly property var rootPane: root + flickableDirection: Flickable.VerticalFlick contentHeight: sidebarLayout.height @@ -161,20 +164,20 @@ Item { ColumnLayout { id: sidebarLayout - anchors.left: parent.left - anchors.right: parent.right - spacing: Appearance.spacing.small readonly property var rootPane: sidebarFlickable.rootPane - readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.small + RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Appearance") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -215,31 +218,37 @@ Item { AnimationsSection { id: animationsSection + rootPane: sidebarFlickable.rootPane } FontsSection { id: fontsSection + rootPane: sidebarFlickable.rootPane } ScalesSection { id: scalesSection + rootPane: sidebarFlickable.rootPane } TransparencySection { id: transparencySection + rootPane: sidebarFlickable.rootPane } BorderSection { id: borderSection + rootPane: sidebarFlickable.rootPane } BackgroundSection { id: backgroundSection + rootPane: sidebarFlickable.rootPane } } @@ -248,4 +257,11 @@ Item { rightContent: appearanceRightContentComponent } + + LoggingCategory { + id: lc + + name: "caelestia.qml.controlcenter.appearance" + defaultLogLevel: LoggingCategory.Info + } } diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml index 0cba5cecd..412441abc 100644 --- a/modules/controlcenter/appearance/sections/AnimationsSection.qml +++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml index 2f75c9e0e..3f31f10b4 100644 --- a/modules/controlcenter/appearance/sections/BackgroundSection.qml +++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root @@ -27,10 +27,19 @@ CollapsibleSection { } } + SwitchRow { + label: qsTr("Wallpaper enabled") + checked: rootPane.wallpaperEnabled + onToggled: checked => { + rootPane.wallpaperEnabled = checked; + rootPane.saveConfig(); + } + } + StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Desktop Clock") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -46,9 +55,6 @@ CollapsibleSection { SectionContainer { id: posContainer - contentSpacing: Appearance.spacing.small - z: 1 - readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') readonly property string currentV: pos[0] readonly property string currentH: pos[1] @@ -58,9 +64,12 @@ CollapsibleSection { rootPane.saveConfig(); } + contentSpacing: Tokens.spacing.small + z: 1 + StyledText { text: qsTr("Positioning") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -70,19 +79,22 @@ CollapsibleSection { menuItems: [ MenuItem { + property string val: "top" + text: qsTr("Top") icon: "vertical_align_top" - property string val: "top" }, MenuItem { + property string val: "middle" + text: qsTr("Middle") icon: "vertical_align_center" - property string val: "middle" }, MenuItem { + property string val: "bottom" + text: qsTr("Bottom") icon: "vertical_align_bottom" - property string val: "bottom" } ] @@ -104,19 +116,22 @@ CollapsibleSection { menuItems: [ MenuItem { + property string val: "left" + text: qsTr("Left") icon: "align_horizontal_left" - property string val: "left" }, MenuItem { + property string val: "center" + text: qsTr("Center") icon: "align_horizontal_center" - property string val: "center" }, MenuItem { + property string val: "right" + text: qsTr("Right") icon: "align_horizontal_right" - property string val: "right" } ] @@ -141,11 +156,11 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small StyledText { text: qsTr("Shadow") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -159,7 +174,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -184,7 +199,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -210,11 +225,11 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small StyledText { text: qsTr("Background") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -237,7 +252,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -263,9 +278,9 @@ CollapsibleSection { } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Visualiser") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -288,7 +303,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -313,7 +328,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml index 9532d70d6..7dbd2dbe6 100644 --- a/modules/controlcenter/appearance/sections/BorderSection.qml +++ b/modules/controlcenter/appearance/sections/BorderSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -43,14 +43,14 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true label: qsTr("Border thickness") value: rootPane.borderThickness - from: 0.1 + from: 0 to: 100 decimals: 1 suffix: "px" diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml index 95cb4b725..0a65cbdc0 100644 --- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml +++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts CollapsibleSection { title: qsTr("Color scheme") @@ -18,7 +18,7 @@ CollapsibleSection { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 Repeater { model: Schemes.list @@ -32,12 +32,13 @@ CollapsibleSection { readonly property bool isCurrent: schemeKey === Schemes.currentScheme color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: schemeRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { const name = modelData.name; const flavour = modelData.flavour; const schemeKey = `${name} ${flavour}`; @@ -53,6 +54,7 @@ CollapsibleSection { Timer { id: reloadTimer + interval: 300 onTriggered: { Schemes.reload(); @@ -63,9 +65,9 @@ CollapsibleSection { id: schemeRow anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { id: preview @@ -76,15 +78,16 @@ CollapsibleSection { border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) color: `#${modelData.colours?.surface}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitWidth: iconPlaceholder.implicitWidth implicitHeight: iconPlaceholder.implicitWidth MaterialIcon { id: iconPlaceholder + visible: false text: "circle" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } Item { @@ -102,7 +105,7 @@ CollapsibleSection { implicitWidth: preview.implicitWidth color: `#${modelData.colours?.primary}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full } } } @@ -113,12 +116,12 @@ CollapsibleSection { StyledText { text: modelData.flavour ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: modelData.name ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -128,17 +131,16 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } - - implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml index 3aa17dd9c..c612485d0 100644 --- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml +++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../../../launcher/services" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts CollapsibleSection { title: qsTr("Color variant") @@ -18,7 +18,7 @@ CollapsibleSection { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 Repeater { model: M3Variants.list @@ -29,12 +29,13 @@ CollapsibleSection { Layout.fillWidth: true color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: variantRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { const variant = modelData.variant; Schemes.currentVariant = variant; @@ -48,6 +49,7 @@ CollapsibleSection { Timer { id: reloadTimer + interval: 300 onTriggered: { Schemes.reload(); @@ -60,13 +62,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: modelData.icon - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.variant === Schemes.currentVariant ? 1 : 0 } @@ -80,11 +82,9 @@ CollapsibleSection { visible: modelData.variant === Schemes.currentVariant text: "check" color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } - - implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 } } } diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml index 3988863af..10040c587 100644 --- a/modules/controlcenter/appearance/sections/FontsSection.qml +++ b/modules/controlcenter/appearance/sections/FontsSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root @@ -19,62 +19,64 @@ CollapsibleSection { showBackground: true CollapsibleSection { - id: materialFontSection - title: qsTr("Material font family") + id: sansFontSection + + title: qsTr("Sans-serif font family") expanded: true showBackground: true nested: true Loader { - id: materialFontLoader Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - active: materialFontSection.expanded + asynchronous: true + active: sansFontSection.expanded sourceComponent: StyledListView { - id: materialFontList - property alias contentHeight: materialFontList.contentHeight + id: sansFontList + + property alias contentHeight: sansFontList.contentHeight clip: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { - flickable: materialFontList + flickable: sansFontList } delegate: StyledRect { required property string modelData required property int index + readonly property bool isCurrent: modelData === rootPane.fontFamilySans width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { - rootPane.fontFamilyMaterial = modelData; + onClicked: { + rootPane.fontFamilySans = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilyMaterialRow + id: fontFamilySansRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: modelData - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -82,17 +84,16 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } - - implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 } } } @@ -100,6 +101,7 @@ CollapsibleSection { CollapsibleSection { id: monoFontSection + title: qsTr("Monospace font family") expanded: false showBackground: true @@ -108,14 +110,16 @@ CollapsibleSection { Loader { Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 + asynchronous: true active: monoFontSection.expanded sourceComponent: StyledListView { id: monoFontList + property alias contentHeight: monoFontList.contentHeight clip: true - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 model: Qt.fontFamilies() StyledScrollBar.vertical: StyledScrollBar { @@ -125,17 +129,17 @@ CollapsibleSection { delegate: StyledRect { required property string modelData required property int index + readonly property bool isCurrent: modelData === rootPane.fontFamilyMono width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilyMono color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { rootPane.fontFamilyMono = modelData; rootPane.saveConfig(); } @@ -147,13 +151,13 @@ CollapsibleSection { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: modelData - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -161,78 +165,82 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } - - implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 } } } } CollapsibleSection { - id: sansFontSection - title: qsTr("Sans-serif font family") + id: materialFontSection + + title: qsTr("Material font family") expanded: false showBackground: true nested: true Loader { + id: materialFontLoader + Layout.fillWidth: true Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 - active: sansFontSection.expanded + asynchronous: true + active: materialFontSection.expanded sourceComponent: StyledListView { - id: sansFontList - property alias contentHeight: sansFontList.contentHeight + id: materialFontList + + property alias contentHeight: materialFontList.contentHeight clip: true - spacing: Appearance.spacing.small / 2 - model: Qt.fontFamilies() + spacing: Tokens.spacing.small / 2 + model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) StyledScrollBar.vertical: StyledScrollBar { - flickable: sansFontList + flickable: materialFontList } delegate: StyledRect { required property string modelData required property int index + readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial width: ListView.view.width - - readonly property bool isCurrent: modelData === rootPane.fontFamilySans color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal border.width: isCurrent ? 1 : 0 border.color: Colours.palette.m3primary + implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { - rootPane.fontFamilySans = modelData; + onClicked: { + rootPane.fontFamilyMaterial = modelData; rootPane.saveConfig(); } } RowLayout { - id: fontFamilySansRow + id: fontFamilyMaterialRow anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: modelData - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -240,24 +248,23 @@ CollapsibleSection { } Loader { + asynchronous: true active: isCurrent sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } - - implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 } } } } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml index b0e6e38b8..dac6226e8 100644 --- a/modules/controlcenter/appearance/sections/ScalesSection.qml +++ b/modules/controlcenter/appearance/sections/ScalesSection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root @@ -19,7 +19,7 @@ CollapsibleSection { showBackground: true SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -43,7 +43,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -67,7 +67,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml index 04eed9113..aab53b8bd 100644 --- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml +++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick CollapsibleSection { title: qsTr("Theme mode") diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml index 9a48629c1..f2f15ba13 100644 --- a/modules/controlcenter/appearance/sections/TransparencySection.qml +++ b/modules/controlcenter/appearance/sections/TransparencySection.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts CollapsibleSection { id: root @@ -28,7 +28,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true @@ -53,7 +53,7 @@ CollapsibleSection { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal SliderInput { Layout.fillWidth: true diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml index 01d90be70..f3edb7319 100644 --- a/modules/controlcenter/audio/AudioPane.qml +++ b/modules/controlcenter/audio/AudioPane.qml @@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Widgets +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root @@ -23,9 +23,9 @@ Item { anchors.fill: parent leftContent: Component { - StyledFlickable { id: leftAudioFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: leftContent.height @@ -38,15 +38,15 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Audio") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -64,15 +64,15 @@ Item { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Devices (%1)").arg(Audio.sinks.length) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } } @@ -93,10 +93,11 @@ Item { Layout.fillWidth: true color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal + implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { Audio.setAudioSink(modelData); } } @@ -107,13 +108,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: Audio.sink?.id === modelData.id ? 1 : 0 } @@ -126,8 +127,6 @@ Item { font.weight: Audio.sink?.id === modelData.id ? 500 : 400 } } - - implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -142,15 +141,15 @@ Item { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Devices (%1)").arg(Audio.sources.length) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } } @@ -171,10 +170,11 @@ Item { Layout.fillWidth: true color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal + implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.normal * 2 StateLayer { - function onClicked(): void { + onClicked: { Audio.setAudioSource(modelData); } } @@ -185,13 +185,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "mic" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: Audio.source?.id === modelData.id ? 1 : 0 } @@ -204,8 +204,6 @@ Item { font.weight: Audio.source?.id === modelData.id ? 500 : 400 } } - - implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -217,6 +215,7 @@ Item { rightContent: Component { StyledFlickable { id: rightAudioFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: contentLayout.height @@ -230,7 +229,7 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "volume_up" @@ -243,19 +242,19 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } @@ -265,6 +264,7 @@ Item { StyledInputField { id: outputVolumeInput + Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -276,15 +276,6 @@ Item { text = Math.round(Audio.volume * 100).toString(); } - Connections { - target: Audio - function onVolumeChanged() { - if (!outputVolumeInput.hasFocus) { - outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); - } - } - } - onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -300,24 +291,34 @@ Item { text = Math.round(Audio.volume * 100).toString(); } } + + Connections { + function onVolumeChanged() { + if (!outputVolumeInput.hasFocus) { + outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); + } + } + + target: Audio + } } StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: Audio.muted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: muteIcon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { - function onClicked(): void { + onClicked: { if (Audio.sink?.audio) { Audio.sink.audio.muted = !Audio.sink.audio.muted; } @@ -336,8 +337,9 @@ Item { StyledSlider { id: outputVolumeSlider + Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 value: Audio.volume enabled: !Audio.muted @@ -358,19 +360,19 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Volume") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } @@ -380,6 +382,7 @@ Item { StyledInputField { id: inputVolumeInput + Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -391,15 +394,6 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } - Connections { - target: Audio - function onSourceVolumeChanged() { - if (!inputVolumeInput.hasFocus) { - inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); - } - } - } - onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -415,24 +409,34 @@ Item { text = Math.round(Audio.sourceVolume * 100).toString(); } } + + Connections { + function onSourceVolumeChanged() { + if (!inputVolumeInput.hasFocus) { + inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); + } + } + + target: Audio + } } StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: Audio.sourceMuted ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: muteInputIcon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { - function onClicked(): void { + onClicked: { if (Audio.source?.audio) { Audio.source.audio.muted = !Audio.source.audio.muted; } @@ -451,8 +455,9 @@ Item { StyledSlider { id: inputVolumeSlider + Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 value: Audio.sourceVolume enabled: !Audio.sourceMuted @@ -473,11 +478,11 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: Audio.streams @@ -488,15 +493,15 @@ Item { required property int index Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "apps" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal fill: 0 } @@ -505,12 +510,13 @@ Item { elide: Text.ElideRight maximumLineCount: 1 text: Audio.getStreamName(modelData) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } StyledInputField { id: streamVolumeInput + Layout.preferredWidth: 70 validator: IntValidator { bottom: 0 @@ -522,15 +528,6 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } - Connections { - target: modelData - function onAudioChanged() { - if (!streamVolumeInput.hasFocus && modelData?.audio) { - streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); - } - } - } - onTextEdited: text => { if (hasFocus) { const val = parseInt(text); @@ -546,24 +543,34 @@ Item { text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); } } + + Connections { + function onAudioChanged() { + if (!streamVolumeInput.hasFocus && modelData?.audio) { + streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); + } + } + + target: modelData + } } StyledText { text: "%" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 } StyledRect { implicitWidth: implicitHeight - implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: streamMuteIcon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer StateLayer { - function onClicked(): void { + onClicked: { Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); } } @@ -580,7 +587,7 @@ Item { StyledSlider { Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 value: Audio.getStreamVolume(modelData) enabled: !Audio.getStreamMuted(modelData) @@ -593,12 +600,13 @@ Item { } Connections { - target: modelData function onAudioChanged() { if (modelData?.audio) { value = modelData.audio.volume; } } + + target: modelData } } } @@ -609,7 +617,7 @@ Item { visible: Audio.streams.length === 0 text: qsTr("No applications currently playing audio") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small horizontalAlignment: Text.AlignHCenter } } diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml index 7d3b9ca33..b2b9c0f69 100644 --- a/modules/controlcenter/bluetooth/BtPane.qml +++ b/modules/controlcenter/bluetooth/BtPane.qml @@ -3,13 +3,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import Quickshell.Bluetooth +import Quickshell.Widgets +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers -import qs.config -import Quickshell.Widgets -import Quickshell.Bluetooth -import QtQuick +import qs.components.controls SplitPaneWithDetails { id: root @@ -53,6 +53,7 @@ SplitPaneWithDetails { rightSettingsComponent: Component { StyledFlickable { id: settingsFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml index 529904546..415752265 100644 --- a/modules/controlcenter/bluetooth/Details.qml +++ b/modules/controlcenter/bluetooth/Details.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config import qs.utils -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts StyledFlickable { id: root @@ -54,12 +54,12 @@ StyledFlickable { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Connection status") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -70,9 +70,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 + implicitHeight: deviceStatus.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -81,9 +81,9 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger Toggle { label: qsTr("Connected") @@ -113,12 +113,12 @@ StyledFlickable { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Device properties") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -129,9 +129,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 + implicitHeight: deviceProps.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -140,19 +140,19 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Item { id: renameDevice Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.spacing.small implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight @@ -167,15 +167,13 @@ StyledFlickable { PropertyChanges { renameDevice.implicitHeight: deviceNameEdit.implicitHeight renameLabel.opacity: 0 - deviceNameEdit.padding: Appearance.padding.normal + deviceNameEdit.padding: root.Tokens.padding.normal } } transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + AnchorAnim { + type: AnchorAnim.Standard } Anim { properties: "implicitHeight,opacity,padding" @@ -189,7 +187,7 @@ StyledFlickable { text: qsTr("Device name") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledTextField { @@ -198,7 +196,7 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal + anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.normal text: root.device?.name ?? "" readOnly: !root.session.bt.editingDeviceName @@ -207,11 +205,11 @@ StyledFlickable { root.device.name = text; } - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal + leftPadding: Tokens.padding.normal + rightPadding: Tokens.padding.normal background: StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingDeviceName ? 1 : 0 @@ -233,21 +231,21 @@ StyledFlickable { StyledRect { implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingDeviceName ? 1 : 0 scale: root.session.bt.editingDeviceName ? 1 : 0.5 StateLayer { - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingDeviceName - - function onClicked(): void { + onClicked: { root.session.bt.editingDeviceName = false; deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); } + + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingDeviceName } MaterialIcon { @@ -265,29 +263,28 @@ StyledFlickable { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } StyledRect { implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: root.session.bt.editingDeviceName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) StateLayer { - color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - function onClicked(): void { + onClicked: { root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; if (root.session.bt.editingDeviceName) deviceNameEdit.forceActiveFocus(); else deviceNameEdit.accepted(); } + + color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { @@ -322,12 +319,12 @@ StyledFlickable { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Device information") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -338,9 +335,9 @@ StyledFlickable { StyledRect { Layout.fillWidth: true - implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 + implicitHeight: deviceInfo.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -349,88 +346,83 @@ StyledFlickable { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") } RowLayout { - Layout.topMargin: Appearance.spacing.small / 2 - Layout.fillWidth: true - Layout.preferredHeight: Appearance.padding.smaller - spacing: Appearance.spacing.small / 2 + id: batteryPercent - StyledRect { - Layout.fillHeight: true - implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 - radius: Appearance.rounding.full - color: Colours.palette.m3primary - } + Layout.topMargin: Tokens.spacing.small / 2 + Layout.fillWidth: true + Layout.preferredHeight: Tokens.padding.smaller + spacing: Tokens.spacing.small / 2 StyledRect { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3secondaryContainer StyledRect { - anchors.right: parent.right + anchors.left: parent.left anchors.top: parent.top anchors.bottom: parent.bottom anchors.margins: parent.height * 0.25 - implicitWidth: height - radius: Appearance.rounding.full + implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 + radius: Tokens.rounding.full color: Colours.palette.m3primary } } } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Dbus path") } StyledText { text: root.device?.dbusPath ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("MAC address") } StyledText { text: root.device?.address ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Bonded") } StyledText { text: root.device?.bonded ? qsTr("Yes") : qsTr("No") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("System name") } StyledText { text: root.device?.deviceName ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } } @@ -443,7 +435,7 @@ StyledFlickable { ColumnLayout { anchors.right: fabRoot.right anchors.bottom: fabRoot.top - anchors.bottomMargin: Appearance.padding.normal + anchors.bottomMargin: Tokens.padding.normal Repeater { id: fabMenu @@ -475,9 +467,9 @@ StyledFlickable { Layout.alignment: Qt.AlignRight - implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2 + implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.larger * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primaryContainer opacity: 0 @@ -487,7 +479,7 @@ StyledFlickable { when: root.session.bt.fabMenuOpen PropertyChanges { - fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2 + fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.large * 2 fabMenuItem.opacity: 1 fabMenuItemInner.opacity: 1 } @@ -499,17 +491,16 @@ StyledFlickable { SequentialAnimation { PauseAnimation { - duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8 + duration: (fabMenu.count - 1 - fabMenuItem.index) * Tokens.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "opacity" - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } @@ -519,17 +510,16 @@ StyledFlickable { SequentialAnimation { PauseAnimation { - duration: fabMenuItem.index * Appearance.anim.durations.small / 8 + duration: fabMenuItem.index * Tokens.anim.durations.small / 8 } ParallelAnimation { Anim { property: "implicitWidth" - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "opacity" - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } @@ -537,7 +527,7 @@ StyledFlickable { ] StateLayer { - function onClicked(): void { + onClicked: { root.session.bt.fabMenuOpen = false; const name = fabMenuItem.modelData.name; @@ -554,7 +544,7 @@ StyledFlickable { id: fabMenuItemInner anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal opacity: 0 MaterialIcon { @@ -572,7 +562,7 @@ StyledFlickable { Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } @@ -599,7 +589,7 @@ StyledFlickable { implicitWidth: 64 implicitHeight: 64 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer states: State { @@ -610,15 +600,14 @@ StyledFlickable { fabBg.implicitWidth: 48 fabBg.implicitHeight: 48 fabBg.radius: 48 / 2 - fab.font.pointSize: Appearance.font.size.larger + fab.font.pointSize: Tokens.font.size.larger } } transitions: Transition { Anim { properties: "implicitWidth,implicitHeight" - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } Anim { properties: "radius,font.pointSize" @@ -635,11 +624,11 @@ StyledFlickable { StateLayer { id: fabState - color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - - function onClicked(): void { + onClicked: { root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; } + + color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer } MaterialIcon { @@ -649,7 +638,7 @@ StyledFlickable { animate: true text: root.session.bt.fabMenuOpen ? "close" : "settings" color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: 1 } } @@ -661,7 +650,7 @@ StyledFlickable { property alias toggle: toggle Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml index 2a2bde934..419410eef 100644 --- a/modules/controlcenter/bluetooth/DeviceList.qml +++ b/modules/controlcenter/bluetooth/DeviceList.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Bluetooth +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config import qs.utils -import Quickshell -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts DeviceList { id: root @@ -32,11 +32,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Bluetooth") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -48,9 +48,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.enabled ?? false icon: "power" accent: "Tertiary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Toggle Bluetooth") onClicked: { @@ -64,9 +64,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.discoverable ?? false icon: root.smallDiscoverable ? "group_search" : "" label: root.smallDiscoverable ? "" : qsTr("Discoverable") - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Make discoverable") onClicked: { @@ -80,9 +80,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.pairable ?? false icon: "missing_controller" label: root.smallPairable ? "" : qsTr("Pairable") - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Make pairable") onClicked: { @@ -96,9 +96,9 @@ DeviceList { toggled: Bluetooth.defaultAdapter?.discovering ?? false icon: "bluetooth_searching" accent: "Secondary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Scan for devices") onClicked: { @@ -112,9 +112,9 @@ DeviceList { toggled: !root.session.bt.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Bluetooth settings") onClicked: { @@ -137,15 +137,15 @@ DeviceList { readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected width: ListView.view ? ListView.view.width : undefined - implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: deviceInner.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { id: stateLayer - function onClicked(): void { + onClicked: { if (device.modelData) root.session.bt.active = device.modelData; } @@ -155,15 +155,15 @@ DeviceList { id: deviceInner anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { @@ -178,7 +178,7 @@ DeviceList { anchors.centerIn: parent text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: device.connected ? 1 : 0 Behavior on fill { @@ -202,7 +202,7 @@ DeviceList { Layout.fillWidth: true text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } } @@ -211,9 +211,9 @@ DeviceList { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) CircularIndicator { @@ -222,10 +222,7 @@ DeviceList { } StateLayer { - color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - disabled: device.loading - - function onClicked(): void { + onClicked: { if (device.loading) return; @@ -239,6 +236,9 @@ DeviceList { } } } + + color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface + disabled: device.loading } MaterialIcon { diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml index c5472406b..1202cc5bf 100644 --- a/modules/controlcenter/bluetooth/Settings.qml +++ b/modules/controlcenter/bluetooth/Settings.qml @@ -2,21 +2,21 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "bluetooth" @@ -24,9 +24,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Adapter status") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -37,9 +37,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2 + implicitHeight: adapterStatus.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -48,9 +48,9 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger Toggle { label: qsTr("Powered") @@ -85,9 +85,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Adapter properties") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -98,9 +98,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2 + implicitHeight: adapterSettings.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -109,13 +109,13 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.larger + spacing: Tokens.spacing.larger RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -127,28 +127,28 @@ ColumnLayout { property bool expanded - implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 + implicitWidth: adapterPicker.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: adapterPicker.implicitHeight + Tokens.padding.smaller * 2 StateLayer { - radius: Appearance.rounding.small - - function onClicked(): void { + onClicked: { adapterPickerButton.expanded = !adapterPickerButton.expanded; } + + radius: Tokens.rounding.small } RowLayout { id: adapterPicker anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.topMargin: Appearance.padding.smaller - anchors.bottomMargin: Appearance.padding.smaller - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.normal + anchors.topMargin: Tokens.padding.smaller + anchors.bottomMargin: Tokens.padding.smaller + spacing: Tokens.spacing.normal StyledText { - Layout.leftMargin: Appearance.padding.small + Layout.leftMargin: Tokens.padding.small text: Bluetooth.defaultAdapter?.name ?? qsTr("None") } @@ -170,8 +170,7 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } @@ -185,7 +184,7 @@ ColumnLayout { implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight color: Colours.palette.m3secondaryContainer - radius: Appearance.rounding.small + radius: Tokens.rounding.small opacity: adapterPickerButton.expanded ? 1 : 0 scale: adapterPickerButton.expanded ? 1 : 0.7 @@ -207,15 +206,15 @@ ColumnLayout { required property BluetoothAdapter modelData Layout.fillWidth: true - implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: adapterInner.implicitHeight + Tokens.padding.normal * 2 StateLayer { - disabled: !adapterPickerButton.expanded - - function onClicked(): void { + onClicked: { adapterPickerButton.expanded = false; root.session.bt.currentAdapter = adapter.modelData; } + + disabled: !adapterPickerButton.expanded } RowLayout { @@ -224,12 +223,12 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.small + Layout.leftMargin: Tokens.padding.small text: adapter.modelData.name color: Colours.palette.m3onSecondaryContainer } @@ -250,15 +249,13 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } @@ -267,7 +264,7 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -287,13 +284,13 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Item { id: renameAdapter Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small + Layout.rightMargin: Tokens.spacing.small implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight @@ -308,15 +305,13 @@ ColumnLayout { PropertyChanges { renameAdapter.implicitHeight: adapterNameEdit.implicitHeight renameLabel.opacity: 0 - adapterNameEdit.padding: Appearance.padding.normal + adapterNameEdit.padding: Tokens.padding.normal } } transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + AnchorAnim { + type: AnchorAnim.Standard } Anim { properties: "implicitHeight,opacity,padding" @@ -330,7 +325,7 @@ ColumnLayout { text: qsTr("Rename adapter (currently does not work)") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledTextField { @@ -339,7 +334,7 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.top: renameLabel.bottom - anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal + anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Tokens.padding.normal text: root.session.bt.currentAdapter?.name ?? "" readOnly: !root.session.bt.editingAdapterName @@ -347,11 +342,11 @@ ColumnLayout { root.session.bt.editingAdapterName = false; } - leftPadding: Appearance.padding.normal - rightPadding: Appearance.padding.normal + leftPadding: Tokens.padding.normal + rightPadding: Tokens.padding.normal background: StyledRect { - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 2 border.color: Colours.palette.m3primary opacity: root.session.bt.editingAdapterName ? 1 : 0 @@ -373,21 +368,21 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.palette.m3secondaryContainer opacity: root.session.bt.editingAdapterName ? 1 : 0 scale: root.session.bt.editingAdapterName ? 1 : 0.5 StateLayer { - color: Colours.palette.m3onSecondaryContainer - disabled: !root.session.bt.editingAdapterName - - function onClicked(): void { + onClicked: { root.session.bt.editingAdapterName = false; adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); } + + color: Colours.palette.m3onSecondaryContainer + disabled: !root.session.bt.editingAdapterName } MaterialIcon { @@ -405,29 +400,28 @@ ColumnLayout { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } StyledRect { implicitWidth: implicitHeight - implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) + radius: root.session.bt.editingAdapterName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) StateLayer { - color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - function onClicked(): void { + onClicked: { root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; if (root.session.bt.editingAdapterName) adapterNameEdit.forceActiveFocus(); else adapterNameEdit.accepted(); } + + color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface } MaterialIcon { @@ -448,9 +442,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Adapter information") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -461,9 +455,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2 + implicitHeight: adapterInfo.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -472,9 +466,9 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: qsTr("Adapter state") @@ -483,29 +477,29 @@ ColumnLayout { StyledText { text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Dbus path") } StyledText { text: Bluetooth.defaultAdapter?.dbusPath ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Adapter id") } StyledText { text: Bluetooth.defaultAdapter?.adapterId ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } } @@ -516,7 +510,7 @@ ColumnLayout { property alias toggle: toggle Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true diff --git a/modules/controlcenter/components/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml new file mode 100644 index 000000000..f782838d6 --- /dev/null +++ b/modules/controlcenter/components/ConnectedButtonGroup.qml @@ -0,0 +1,114 @@ +import ".." +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services + +StyledRect { + id: root + + property var options: [] // Array of {label: string, propertyName: string, onToggled: function, state: bool?} + property var rootItem: null // The root item that contains the properties we want to bind to + property string title: "" // Optional title text + property int rows: 1 // Number of rows + + Layout.fillWidth: true + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + clip: true + + Behavior on implicitHeight { + Anim {} + } + + ColumnLayout { + id: layout + + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal + + StyledText { + visible: root.title !== "" + text: root.title + font.pointSize: Tokens.font.size.normal + } + + GridLayout { + id: buttonGrid + + Layout.alignment: Qt.AlignHCenter + rowSpacing: Tokens.spacing.small + columnSpacing: Tokens.spacing.small + rows: root.rows + columns: Math.ceil(root.options.length / root.rows) + + Repeater { + id: repeater + + model: root.options + + delegate: TextButton { + id: button + + required property int index + required property var modelData + + property bool _checked: false + + Layout.fillWidth: true + text: modelData.label + checked: _checked + toggle: false + type: TextButton.Tonal + + // Create binding in Component.onCompleted + Component.onCompleted: { + if (modelData.state !== undefined && modelData.state) { + _checked = modelData.state; + } else if (root.rootItem && modelData.propertyName) { + const propName = modelData.propertyName; + const rootItem = root.rootItem; + _checked = Qt.binding(function () { + return rootItem[propName] ?? false; + }); + } + } + + // Match utilities Toggles radius styling + // Each button has full rounding (not connected) since they have spacing + radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal + + // Match utilities Toggles inactive color + inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) + + // Adjust width similar to utilities toggles + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) + + onClicked: { + if (modelData.onToggled && root.rootItem && modelData.propertyName) { + const currentValue = root.rootItem[modelData.propertyName] ?? false; + modelData.onToggled(!currentValue); + } + } + + Behavior on Layout.preferredWidth { + Anim { + type: Anim.FastSpatial + } + } + + Behavior on radius { + Anim { + type: Anim.FastSpatial + } + } + } + } + } + } +} diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml index a5d06471c..075024f6c 100644 --- a/modules/controlcenter/components/DeviceDetails.qml +++ b/modules/controlcenter/components/DeviceDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers -import qs.config -import QtQuick -import QtQuick.Layouts Item { id: root @@ -30,12 +30,13 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Loader { id: headerLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null } @@ -44,6 +45,7 @@ Item { id: topContentLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.topContent visible: root.topContent !== null } @@ -55,6 +57,7 @@ Item { required property Component modelData Layout.fillWidth: true + asynchronous: true sourceComponent: modelData } } @@ -63,6 +66,7 @@ Item { id: bottomContentLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.bottomContent visible: root.bottomContent !== null } diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml index 722f9a16e..f02cc3c08 100644 --- a/modules/controlcenter/components/DeviceList.qml +++ b/modules/controlcenter/components/DeviceList.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root @@ -23,15 +23,17 @@ ColumnLayout { property Component headerComponent: null property Component titleSuffix: null property bool showHeader: true + property alias view: view signal itemSelected(var item) - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Loader { id: headerLoader Layout.fillWidth: true + asynchronous: true sourceComponent: root.headerComponent visible: root.headerComponent !== null && root.showHeader } @@ -39,17 +41,18 @@ ColumnLayout { RowLayout { Layout.fillWidth: true Layout.topMargin: root.headerComponent ? 0 : 0 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small visible: root.title !== "" || root.description !== "" StyledText { visible: root.title !== "" text: root.title - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } Loader { + asynchronous: true sourceComponent: root.titleSuffix visible: root.titleSuffix !== null } @@ -59,8 +62,6 @@ ColumnLayout { } } - property alias view: view - StyledText { visible: root.description !== "" Layout.fillWidth: true @@ -77,7 +78,7 @@ ColumnLayout { model: root.model delegate: root.delegate - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false clip: false } diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml index 5d80dbec2..489d3c784 100644 --- a/modules/controlcenter/components/PaneTransition.qml +++ b/modules/controlcenter/components/PaneTransition.qml @@ -1,7 +1,7 @@ pragma ComponentBehavior: Bound -import qs.config import QtQuick +import Caelestia.Config SequentialAnimation { id: root @@ -20,9 +20,8 @@ SequentialAnimation { property: "opacity" from: root.opacityFrom to: root.opacityTo - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardAccel } NumberAnimation { @@ -30,9 +29,8 @@ SequentialAnimation { property: "scale" from: root.scaleFrom to: root.scaleTo - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardAccel } } @@ -53,9 +51,8 @@ SequentialAnimation { property: "opacity" from: root.opacityTo to: root.opacityFrom - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardDecel } NumberAnimation { @@ -63,9 +60,8 @@ SequentialAnimation { property: "scale" from: root.scaleTo to: root.scaleFrom - duration: Appearance.anim.durations.normal / 2 - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.normal / 2 + easing: Tokens.anim.standardDecel } } } diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml new file mode 100644 index 000000000..8cd493f40 --- /dev/null +++ b/modules/controlcenter/components/ReadonlySlider.qml @@ -0,0 +1,67 @@ +import ".." +import "../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +ColumnLayout { + id: root + + property string label: "" + property real value: 0 + property real from: 0 + property real to: 100 + property string suffix: "" + property bool readonly: false + + spacing: Tokens.spacing.small + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + + StyledText { + visible: root.label !== "" + text: root.label + font.pointSize: Tokens.font.size.normal + color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface + } + + Item { + Layout.fillWidth: true + } + + MaterialIcon { + visible: root.readonly + text: "lock" + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.small + } + + StyledText { + text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") + font.pointSize: Tokens.font.size.normal + color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: Tokens.padding.normal + radius: Tokens.rounding.full + color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) + opacity: root.readonly ? 0.5 : 1.0 + + StyledRect { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * ((root.value - root.from) / (root.to - root.from)) + radius: parent.radius + color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary + } + } +} diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml index 0dc190c05..c6ccbb67a 100644 --- a/modules/controlcenter/components/SettingsHeader.qml +++ b/modules/controlcenter/components/SettingsHeader.qml @@ -1,9 +1,9 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components Item { id: root @@ -18,19 +18,19 @@ Item { id: column anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: root.icon - font.pointSize: Appearance.font.size.extraLarge * 3 + font.pointSize: Tokens.font.size.extraLarge * 3 font.bold: true } StyledText { Layout.alignment: Qt.AlignHCenter text: root.title - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.bold: true } } diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml index 11b3f70dd..73fd00ae6 100644 --- a/modules/controlcenter/components/SliderInput.qml +++ b/modules/controlcenter/components/SliderInput.qml @@ -1,12 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root @@ -21,6 +21,9 @@ ColumnLayout { property int decimals: 1 // Number of decimal places to show (default: 1) property var formatValueFunction: null // Optional custom format function property var parseValueFunction: null // Optional custom parse function + property bool _initialized: false + + signal valueModified(real newValue) function formatValue(val: real): string { if (formatValueFunction) { @@ -49,11 +52,7 @@ ColumnLayout { return parseFloat(text); } - signal valueModified(real newValue) - - property bool _initialized: false - - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Component.onCompleted: { // Set initialized flag after a brief delay to allow component to fully load @@ -62,14 +61,22 @@ ColumnLayout { }); } + // Update input field when value changes externally (slider is already bound) + onValueChanged: { + // Only update if component is initialized to avoid issues during creation + if (root._initialized && !inputField.hasFocus) { + inputField.text = root.formatValue(root.value); + } + } + RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { visible: root.label !== "" text: root.label - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Item { @@ -78,6 +85,7 @@ ColumnLayout { StyledInputField { id: inputField + Layout.preferredWidth: 70 validator: root.validator @@ -130,7 +138,7 @@ ColumnLayout { visible: root.suffix !== "" text: root.suffix color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } @@ -138,20 +146,12 @@ ColumnLayout { id: slider Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 from: root.from to: root.to stepSize: root.stepSize - // Use Binding to allow slider to move freely during dragging - Binding { - target: slider - property: "value" - value: root.value - when: !slider.pressed - } - onValueChanged: { // Update input field text in real-time as slider moves during dragging // Always update when slider value changes (during dragging or external updates) @@ -168,13 +168,13 @@ ColumnLayout { inputField.text = root.formatValue(newValue); } } - } - // Update input field when value changes externally (slider is already bound) - onValueChanged: { - // Only update if component is initialized to avoid issues during creation - if (root._initialized && !inputField.hasFocus) { - inputField.text = root.formatValue(root.value); + // Use Binding to allow slider to move freely during dragging + Binding { + target: slider + property: "value" + value: root.value + when: !slider.pressed } } } diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml index 89504a0b8..64c7c9fc8 100644 --- a/modules/controlcenter/components/SplitPaneLayout.qml +++ b/modules/controlcenter/components/SplitPaneLayout.qml @@ -1,28 +1,26 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.components.effects -import qs.config -import Quickshell.Widgets import QtQuick import QtQuick.Layouts +import Quickshell.Widgets +import Caelestia.Config +import qs.components +import qs.components.effects RowLayout { id: root - spacing: 0 - property Component leftContent: null property Component rightContent: null - property real leftWidthRatio: 0.4 property int leftMinimumWidth: 420 property var leftLoaderProperties: ({}) property var rightLoaderProperties: ({}) - property alias leftLoader: leftLoader property alias rightLoader: rightLoader + spacing: 0 + Item { id: leftPane @@ -34,9 +32,9 @@ RowLayout { id: leftClippingRect anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 + anchors.rightMargin: Tokens.padding.normal / 2 radius: leftBorder.innerRadius color: "transparent" @@ -45,10 +43,11 @@ RowLayout { id: leftLoader anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 + anchors.margins: Tokens.padding.large + Tokens.padding.normal + anchors.leftMargin: Tokens.padding.large + anchors.rightMargin: Tokens.padding.large + Tokens.padding.normal / 2 + asynchronous: true sourceComponent: root.leftContent Component.onCompleted: { @@ -63,7 +62,7 @@ RowLayout { id: leftBorder leftThickness: 0 - rightThickness: Appearance.padding.normal / 2 + rightThickness: Tokens.padding.normal / 2 } } @@ -77,9 +76,9 @@ RowLayout { id: rightClippingRect anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal / 2 + anchors.rightMargin: Tokens.padding.normal / 2 radius: rightBorder.innerRadius color: "transparent" @@ -88,8 +87,9 @@ RowLayout { id: rightLoader anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Tokens.padding.large * 2 + asynchronous: true sourceComponent: root.rightContent Component.onCompleted: { @@ -103,7 +103,7 @@ RowLayout { InnerBorder { id: rightBorder - leftThickness: Appearance.padding.normal / 2 + leftThickness: Tokens.padding.normal / 2 } } } diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml index ce8c9d07d..ad1c444d4 100644 --- a/modules/controlcenter/components/SplitPaneWithDetails.qml +++ b/modules/controlcenter/components/SplitPaneWithDetails.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound import ".." -import qs.components -import qs.components.effects -import qs.components.containers -import qs.config -import Quickshell.Widgets import QtQuick import QtQuick.Layouts +import Quickshell.Widgets +import Caelestia.Config +import qs.components +import qs.components.containers +import qs.components.effects Item { id: root @@ -48,11 +48,17 @@ Item { nextComponent = targetComponent; } + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = root.paneIdGenerator(pane); + } + Loader { id: rightLoader anchors.fill: parent + asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -73,11 +79,6 @@ Item { ] } } - - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = root.paneIdGenerator(pane); - } } } } @@ -86,6 +87,7 @@ Item { id: overlayLoader anchors.fill: parent + asynchronous: true z: 1000 sourceComponent: root.overlayComponent active: root.overlayComponent !== null diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml index ed6bb40a8..44a615c6b 100644 --- a/modules/controlcenter/components/WallpaperGrid.qml +++ b/modules/controlcenter/components/WallpaperGrid.qml @@ -1,25 +1,25 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import Caelestia.Config +import Caelestia.Models import qs.components import qs.components.controls import qs.components.effects import qs.components.images import qs.services -import qs.config -import Caelestia.Models -import QtQuick GridView { id: root required property Session session - readonly property int minCellWidth: 200 + Appearance.spacing.normal + readonly property int minCellWidth: 200 + Tokens.spacing.normal readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) cellWidth: width / columnsCount - cellHeight: 140 + Appearance.spacing.normal + cellHeight: 140 + Tokens.spacing.normal model: Wallpapers.list @@ -32,25 +32,24 @@ GridView { delegate: Item { required property var modelData required property int index + readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent + readonly property real itemMargin: Tokens.spacing.normal / 2 + readonly property real itemRadius: Tokens.rounding.normal width: root.cellWidth height: root.cellHeight - readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent - readonly property real itemMargin: Appearance.spacing.normal / 2 - readonly property real itemRadius: Appearance.rounding.normal - StateLayer { + onClicked: { + Wallpapers.setWallpaper(modelData.path); + } + anchors.fill: parent anchors.leftMargin: itemMargin anchors.rightMargin: itemMargin anchors.topMargin: itemMargin anchors.bottomMargin: itemMargin radius: itemRadius - - function onClicked(): void { - Wallpapers.setWallpaper(modelData.path); - } } StyledClippingRect { @@ -117,6 +116,7 @@ GridView { id: fallbackTimer property bool triggered: false + interval: 800 running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null onTriggered: triggered = true @@ -130,7 +130,7 @@ GridView { anchors.right: parent.right anchors.bottom: parent.bottom - implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 + implicitHeight: filenameText.implicitHeight + Tokens.padding.normal * 1.5 radius: 0 gradient: Gradient { @@ -154,16 +154,16 @@ GridView { opacity: 0 + Component.onCompleted: { + opacity = 1; + } + Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } - - Component.onCompleted: { - opacity = 1; - } } } @@ -190,26 +190,27 @@ GridView { MaterialIcon { anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small visible: isCurrent text: "check_circle" color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } StyledText { id: filenameText + anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 - anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 - anchors.bottomMargin: Appearance.padding.normal + anchors.leftMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 + anchors.rightMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 + anchors.bottomMargin: Tokens.padding.normal text: modelData.name - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller font.weight: 500 color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface elide: Text.ElideMiddle @@ -218,16 +219,16 @@ GridView { opacity: 0 + Component.onCompleted: { + opacity = 1; + } + Behavior on opacity { NumberAnimation { duration: 1000 easing.type: Easing.OutCubic } } - - Component.onCompleted: { - opacity = 1; - } } } } diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml new file mode 100644 index 000000000..e9b6e568f --- /dev/null +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -0,0 +1,527 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.UPower +import Quickshell.Widgets +import Caelestia.Config +import qs.components +import qs.components.containers +import qs.components.controls +import qs.components.effects +import qs.services +import qs.utils + +Item { + id: root + + required property Session session + property string activeSection: "general" + + property bool enabled: Config.dashboard.enabled ?? true + property bool showOnHover: Config.dashboard.showOnHover ?? true + property int mediaUpdateInterval: GlobalConfig.dashboard.mediaUpdateInterval ?? 1000 + property int resourceUpdateInterval: GlobalConfig.dashboard.resourceUpdateInterval ?? 1000 + property int dragThreshold: Config.dashboard.dragThreshold ?? 50 + + property bool showDashboard: Config.dashboard.showDashboard ?? true + property bool showMedia: Config.dashboard.showMedia ?? true + property bool showPerformance: Config.dashboard.showPerformance ?? true + property bool showWeather: Config.dashboard.showWeather ?? true + + property bool showBattery: Config.dashboard.performance.showBattery ?? false + property bool showGpu: Config.dashboard.performance.showGpu ?? true + property bool showCpu: Config.dashboard.performance.showCpu ?? true + property bool showMemory: Config.dashboard.performance.showMemory ?? true + property bool showStorage: Config.dashboard.performance.showStorage ?? true + property bool showNetwork: Config.dashboard.performance.showNetwork ?? true + + readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" + readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery + readonly property var sections: [ + { + id: "general", + title: qsTr("General"), + description: qsTr("Visibility and timing"), + icon: "dashboard" + }, + { + id: "tabs", + title: qsTr("Tabs"), + description: qsTr("Dashboard pages"), + icon: "tab" + }, + { + id: "performance", + title: qsTr("Performance"), + description: qsTr("Resource cards"), + icon: "monitoring" + } + ] + + function componentForSection(sectionId) { + switch (sectionId) { + case "tabs": + return tabsComponent; + case "performance": + return performanceComponent; + case "general": + default: + return generalComponent; + } + } + + function performanceOptions() { + const options = []; + + if (root.batteryAvailable) { + options.push({ + label: qsTr("Battery"), + propertyName: "showBattery", + onToggled: function (checked) { + root.showBattery = checked; + root.saveConfig(); + } + }); + } + + if (root.gpuAvailable) { + options.push({ + label: qsTr("GPU"), + propertyName: "showGpu", + onToggled: function (checked) { + root.showGpu = checked; + root.saveConfig(); + } + }); + } + + options.push({ + label: qsTr("CPU"), + propertyName: "showCpu", + onToggled: function (checked) { + root.showCpu = checked; + root.saveConfig(); + } + }, { + label: qsTr("Memory"), + propertyName: "showMemory", + onToggled: function (checked) { + root.showMemory = checked; + root.saveConfig(); + } + }, { + label: qsTr("Storage"), + propertyName: "showStorage", + onToggled: function (checked) { + root.showStorage = checked; + root.saveConfig(); + } + }, { + label: qsTr("Network"), + propertyName: "showNetwork", + onToggled: function (checked) { + root.showNetwork = checked; + root.saveConfig(); + } + }); + + return options; + } + + function saveConfig() { + GlobalConfig.dashboard.enabled = root.enabled; + GlobalConfig.dashboard.showOnHover = root.showOnHover; + GlobalConfig.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; + GlobalConfig.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; + GlobalConfig.dashboard.dragThreshold = root.dragThreshold; + GlobalConfig.dashboard.showDashboard = root.showDashboard; + GlobalConfig.dashboard.showMedia = root.showMedia; + GlobalConfig.dashboard.showPerformance = root.showPerformance; + GlobalConfig.dashboard.showWeather = root.showWeather; + GlobalConfig.dashboard.performance.showBattery = root.showBattery; + GlobalConfig.dashboard.performance.showGpu = root.showGpu; + GlobalConfig.dashboard.performance.showCpu = root.showCpu; + GlobalConfig.dashboard.performance.showMemory = root.showMemory; + GlobalConfig.dashboard.performance.showStorage = root.showStorage; + GlobalConfig.dashboard.performance.showNetwork = root.showNetwork; + } + + anchors.fill: parent + + SplitPaneLayout { + anchors.fill: parent + leftWidthRatio: 0.32 + leftMinimumWidth: 300 + + leftContent: Component { + StyledFlickable { + id: leftFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } + + ColumnLayout { + id: leftContentLayout + + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Dashboard") + font.pointSize: Tokens.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + Repeater { + model: root.sections + + delegate: SectionNavButton { + required property var modelData + + Layout.fillWidth: true + section: modelData + active: root.activeSection === modelData.id + onClicked: root.activeSection = modelData.id + } + } + } + } + } + + rightContent: Component { + Item { + id: rightPaneItem + + property string paneId: root.activeSection + property Component targetComponent: root.componentForSection(root.activeSection) + property Component nextComponent: root.componentForSection(root.activeSection) + + onPaneIdChanged: { + nextComponent = root.componentForSection(root.activeSection); + } + + Loader { + id: rightLoader + + anchors.fill: parent + asynchronous: true + opacity: 1 + scale: 1 + transformOrigin: Item.Center + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ] + } + } + } + } + } + + Component { + id: generalComponent + + SectionPage { + title: qsTr("General") + subtitle: qsTr("Control dashboard visibility and interaction timing.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SwitchRow { + label: qsTr("Enabled") + checked: root.enabled + onToggled: checked => { + root.enabled = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Show on hover") + checked: root.showOnHover + onToggled: checked => { + root.showOnHover = checked; + root.saveConfig(); + } + } + } + + SectionContainer { + Layout.fillWidth: true + contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true + label: qsTr("Media update interval") + value: root.mediaUpdateInterval + from: 100 + to: 10000 + stepSize: 100 + suffix: "ms" + validator: IntValidator { + bottom: 100 + top: 10000 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + onValueModified: newValue => { + root.mediaUpdateInterval = Math.round(newValue); + root.saveConfig(); + } + } + + SliderInput { + Layout.fillWidth: true + label: qsTr("Drag threshold") + value: root.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { + bottom: 0 + top: 100 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + onValueModified: newValue => { + root.dragThreshold = Math.round(newValue); + root.saveConfig(); + } + } + } + } + } + + Component { + id: tabsComponent + + SectionPage { + title: qsTr("Tabs") + subtitle: qsTr("Choose which dashboard pages are available.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ConnectedButtonGroup { + rootItem: root + rows: 2 + options: [ + { + label: qsTr("Dashboard"), + propertyName: "showDashboard", + onToggled: function (checked) { + root.showDashboard = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Media"), + propertyName: "showMedia", + onToggled: function (checked) { + root.showMedia = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Performance"), + propertyName: "showPerformance", + onToggled: function (checked) { + root.showPerformance = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Weather"), + propertyName: "showWeather", + onToggled: function (checked) { + root.showWeather = checked; + root.saveConfig(); + } + } + ] + } + } + } + } + + Component { + id: performanceComponent + + SectionPage { + title: qsTr("Performance") + subtitle: qsTr("Configure which resource cards the dashboard shows.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ConnectedButtonGroup { + rootItem: root + options: root.performanceOptions() + } + } + + SectionContainer { + Layout.fillWidth: true + contentSpacing: Tokens.spacing.normal + + SliderInput { + Layout.fillWidth: true + label: qsTr("Resource update interval") + value: root.resourceUpdateInterval + from: 100 + to: 10000 + stepSize: 100 + suffix: "ms" + validator: IntValidator { + bottom: 100 + top: 10000 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + onValueModified: newValue => { + root.resourceUpdateInterval = Math.round(newValue); + root.saveConfig(); + } + } + } + } + } + + component SectionPage: StyledFlickable { + id: sectionPage + + required property string title + property string subtitle: "" + default property alias contentItems: contentLayout.data + + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sectionPage + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true + text: sectionPage.title + font.pointSize: Tokens.font.size.extraLarge + font.weight: 600 + } + + StyledText { + Layout.fillWidth: true + Layout.bottomMargin: Tokens.spacing.small + text: sectionPage.subtitle + color: Colours.palette.m3outline + visible: text.length > 0 + wrapMode: Text.WordWrap + } + } + } + + component SectionNavButton: StyledRect { + id: navButton + + required property var section + property bool active: false + + signal clicked + + implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 + color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Tokens.rounding.normal + + Behavior on color { + CAnim {} + } + + StateLayer { + onClicked: navButton.clicked() + } + + RowLayout { + id: navRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: navButton.section.icon + fill: navButton.active ? 1 : 0 + color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.large + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: navButton.section.title + font.weight: navButton.active ? 500 : 400 + elide: Text.ElideRight + maximumLineCount: 1 + } + + StyledText { + Layout.fillWidth: true + text: navButton.section.description + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: "chevron_right" + opacity: navButton.active ? 1 : 0 + color: Colours.palette.m3primary + } + } + } +} diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml new file mode 100644 index 000000000..be67eab27 --- /dev/null +++ b/modules/controlcenter/dashboard/GeneralSection.qml @@ -0,0 +1,128 @@ +import ".." +import "../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +SectionContainer { + id: root + + required property var rootItem + + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("General Settings") + font.pointSize: Tokens.font.size.normal + } + + SwitchRow { + label: qsTr("Enabled") + checked: root.rootItem.enabled + onToggled: checked => { + root.rootItem.enabled = checked; + root.rootItem.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Show on hover") + checked: root.rootItem.showOnHover + onToggled: checked => { + root.rootItem.showOnHover = checked; + root.rootItem.saveConfig(); + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Show Dashboard tab") + checked: root.rootItem.showDashboard + onToggled: checked => { + root.rootItem.showDashboard = checked; + root.rootItem.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Show Media tab") + checked: root.rootItem.showMedia + onToggled: checked => { + root.rootItem.showMedia = checked; + root.rootItem.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Show Performance tab") + checked: root.rootItem.showPerformance + onToggled: checked => { + root.rootItem.showPerformance = checked; + root.rootItem.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Show Weather tab") + checked: root.rootItem.showWeather + onToggled: checked => { + root.rootItem.showWeather = checked; + root.rootItem.saveConfig(); + } + } + } + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Media update interval") + value: root.rootItem.mediaUpdateInterval + from: 100 + to: 10000 + stepSize: 100 + suffix: "ms" + validator: IntValidator { + bottom: 100 + top: 10000 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + + onValueModified: newValue => { + root.rootItem.mediaUpdateInterval = Math.round(newValue); + root.rootItem.saveConfig(); + } + } + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Drag threshold") + value: root.rootItem.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { + bottom: 0 + top: 100 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + + onValueModified: newValue => { + root.rootItem.dragThreshold = Math.round(newValue); + root.rootItem.saveConfig(); + } + } +} diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml new file mode 100644 index 000000000..ea6c68b3e --- /dev/null +++ b/modules/controlcenter/dashboard/PerformanceSection.qml @@ -0,0 +1,106 @@ +import ".." +import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.UPower +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +SectionContainer { + id: root + + required property var rootItem + // GPU toggle is hidden when gpuType is "NONE" (no GPU data available) + readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" + // Battery toggle is hidden when no laptop battery is present + readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery + + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Performance Resources") + font.pointSize: Tokens.font.size.normal + } + + ConnectedButtonGroup { + rootItem: root.rootItem + options: { + let opts = []; + if (root.batteryAvailable) + opts.push({ + "label": qsTr("Battery"), + "propertyName": "showBattery", + "onToggled": function (checked) { + root.rootItem.showBattery = checked; + root.rootItem.saveConfig(); + } + }); + + if (root.gpuAvailable) + opts.push({ + "label": qsTr("GPU"), + "propertyName": "showGpu", + "onToggled": function (checked) { + root.rootItem.showGpu = checked; + root.rootItem.saveConfig(); + } + }); + + opts.push({ + "label": qsTr("CPU"), + "propertyName": "showCpu", + "onToggled": function (checked) { + root.rootItem.showCpu = checked; + root.rootItem.saveConfig(); + } + }, { + "label": qsTr("Memory"), + "propertyName": "showMemory", + "onToggled": function (checked) { + root.rootItem.showMemory = checked; + root.rootItem.saveConfig(); + } + }, { + "label": qsTr("Storage"), + "propertyName": "showStorage", + "onToggled": function (checked) { + root.rootItem.showStorage = checked; + root.rootItem.saveConfig(); + } + }, { + "label": qsTr("Network"), + "propertyName": "showNetwork", + "onToggled": function (checked) { + root.rootItem.showNetwork = checked; + root.rootItem.saveConfig(); + } + }); + return opts; + } + } + + SliderInput { + Layout.fillWidth: true + + label: qsTr("Resource update interval") + value: root.rootItem.resourceUpdateInterval + from: 100 + to: 10000 + stepSize: 100 + suffix: "ms" + validator: IntValidator { + bottom: 100 + top: 10000 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + + onValueModified: newValue => { + root.rootItem.resourceUpdateInterval = Math.round(newValue); + root.rootItem.saveConfig(); + } + } +} diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml index 0dd464f16..f2372dbe1 100644 --- a/modules/controlcenter/launcher/LauncherPane.qml +++ b/modules/controlcenter/launcher/LauncherPane.qml @@ -3,19 +3,19 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "../../launcher/services" +import "../../../utils/scripts/fuzzysort.js" as Fuzzy +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config import qs.utils -import Caelestia -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts -import "../../../utils/scripts/fuzzysort.js" as Fuzzy Item { id: root @@ -24,35 +24,21 @@ Item { property var selectedApp: root.session.launcher.active property bool hideFromLauncherChecked: false - - anchors.fill: parent - - onSelectedAppChanged: { - root.session.launcher.active = root.selectedApp; - updateToggleState(); - } - - Connections { - target: root.session.launcher - function onActiveChanged() { - root.selectedApp = root.session.launcher.active; - updateToggleState(); - } - } + property bool favouriteChecked: false + property string searchText: "" + property list filteredApps: [] function updateToggleState() { if (!root.selectedApp) { root.hideFromLauncherChecked = false; + root.favouriteChecked = false; return; } const appId = root.selectedApp.id || root.selectedApp.entry?.id; - if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { - root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); - } else { - root.hideFromLauncherChecked = false; - } + root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); + root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); } function saveHiddenApps(isHidden) { @@ -62,7 +48,7 @@ Item { const appId = root.selectedApp.id || root.selectedApp.entry?.id; - const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; if (isHidden) { if (!hiddenApps.includes(appId)) { @@ -75,19 +61,9 @@ Item { } } - Config.launcher.hiddenApps = hiddenApps; - Config.save(); - } - - AppDb { - id: allAppsDb - - path: `${Paths.state}/apps.sqlite` - entries: DesktopEntries.applications.values + GlobalConfig.launcher.hiddenApps = hiddenApps; } - property string searchText: "" - function filterApps(search: string): list { if (!search || search.trim() === "") { const apps = []; @@ -120,12 +96,17 @@ Item { return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); } - property list filteredApps: [] - function updateFilteredApps() { filteredApps = filterApps(searchText); } + anchors.fill: parent + + onSelectedAppChanged: { + root.session.launcher.active = root.selectedApp; + updateToggleState(); + } + onSearchTextChanged: { updateFilteredApps(); } @@ -135,29 +116,47 @@ Item { } Connections { - target: allAppsDb + function onActiveChanged() { + root.selectedApp = root.session.launcher.active; + updateToggleState(); + } + + target: root.session.launcher + } + + AppDb { + id: allAppsDb + + path: `${Paths.state}/apps.sqlite` + favouriteApps: GlobalConfig.launcher.favouriteApps + entries: DesktopEntries.applications.values + } + + Connections { function onAppsChanged() { updateFilteredApps(); } + + target: allAppsDb } SplitPaneLayout { anchors.fill: parent leftContent: Component { - ColumnLayout { id: leftLauncherLayout + anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Launcher") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -169,9 +168,9 @@ Item { toggled: !root.session.launcher.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Launcher settings") onClicked: { @@ -187,9 +186,9 @@ Item { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } @@ -200,11 +199,11 @@ Item { StyledRect { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.bottomMargin: Appearance.spacing.small + Layout.topMargin: Tokens.spacing.normal + Layout.bottomMargin: Tokens.spacing.small color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) @@ -213,7 +212,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Appearance.padding.normal + anchors.leftMargin: Tokens.padding.normal text: "search" color: Colours.palette.m3onSurfaceVariant @@ -224,11 +223,11 @@ Item { anchors.left: searchIcon.right anchors.right: clearIcon.left - anchors.leftMargin: Appearance.spacing.small - anchors.rightMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small + anchors.rightMargin: Tokens.spacing.small - topPadding: Appearance.padding.normal - bottomPadding: Appearance.padding.normal + topPadding: Tokens.padding.normal + bottomPadding: Tokens.padding.normal placeholderText: qsTr("Search applications...") @@ -242,7 +241,7 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Appearance.padding.normal + anchors.rightMargin: Tokens.padding.normal width: searchField.text ? implicitWidth : implicitWidth / 2 opacity: { @@ -270,13 +269,13 @@ Item { Behavior on width { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } @@ -284,8 +283,10 @@ Item { Loader { id: appsListLoader + Layout.fillWidth: true Layout.fillHeight: true + asynchronous: true active: true sourceComponent: StyledListView { @@ -295,7 +296,7 @@ Item { Layout.fillHeight: true model: root.filteredApps - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 clip: true StyledScrollBar.vertical: StyledScrollBar { @@ -305,15 +306,20 @@ Item { delegate: StyledRect { required property var modelData - width: parent ? parent.width : 0 - readonly property bool isSelected: root.selectedApp === modelData + width: parent ? parent.width : 0 + implicitHeight: 40 + color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal opacity: 0 + Component.onCompleted: { + opacity = 1; + } + Behavior on opacity { NumberAnimation { duration: 1000 @@ -321,12 +327,8 @@ Item { } } - Component.onCompleted: { - opacity = 1; - } - StateLayer { - function onClicked(): void { + onClicked: { root.session.launcher.active = modelData; } } @@ -335,11 +337,12 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IconImage { + asynchronous: true Layout.alignment: Qt.AlignVCenter implicitSize: 32 source: { @@ -351,11 +354,40 @@ Item { StyledText { Layout.fillWidth: true text: modelData.name || modelData.entry?.name || qsTr("Unknown") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } - } - implicitHeight: 40 + Loader { + readonly property bool isHidden: modelData ? Strings.testRegexList(GlobalConfig.launcher.hiddenApps, modelData.id) : false + readonly property bool isFav: modelData ? Strings.testRegexList(GlobalConfig.launcher.favouriteApps, modelData.id) : false + + Layout.alignment: Qt.AlignVCenter + asynchronous: true + active: isHidden || isFav + + sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) + } + + Component { + id: hiddenIcon + + MaterialIcon { + text: "visibility_off" + fill: 1 + color: Colours.palette.m3primary + } + } + + Component { + id: favouriteIcon + + MaterialIcon { + text: "favorite" + fill: 1 + color: Colours.palette.m3primary + } + } + } } } } @@ -382,11 +414,30 @@ Item { nextComponent = targetComponent; } + onPaneChanged: { + nextComponent = getComponentForPane(); + paneId = pane ? (pane.id || pane.entry?.id || "") : ""; + } + + onDisplayedAppChanged: { + if (displayedApp) { + const appId = displayedApp.id || displayedApp.entry?.id; + root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); + root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); + } else { + root.hideFromLauncherChecked = false; + root.favouriteChecked = false; + } + } + Loader { id: rightLauncherLoader + property var displayedApp: rightLauncherPane.displayedApp + anchors.fill: parent + asynchronous: true opacity: 1 scale: 1 transformOrigin: Item.Center @@ -395,8 +446,6 @@ Item { sourceComponent: rightLauncherPane.targetComponent active: true - property var displayedApp: rightLauncherPane.displayedApp - onItemChanged: { if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { rightLauncherPane.displayedApp = rightLauncherPane.pane; @@ -431,24 +480,6 @@ Item { ] } } - - onPaneChanged: { - nextComponent = getComponentForPane(); - paneId = pane ? (pane.id || pane.entry?.id || "") : ""; - } - - onDisplayedAppChanged: { - if (displayedApp) { - const appId = displayedApp.id || displayedApp.entry?.id; - if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { - root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); - } else { - root.hideFromLauncherChecked = false; - } - } else { - root.hideFromLauncherChecked = false; - } - } } } } @@ -458,6 +489,7 @@ Item { StyledFlickable { id: settingsFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -481,16 +513,16 @@ Item { ColumnLayout { id: appDetailsLayout - anchors.fill: parent readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null - spacing: Appearance.spacing.normal + anchors.fill: parent + spacing: Tokens.spacing.normal SettingsHeader { - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.padding.large * 2 + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.padding.large * 2 visible: displayedApp === null icon: "apps" title: qsTr("Launcher Applications") @@ -498,21 +530,23 @@ Item { Item { Layout.alignment: Qt.AlignHCenter - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.padding.large * 2 + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.padding.large * 2 visible: displayedApp !== null implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) - implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight + implicitHeight: appIconImage.implicitHeight + Tokens.spacing.normal + appTitleText.implicitHeight ColumnLayout { anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IconImage { id: appIconImage + + asynchronous: true Layout.alignment: Qt.AlignHCenter - implicitSize: Appearance.font.size.extraLarge * 3 * 2 + implicitSize: Tokens.font.size.extraLarge * 3 * 2 source: { const app = appDetailsLayout.displayedApp; if (!app) @@ -527,9 +561,10 @@ Item { StyledText { id: appTitleText + Layout.alignment: Qt.AlignHCenter text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.bold: true } } @@ -538,12 +573,13 @@ Item { Item { Layout.fillWidth: true Layout.fillHeight: true - Layout.topMargin: Appearance.spacing.large - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 + Layout.topMargin: Tokens.spacing.large + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 StyledFlickable { id: detailsFlickable + anchors.fill: parent flickableDirection: Flickable.VerticalFlick contentHeight: debugLayout.height @@ -554,23 +590,62 @@ Item { ColumnLayout { id: debugLayout + anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SwitchRow { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal + visible: appDetailsLayout.displayedApp !== null + label: qsTr("Mark as favourite") + checked: root.favouriteChecked + // disabled if: + // * app is hidden + // * app isn't in favouriteApps array but marked as favourite anyway + // ^^^ This means that this app is favourited because of a regex check + // this button can not toggle regexed apps + enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (GlobalConfig.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) + opacity: enabled ? 1 : 0.6 + onToggled: checked => { + root.favouriteChecked = checked; + const app = appDetailsLayout.displayedApp; + if (app) { + const appId = app.id || app.entry?.id; + const favouriteApps = GlobalConfig.launcher.favouriteApps ? [...GlobalConfig.launcher.favouriteApps] : []; + if (checked) { + if (!favouriteApps.includes(appId)) { + favouriteApps.push(appId); + } + } else { + const index = favouriteApps.indexOf(appId); + if (index !== -1) { + favouriteApps.splice(index, 1); + } + } + GlobalConfig.launcher.favouriteApps = favouriteApps; + } + } + } + SwitchRow { + Layout.topMargin: Tokens.spacing.normal visible: appDetailsLayout.displayedApp !== null label: qsTr("Hide from launcher") checked: root.hideFromLauncherChecked - enabled: appDetailsLayout.displayedApp !== null + // disabled if: + // * app is favourited + // * app isn't in hiddenApps array but marked as hidden anyway + // ^^^ This means that this app is hidden because of a regex check + // this button can not toggle regexed apps + enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (GlobalConfig.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) + opacity: enabled ? 1 : 0.6 onToggled: checked => { root.hideFromLauncherChecked = checked; const app = appDetailsLayout.displayedApp; if (app) { const appId = app.id || app.entry?.id; - const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; + const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; if (checked) { if (!hiddenApps.includes(appId)) { hiddenApps.push(appId); @@ -581,8 +656,7 @@ Item { hiddenApps.splice(index, 1); } } - Config.launcher.hiddenApps = hiddenApps; - Config.save(); + GlobalConfig.launcher.hiddenApps = hiddenApps; } } } diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml index 5eaf6e0e0..f01812308 100644 --- a/modules/controlcenter/launcher/Settings.qml +++ b/modules/controlcenter/launcher/Settings.qml @@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "apps" @@ -23,7 +23,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("General") description: qsTr("General launcher settings") } @@ -33,8 +33,7 @@ ColumnLayout { label: qsTr("Enabled") checked: Config.launcher.enabled toggle.onToggled: { - Config.launcher.enabled = checked; - Config.save(); + GlobalConfig.launcher.enabled = checked; } } @@ -42,38 +41,35 @@ ColumnLayout { label: qsTr("Show on hover") checked: Config.launcher.showOnHover toggle.onToggled: { - Config.launcher.showOnHover = checked; - Config.save(); + GlobalConfig.launcher.showOnHover = checked; } } ToggleRow { label: qsTr("Vim keybinds") - checked: Config.launcher.vimKeybinds + checked: GlobalConfig.launcher.vimKeybinds toggle.onToggled: { - Config.launcher.vimKeybinds = checked; - Config.save(); + GlobalConfig.launcher.vimKeybinds = checked; } } ToggleRow { label: qsTr("Enable dangerous actions") - checked: Config.launcher.enableDangerousActions + checked: GlobalConfig.launcher.enableDangerousActions toggle.onToggled: { - Config.launcher.enableDangerousActions = checked; - Config.save(); + GlobalConfig.launcher.enableDangerousActions = checked; } } } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Display") description: qsTr("Display and appearance settings") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Max shown items") @@ -94,28 +90,28 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Prefixes") description: qsTr("Command prefix settings") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Special prefix") - value: Config.launcher.specialPrefix || qsTr("None") + value: GlobalConfig.launcher.specialPrefix || qsTr("None") } PropertyRow { showTopMargin: true label: qsTr("Action prefix") - value: Config.launcher.actionPrefix || qsTr("None") + value: GlobalConfig.launcher.actionPrefix || qsTr("None") } } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Fuzzy search") description: qsTr("Fuzzy search settings") } @@ -123,95 +119,90 @@ ColumnLayout { SectionContainer { ToggleRow { label: qsTr("Apps") - checked: Config.launcher.useFuzzy.apps + checked: GlobalConfig.launcher.useFuzzy.apps toggle.onToggled: { - Config.launcher.useFuzzy.apps = checked; - Config.save(); + GlobalConfig.launcher.useFuzzy.apps = checked; } } ToggleRow { label: qsTr("Actions") - checked: Config.launcher.useFuzzy.actions + checked: GlobalConfig.launcher.useFuzzy.actions toggle.onToggled: { - Config.launcher.useFuzzy.actions = checked; - Config.save(); + GlobalConfig.launcher.useFuzzy.actions = checked; } } ToggleRow { label: qsTr("Schemes") - checked: Config.launcher.useFuzzy.schemes + checked: GlobalConfig.launcher.useFuzzy.schemes toggle.onToggled: { - Config.launcher.useFuzzy.schemes = checked; - Config.save(); + GlobalConfig.launcher.useFuzzy.schemes = checked; } } ToggleRow { label: qsTr("Variants") - checked: Config.launcher.useFuzzy.variants + checked: GlobalConfig.launcher.useFuzzy.variants toggle.onToggled: { - Config.launcher.useFuzzy.variants = checked; - Config.save(); + GlobalConfig.launcher.useFuzzy.variants = checked; } } ToggleRow { label: qsTr("Wallpapers") - checked: Config.launcher.useFuzzy.wallpapers + checked: GlobalConfig.launcher.useFuzzy.wallpapers toggle.onToggled: { - Config.launcher.useFuzzy.wallpapers = checked; - Config.save(); + GlobalConfig.launcher.useFuzzy.wallpapers = checked; } } } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Sizes") description: qsTr("Size settings for launcher items") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Item width") - value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemWidth) } PropertyRow { showTopMargin: true label: qsTr("Item height") - value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemHeight) } PropertyRow { showTopMargin: true label: qsTr("Wallpaper width") - value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperWidth) } PropertyRow { showTopMargin: true label: qsTr("Wallpaper height") - value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight) + value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperHeight) } } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Hidden apps") description: qsTr("Applications hidden from launcher") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Total hidden") - value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) + value: qsTr("%1").arg(GlobalConfig.launcher.hiddenApps ? GlobalConfig.launcher.hiddenApps.length : 0) } } } diff --git a/modules/controlcenter/monitors/MonitorsPane.qml b/modules/controlcenter/monitors/MonitorsPane.qml index 8cdc49e9f..82809fd9f 100644 --- a/modules/controlcenter/monitors/MonitorsPane.qml +++ b/modules/controlcenter/monitors/MonitorsPane.qml @@ -8,7 +8,7 @@ import qs.components.controls import qs.components.effects import qs.components.containers import qs.services -import qs.config +import Caelestia.Config import Quickshell import Quickshell.Hyprland import QtQuick @@ -18,9 +18,47 @@ Item { id: root required property Session session + readonly property var monitorModel: Hyprctl.monitors + + function selectMonitor(monitor: var): void { + if (!monitor) + return; + root.session.monitor.active = { + id: monitor.id, + name: monitor.name + }; + } + + function selectedMonitor(): var { + const active = root.session.monitor.active; + if (!active) + return null; + + for (const monitor of root.monitorModel) { + if (monitor.name === active.name || monitor.id === active.id) + return monitor; + } + + return null; + } + + function ensureSingleMonitorSelected(): void { + if (!root.session.monitor.active && (root.monitorModel?.length ?? 0) === 1) + root.selectMonitor(root.monitorModel[0]); + } anchors.fill: parent + Component.onCompleted: Qt.callLater(root.ensureSingleMonitorSelected) + + Connections { + target: Hyprctl + + function onMonitorsChanged(): void { + root.ensureSingleMonitorSelected(); + } + } + // ── Two-column split (mirrors NetworkingPane) ────────────────────── SplitPaneLayout { id: splitLayout @@ -44,16 +82,16 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal // Header row RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Monitors") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -64,9 +102,9 @@ Item { toggled: Monitors.identifying icon: "tv_signin" accent: "Secondary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Identify monitors") onClicked: Monitors.toggleIdentification() @@ -76,14 +114,14 @@ Item { // Subtitle StyledText { Layout.fillWidth: true - text: qsTr("%1 display(s) connected").arg(Hyprland.monitors.length) + text: qsTr("%1 display(s) connected").arg(root.monitorModel.length) color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } - // Monitor list — use Hyprland.monitors directly as model + // Monitor list — use hyprctl data so refresh rate and modes are available Repeater { - model: Hyprland.monitors + model: root.monitorModel delegate: MonitorListItem { required property var modelData @@ -94,9 +132,10 @@ Item { monitor: modelData active: root.session.monitor.active !== null && root.session.monitor.active !== undefined - && root.session.monitor.active.id === modelData.id + && (root.session.monitor.active.id === modelData.id + || root.session.monitor.active.name === modelData.name) - onClicked: root.session.monitor.active = modelData + onClicked: root.selectMonitor(modelData) } } } @@ -108,7 +147,7 @@ Item { Item { id: rightPaneItem - property var selectedMonitor: root.session.monitor.active + property var selectedMonitor: root.selectedMonitor() property string paneId: selectedMonitor ? ("mon:" + (selectedMonitor.name ?? "")) : "overview" @@ -127,6 +166,15 @@ Item { Connections { target: root.session.monitor function onActiveChanged(): void { + rightPaneItem.selectedMonitor = root.selectedMonitor(); + rightPaneItem.nextComponent = rightPaneItem.resolveComponent(); + } + } + + Connections { + target: Hyprctl + function onMonitorsChanged(): void { + rightPaneItem.selectedMonitor = root.selectedMonitor(); rightPaneItem.nextComponent = rightPaneItem.resolveComponent(); } } @@ -166,11 +214,11 @@ Item { signal clicked() - implicitHeight: itemRow.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: itemRow.implicitHeight + Tokens.padding.normal * 2 StyledRect { anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Qt.alpha( Colours.tPalette.m3surfaceContainer, listItem.active ? Colours.tPalette.m3surfaceContainer.a : 0 @@ -186,14 +234,14 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal // Monitor icon badge StyledRect { implicitWidth: implicitHeight - implicitHeight: monIcon.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.normal + implicitHeight: monIcon.implicitHeight + Tokens.padding.normal * 2 + radius: Tokens.rounding.normal color: listItem.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh @@ -202,7 +250,7 @@ Item { id: monIcon anchors.centerIn: parent text: "monitor" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: listItem.active ? 1 : 0 color: listItem.active ? Colours.palette.m3onPrimaryContainer @@ -226,7 +274,7 @@ Item { StyledText { Layout.fillWidth: true elide: Text.ElideRight - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline text: { const m = listItem.monitor; @@ -240,16 +288,16 @@ Item { // Focused badge StyledRect { visible: listItem.monitor?.focused ?? false - implicitWidth: focusedLabel.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: focusedLabel.implicitHeight + Appearance.padding.small * 2 - radius: Appearance.rounding.full + implicitWidth: focusedLabel.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: focusedLabel.implicitHeight + Tokens.padding.small * 2 + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, 0.9) StyledText { id: focusedLabel anchors.centerIn: parent text: qsTr("Active") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onPrimaryContainer } } @@ -284,7 +332,7 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "monitor" @@ -297,10 +345,10 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small Repeater { - model: Hyprland.monitors + model: root.monitorModel delegate: PropertyRow { required property var modelData @@ -329,15 +377,15 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "tv_signin" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: Colours.palette.m3onSurfaceVariant } @@ -346,12 +394,12 @@ Item { spacing: 0 StyledText { text: qsTr("Identify displays") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: qsTr("Show monitor IDs on each screen") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } @@ -381,7 +429,7 @@ Item { flickable: detailFlickable } - readonly property var mon: root.session.monitor.active + readonly property var mon: root.selectedMonitor() readonly property var brightnessMon: mon ? Brightness.getMonitor(mon.name) : null ColumnLayout { @@ -389,7 +437,7 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal // ── Header ────────────────────────────────────────── ConnectionHeader { @@ -402,7 +450,7 @@ Item { Layout.fillWidth: true visible: detailFlickable.brightnessMon !== null && detailFlickable.brightnessMon !== undefined - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Brightness") @@ -410,22 +458,22 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: (detailFlickable.brightnessMon?.brightness ?? 0) > 0.5 ? "brightness_high" : "brightness_low" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurfaceVariant } StyledSlider { Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 from: 0; to: 1; stepSize: 0.01 value: detailFlickable.brightnessMon?.brightness ?? 0 onMoved: detailFlickable.brightnessMon?.setBrightness(value) @@ -435,17 +483,96 @@ Item { text: qsTr("%1%").arg( Math.round((detailFlickable.brightnessMon?.brightness ?? 0) * 100)) Layout.preferredWidth: 38 - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline } } } } + // ── Refresh rate ───────────────────────────────────── + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + + SectionHeader { + title: qsTr("Refresh rate") + description: qsTr("Set the display refresh rate") + } + + SectionContainer { + contentSpacing: Tokens.spacing.normal + + SpinBoxRow { + Layout.fillWidth: true + label: qsTr("Refresh rate") + value: detailFlickable.mon?.refreshRate ?? 60 + min: 10 + max: 1000 + step: 0.01 + onValueModified: value => { + if (detailFlickable.mon) + Monitors.setRefreshRate(detailFlickable.mon.name, value); + } + } + + RowLayout { + Layout.fillWidth: true + visible: (detailFlickable.mon?.availableModes?.length ?? 0) > 0 + spacing: Tokens.spacing.small + + Repeater { + model: detailFlickable.mon?.availableModes ?? [] + + delegate: StyledRect { + required property string modelData + required property int index + + Layout.fillWidth: true + implicitHeight: modeLabel.implicitHeight + Tokens.padding.normal * 2 + radius: Tokens.rounding.full + + readonly property real modeRate: { + const match = modelData.match(/@(\d+(?:\.\d+)?)Hz/); + return match ? parseFloat(match[1]) : 0; + } + readonly property bool isActive: Math.abs((detailFlickable.mon?.refreshRate ?? 0) - modeRate) < 0.1 + + color: isActive + ? Colours.palette.m3secondaryContainer + : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) + + StateLayer { + color: parent.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant + onClicked: { + if (detailFlickable.mon && parent.modeRate > 0) + Monitors.setRefreshRate(detailFlickable.mon.name, parent.modeRate); + } + } + + StyledText { + id: modeLabel + anchors.centerIn: parent + text: modelData + font.pointSize: Tokens.font.size.small + color: parent.isActive + ? Colours.palette.m3onSecondaryContainer + : Colours.palette.m3onSurfaceVariant + } + + Behavior on color { CAnim {} } + } + } + } + } + } + // ── Rotation ───────────────────────────────────────── ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Rotation") @@ -453,11 +580,11 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: [ @@ -488,7 +615,7 @@ Item { // ── Scale ──────────────────────────────────────────── ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Scale") @@ -496,22 +623,22 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "zoom_in" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurfaceVariant } StyledSlider { id: scaleSlider Layout.fillWidth: true - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 from: 0.5; to: 3.0; stepSize: 0.25 value: detailFlickable.mon?.scale ?? 1 @@ -524,7 +651,7 @@ Item { StyledText { text: qsTr("×%1").arg((detailFlickable.mon?.scale ?? 1).toFixed(2)) Layout.preferredWidth: 42 - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline } } @@ -532,7 +659,7 @@ Item { // Quick-pick chips: 1×, 1.25×, 1.5×, 2× RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: [1.0, 1.25, 1.5, 2.0] @@ -542,8 +669,8 @@ Item { required property int index Layout.fillWidth: true - implicitHeight: scaleChipLabel.implicitHeight + Appearance.padding.normal * 2 - radius: Appearance.rounding.full + implicitHeight: scaleChipLabel.implicitHeight + Tokens.padding.normal * 2 + radius: Tokens.rounding.full readonly property bool isActive: Math.abs((detailFlickable.mon?.scale ?? 1) - modelData) < 0.01 @@ -566,7 +693,7 @@ Item { id: scaleChipLabel anchors.centerIn: parent text: qsTr("×%1").arg(modelData.toFixed(2)) - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: parent.isActive ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant @@ -582,8 +709,8 @@ Item { // ── Arrangement ────────────────────────────────────── ColumnLayout { Layout.fillWidth: true - visible: Hyprland.monitors.length > 1 - spacing: Appearance.spacing.normal + visible: root.monitorModel.length > 1 + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Arrangement") @@ -592,14 +719,16 @@ Item { // One card per OTHER monitor — use visible to skip self Repeater { - model: Hyprland.monitors + model: root.monitorModel delegate: SectionContainer { + id: targetSection + required property var modelData required property int index Layout.fillWidth: true - contentSpacing: Appearance.spacing.small + contentSpacing: Tokens.spacing.small // Hide the current monitor's own entry without JS filter visible: detailFlickable.mon !== null @@ -609,11 +738,11 @@ Item { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { text: "tv" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurfaceVariant } @@ -622,15 +751,15 @@ Item { text: qsTr("Relative to Monitor %1 (%2)") .arg(modelData.id ?? 0) .arg(modelData.name ?? "") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } GridLayout { Layout.fillWidth: true columns: 4 - columnSpacing: Appearance.spacing.small - rowSpacing: Appearance.spacing.small + columnSpacing: Tokens.spacing.small + rowSpacing: Tokens.spacing.small Repeater { model: [ @@ -652,7 +781,7 @@ Item { Monitors.arrange( detailFlickable.mon.name, modelData.pos, - parent.parent.parent.modelData.id + targetSection.modelData.id ); } } @@ -665,7 +794,7 @@ Item { // ── Display information ─────────────────────────────── ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Display information") @@ -673,7 +802,7 @@ Item { } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Name") @@ -756,11 +885,11 @@ Item { required property bool isActive signal clicked() - implicitHeight: chipContent.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: chipContent.implicitHeight + Tokens.padding.normal * 2 StyledRect { anchors.fill: parent - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: chip.isActive ? Colours.palette.m3secondaryContainer : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) @@ -781,7 +910,7 @@ Item { Layout.alignment: Qt.AlignHCenter text: "screen_rotation" rotation: chip.chipAngle - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: chip.isActive ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant @@ -791,7 +920,7 @@ Item { StyledText { Layout.alignment: Qt.AlignHCenter text: chip.chipLabel - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: chip.isActive ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant @@ -808,11 +937,11 @@ Item { required property string btnLabel signal clicked() - implicitHeight: btnContent.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: btnContent.implicitHeight + Tokens.padding.normal * 2 StyledRect { anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) StateLayer { @@ -828,14 +957,14 @@ Item { MaterialIcon { Layout.alignment: Qt.AlignHCenter text: arrangeBtn.btnIcon - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal color: Colours.palette.m3onSurfaceVariant } StyledText { Layout.alignment: Qt.AlignHCenter text: arrangeBtn.btnLabel - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } } diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml index 4e60b3d48..1daeb4a29 100644 --- a/modules/controlcenter/network/EthernetDetails.qml +++ b/modules/controlcenter/network/EthernetDetails.qml @@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts DeviceDetails { id: root @@ -43,7 +43,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection status") @@ -69,7 +69,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Device properties") @@ -77,7 +77,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Interface") @@ -100,7 +100,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection information") diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml index d1eb95798..3a947c866 100644 --- a/modules/controlcenter/network/EthernetList.qml +++ b/modules/controlcenter/network/EthernetList.qml @@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts DeviceList { id: root @@ -23,11 +23,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Settings") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -39,9 +39,9 @@ DeviceList { toggled: !root.session.ethernet.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { if (root.session.ethernet.active) @@ -62,15 +62,15 @@ DeviceList { readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface width: ListView.view ? ListView.view.width : undefined - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { id: stateLayer - function onClicked(): void { + onClicked: { root.session.ethernet.active = modelData; } } @@ -79,15 +79,15 @@ DeviceList { id: rowLayout anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh StyledRect { @@ -101,7 +101,7 @@ DeviceList { anchors.centerIn: parent text: "cable" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.connected ? 1 : 0 color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface @@ -124,13 +124,13 @@ DeviceList { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: modelData.connected ? 500 : 400 elide: Text.ElideRight } @@ -141,21 +141,21 @@ DeviceList { id: connectBtn implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) StateLayer { - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - - function onClicked(): void { + onClicked: { if (modelData.connected && modelData.connection) { Nmcli.disconnectEthernet(modelData.connection, () => {}); } else { Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); } } + + color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } MaterialIcon { diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml index 59d82bb08..d66620f1f 100644 --- a/modules/controlcenter/network/EthernetPane.qml +++ b/modules/controlcenter/network/EthernetPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers -import qs.config -import Quickshell.Widgets -import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml index 90bfcf46a..f13b6fca8 100644 --- a/modules/controlcenter/network/EthernetSettings.qml +++ b/modules/controlcenter/network/EthernetSettings.qml @@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "cable" @@ -23,9 +23,9 @@ ColumnLayout { } StyledText { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large text: qsTr("Ethernet devices") - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } @@ -36,9 +36,9 @@ ColumnLayout { StyledRect { Layout.fillWidth: true - implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 + implicitHeight: ethernetInfo.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { @@ -47,9 +47,9 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { text: qsTr("Total devices") @@ -58,18 +58,18 @@ ColumnLayout { StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.length) color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("Connected devices") } StyledText { text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } } diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml index bda7cb18a..d8700ecd9 100644 --- a/modules/controlcenter/network/NetworkSettings.qml +++ b/modules/controlcenter/network/NetworkSettings.qml @@ -2,22 +2,22 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services -import qs.config -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "router" @@ -25,13 +25,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Ethernet") description: qsTr("Ethernet device information") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Total devices") @@ -46,7 +46,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Wireless") description: qsTr("WiFi network settings") } @@ -62,34 +62,33 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("VPN") description: qsTr("VPN provider settings") - visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 + visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 } SectionContainer { - visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 + visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 ToggleRow { label: qsTr("VPN enabled") - checked: Config.utilities.vpn.enabled + checked: GlobalConfig.utilities.vpn.enabled toggle.onToggled: { - Config.utilities.vpn.enabled = checked; - Config.save(); + GlobalConfig.utilities.vpn.enabled = checked; } } PropertyRow { showTopMargin: true label: qsTr("Providers") - value: qsTr("%1").arg(Config.utilities.vpn.provider.length) + value: qsTr("%1").arg(GlobalConfig.utilities.vpn.provider.length) } TextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.topMargin: Tokens.spacing.normal + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 text: qsTr("⚙ Manage VPN Providers") inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer @@ -101,13 +100,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Current connection") description: qsTr("Active network connection information") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Network") @@ -141,20 +140,20 @@ ColumnLayout { parent: Overlay.overlay anchors.centerIn: parent - width: Math.min(600, parent.width - Appearance.padding.large * 2) - height: Math.min(700, parent.height - Appearance.padding.large * 2) + width: Math.min(600, parent.width - Tokens.padding.large * 2) + height: Math.min(700, parent.height - Tokens.padding.large * 2) modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside background: StyledRect { color: Colours.palette.m3surface - radius: Appearance.rounding.large + radius: Tokens.rounding.large } StyledFlickable { anchors.fill: parent - anchors.margins: Appearance.padding.large * 1.5 + anchors.margins: Tokens.padding.large * 1.5 flickableDirection: Flickable.VerticalFlick contentHeight: vpnSettingsContent.height clip: true diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml index 26cdbfacd..e02bfafbc 100644 --- a/modules/controlcenter/network/NetworkingPane.qml +++ b/modules/controlcenter/network/NetworkingPane.qml @@ -3,17 +3,17 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root @@ -43,15 +43,15 @@ Item { anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Network") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -63,9 +63,9 @@ Item { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Toggle WiFi") onClicked: { @@ -77,9 +77,9 @@ Item { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Scan for networks") onClicked: { @@ -91,9 +91,9 @@ Item { toggled: !root.session.ethernet.active && !root.session.network.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller tooltip: qsTr("Network settings") onClicked: { @@ -120,6 +120,7 @@ Item { Loader { Layout.fillWidth: true + asynchronous: true sourceComponent: Component { VpnList { session: root.session @@ -138,6 +139,7 @@ Item { Loader { Layout.fillWidth: true + asynchronous: true sourceComponent: Component { EthernetList { session: root.session @@ -156,6 +158,7 @@ Item { Loader { Layout.fillWidth: true + asynchronous: true sourceComponent: Component { WirelessList { session: root.session @@ -196,9 +199,6 @@ Item { } Connections { - target: root.session && root.session.vpn ? root.session.vpn : null - enabled: target !== null - function onActiveChanged() { // Clear others when VPN is selected if (root.session && root.session.vpn && root.session.vpn.active) { @@ -209,12 +209,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - } - Connections { - target: root.session && root.session.ethernet ? root.session.ethernet : null + target: root.session && root.session.vpn ? root.session.vpn : null enabled: target !== null + } + Connections { function onActiveChanged() { // Clear others when ethernet is selected if (root.session && root.session.ethernet && root.session.ethernet.active) { @@ -225,12 +225,12 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } - } - Connections { - target: root.session && root.session.network ? root.session.network : null + target: root.session && root.session.ethernet ? root.session.ethernet : null enabled: target !== null + } + Connections { function onActiveChanged() { // Clear others when wireless is selected if (root.session && root.session.network && root.session.network.active) { @@ -241,6 +241,9 @@ Item { } rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); } + + target: root.session && root.session.network ? root.session.network : null + enabled: target !== null } Loader { @@ -278,6 +281,7 @@ Item { StyledFlickable { id: settingsFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: settingsInner.height @@ -301,6 +305,7 @@ Item { StyledFlickable { id: ethernetFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: ethernetDetailsInner.height @@ -324,6 +329,7 @@ Item { StyledFlickable { id: wirelessFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: wirelessDetailsInner.height @@ -347,6 +353,7 @@ Item { StyledFlickable { id: vpnFlickable + flickableDirection: Flickable.VerticalFlick contentHeight: vpnDetailsInner.height diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml index 1c71cd716..7ac2dc50f 100644 --- a/modules/controlcenter/network/VpnDetails.qml +++ b/modules/controlcenter/network/VpnDetails.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config import qs.utils -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts DeviceDetails { id: root @@ -21,7 +21,7 @@ DeviceDetails { readonly property bool providerEnabled: { if (!vpnProvider || vpnProvider.index === undefined) return false; - const provider = Config.utilities.vpn.provider[vpnProvider.index]; + const provider = GlobalConfig.utilities.vpn.provider[vpnProvider.index]; return provider && typeof provider === "object" && provider.enabled === true; } @@ -37,7 +37,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection status") @@ -55,8 +55,8 @@ DeviceDetails { const index = root.vpnProvider.index; // Copy providers and update enabled state - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - const p = Config.utilities.vpn.provider[i]; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, @@ -72,25 +72,31 @@ DeviceDetails { newProvider.enabled = (i === index) ? false : (p.enabled !== false); } + if (p.connectCmd && p.connectCmd.length > 0) { + newProvider.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + newProvider.disconnectCmd = p.disconnectCmd; + } + providers.push(newProvider); } else { providers.push(p); } } - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; } } RowLayout { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - spacing: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal + spacing: Tokens.spacing.normal TextButton { Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 visible: root.providerEnabled enabled: !VPN.connecting inactiveColour: Colours.palette.m3primaryContainer @@ -109,10 +115,13 @@ DeviceDetails { inactiveOnColour: Colours.palette.m3onSecondaryContainer onClicked: { + const provider = GlobalConfig.utilities.vpn.provider[root.vpnProvider.index]; editVpnDialog.editIndex = root.vpnProvider.index; editVpnDialog.providerName = root.vpnProvider.name; editVpnDialog.displayName = root.vpnProvider.displayName; editVpnDialog.interfaceName = root.vpnProvider.interface; + editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : ""; + editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : ""; editVpnDialog.open(); } } @@ -125,23 +134,46 @@ DeviceDetails { onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i !== root.vpnProvider.index) { - providers.push(Config.utilities.vpn.provider[i]); + providers.push(GlobalConfig.utilities.vpn.provider[i]); } } - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; root.session.vpn.active = null; } } } + + TextButton { + Layout.fillWidth: true + Layout.topMargin: Tokens.spacing.normal + visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" + text: qsTr("Open Login Page") + inactiveColour: Colours.palette.m3tertiaryContainer + inactiveOnColour: Colours.palette.m3onTertiaryContainer + + onClicked: { + Qt.openUrlExternally(VPN.status.authUrl); + } + } + + StyledText { + Layout.fillWidth: true + Layout.topMargin: Tokens.spacing.normal + visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" + text: qsTr("Click 'Connect' to generate authentication URL") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + } } } }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Provider details") @@ -149,7 +181,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Provider") @@ -176,12 +208,31 @@ DeviceDetails { return qsTr("Disabled"); if (VPN.connecting) return qsTr("Connecting..."); - if (VPN.connected) + + switch (VPN.status.state) { + case "connected": return qsTr("Connected"); - return qsTr("Enabled (Not connected)"); + case "disconnected": + return qsTr("Disconnected"); + case "connecting": + return qsTr("Connecting..."); + case "needs-auth": + return qsTr("Authentication required"); + case "error": + return qsTr("Error"); + default: + return qsTr("Unknown"); + } } } + PropertyRow { + visible: VPN.status.reason !== "" + showTopMargin: true + label: qsTr("Details") + value: VPN.status.reason + } + PropertyRow { showTopMargin: true label: qsTr("Enabled") @@ -200,11 +251,17 @@ DeviceDetails { property string providerName: "" property string displayName: "" property string interfaceName: "" + property string connectCmd: "" + property string disconnectCmd: "" + + function closeWithAnimation(): void { + close(); + } parent: Overlay.overlay anchors.centerIn: parent - width: Math.min(400, parent.width - Appearance.padding.large * 2) - padding: Appearance.padding.large * 1.5 + width: Math.min(400, parent.width - Tokens.padding.large * 2) + padding: Tokens.padding.large * 1.5 modal: true closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside @@ -217,15 +274,13 @@ DeviceDetails { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "scale" from: 0.7 to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } @@ -234,29 +289,23 @@ DeviceDetails { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } Anim { property: "scale" from: 1 to: 0.7 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } - function closeWithAnimation(): void { - close(); - } - Overlay.modal: Rectangle { color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) } background: StyledRect { color: Colours.palette.m3surfaceContainerHigh - radius: Appearance.rounding.large + radius: Tokens.rounding.large Elevation { anchors.fill: parent @@ -267,21 +316,21 @@ DeviceDetails { } contentItem: ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { text: qsTr("Edit VPN Provider") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Display Name") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -289,7 +338,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -302,8 +351,9 @@ DeviceDetails { StyledTextField { id: displayNameField + anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.displayName onTextChanged: editVpnDialog.displayName = text @@ -313,11 +363,11 @@ DeviceDetails { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -325,7 +375,7 @@ DeviceDetails { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -338,8 +388,9 @@ DeviceDetails { StyledTextField { id: interfaceNameField + anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: editVpnDialog.interfaceName onTextChanged: editVpnDialog.interfaceName = text @@ -347,10 +398,86 @@ DeviceDetails { } } + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller / 2 + visible: editVpnDialog.connectCmd.length > 0 + + StyledText { + text: qsTr("Connect Command") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Tokens.rounding.small + border.width: 1 + border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: connectCmdFieldEdit + + anchors.centerIn: parent + width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: editVpnDialog.connectCmd + onTextChanged: editVpnDialog.connectCmd = text + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller / 2 + visible: editVpnDialog.disconnectCmd.length > 0 + + StyledText { + text: qsTr("Disconnect Command") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Tokens.rounding.small + border.width: 1 + border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: disconnectCmdFieldEdit + + anchors.centerIn: parent + width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: editVpnDialog.disconnectCmd + onTextChanged: editVpnDialog.disconnectCmd = text + } + } + } + RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { Layout.fillWidth: true @@ -369,24 +496,44 @@ DeviceDetails { onClicked: { const providers = []; - const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex]; + const oldProvider = GlobalConfig.utilities.vpn.provider[editVpnDialog.editIndex]; const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i === editVpnDialog.editIndex) { - providers.push({ - name: editVpnDialog.providerName, + const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0; + const newProvider = { displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, + enabled: wasEnabled, interface: editVpnDialog.interfaceName, - enabled: wasEnabled - }); + name: editVpnDialog.providerName + }; + + if (hasCommands) { + newProvider.connectCmd = editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0); + newProvider.disconnectCmd = editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); + } + + providers.push(newProvider); } else { - providers.push(Config.utilities.vpn.provider[i]); + const p = GlobalConfig.utilities.vpn.provider[i]; + const reconstructed = { + displayName: p.displayName, + enabled: p.enabled, + interface: p.interface, + name: p.name + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); } } - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; editVpnDialog.closeWithAnimation(); } } diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml index 81f4a45a3..d9266b34e 100644 --- a/modules/controlcenter/network/VpnList.qml +++ b/modules/controlcenter/network/VpnList.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound import ".." +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts ColumnLayout { id: root @@ -18,18 +18,17 @@ ColumnLayout { property bool showHeader: true property int pendingSwitchIndex: -1 - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Connections { - target: VPN function onConnectedChanged() { if (!VPN.connected && root.pendingSwitchIndex >= 0) { const targetIndex = root.pendingSwitchIndex; root.pendingSwitchIndex = -1; const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - const p = Config.utilities.vpn.provider[i]; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, @@ -37,19 +36,26 @@ ColumnLayout { interface: p.interface, enabled: (i === targetIndex) }; + if (p.connectCmd && p.connectCmd.length > 0) { + newProvider.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + newProvider.disconnectCmd = p.disconnectCmd; + } providers.push(newProvider); } else { providers.push(p); } } - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; Qt.callLater(function () { VPN.toggle(); }); } } + + target: VPN } TextButton { @@ -70,10 +76,10 @@ ColumnLayout { Layout.preferredHeight: contentHeight interactive: false - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller model: ScriptModel { - values: Config.utilities.vpn.provider.map((provider, index) => { + values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { const isObject = typeof provider === "object"; const name = isObject ? (provider.name || "custom") : String(provider); const displayName = isObject ? (provider.displayName || name) : name; @@ -97,12 +103,13 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined + implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { - function onClicked(): void { + onClicked: { if (root.session && root.session.vpn) { root.session.vpn.active = modelData; } @@ -115,15 +122,15 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { @@ -131,7 +138,7 @@ ColumnLayout { anchors.centerIn: parent text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.enabled && VPN.connected ? 1 : 0 color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } @@ -152,21 +159,44 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: { - if (modelData.enabled && VPN.connected) + if (!modelData.enabled) + return qsTr("Disabled"); + + if (VPN.connecting) + return qsTr("Connecting..."); + + switch (VPN.status.state) { + case "connected": return qsTr("Connected"); - if (modelData.enabled && VPN.connecting) + case "disconnected": + return qsTr("Enabled"); + case "connecting": return qsTr("Connecting..."); - if (modelData.enabled) + case "needs-auth": + return qsTr("Auth required"); + case "error": + return qsTr("Error"); + default: return qsTr("Enabled"); - return qsTr("Disabled"); + } + } + color: { + if (!modelData.enabled) + return Colours.palette.m3outline; + if (VPN.status.state === "connected") + return Colours.palette.m3primary; + if (VPN.status.state === "error") + return Colours.palette.m3error; + if (VPN.status.state === "needs-auth") + return Colours.palette.m3tertiary; + return Colours.palette.m3onSurface; } - color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: modelData.enabled && VPN.connected ? 500 : 400 elide: Text.ElideRight } @@ -175,14 +205,13 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) StateLayer { - enabled: !VPN.connecting - function onClicked(): void { + onClicked: { const clickedIndex = modelData.index; if (modelData.enabled) { @@ -193,8 +222,8 @@ ColumnLayout { VPN.toggle(); } else { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - const p = Config.utilities.vpn.provider[i]; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; if (typeof p === "object") { const newProvider = { name: p.name, @@ -202,13 +231,18 @@ ColumnLayout { interface: p.interface, enabled: (i === clickedIndex) }; + if (p.connectCmd && p.connectCmd.length > 0) { + newProvider.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + newProvider.disconnectCmd = p.disconnectCmd; + } providers.push(newProvider); } else { providers.push(p); } } - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; Qt.callLater(function () { VPN.toggle(); @@ -216,6 +250,8 @@ ColumnLayout { } } } + + enabled: !VPN.connecting } MaterialIcon { @@ -229,21 +265,33 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: deleteIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: "transparent" StateLayer { - function onClicked(): void { + onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i !== modelData.index) { - providers.push(Config.utilities.vpn.provider[i]); + const p = GlobalConfig.utilities.vpn.provider[i]; + const reconstructed = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: p.enabled + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); } } - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; } } @@ -256,8 +304,6 @@ ColumnLayout { } } } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } } @@ -270,56 +316,8 @@ ColumnLayout { property string providerName: "" property string displayName: "" property string interfaceName: "" - - parent: Overlay.overlay - x: Math.round((parent.width - width) / 2) - y: Math.round((parent.height - height) / 2) - implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) - padding: Appearance.padding.large * 1.5 - - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - opacity: 0 - scale: 0.7 - - enter: Transition { - ParallelAnimation { - Anim { - property: "opacity" - from: 0 - to: 1 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized - } - Anim { - property: "scale" - from: 0.7 - to: 1 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - } - - exit: Transition { - ParallelAnimation { - Anim { - property: "opacity" - from: 1 - to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - Anim { - property: "scale" - from: 1 - to: 0.7 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - } + property string connectCmd: "" + property string disconnectCmd: "" function showProviderSelection(): void { currentState = "selection"; @@ -335,6 +333,8 @@ ColumnLayout { providerName = providerType; displayName = defaultDisplayName; interfaceName = ""; + connectCmd = ""; + disconnectCmd = ""; if (currentState === "selection") { transitionToForm.start(); @@ -346,59 +346,77 @@ ColumnLayout { } function showEditForm(index: int): void { - const provider = Config.utilities.vpn.provider[index]; + const provider = GlobalConfig.utilities.vpn.provider[index]; const isObject = typeof provider === "object"; editIndex = index; providerName = isObject ? (provider.name || "custom") : String(provider); displayName = isObject ? (provider.displayName || providerName) : providerName; interfaceName = isObject ? (provider.interface || "") : ""; + connectCmd = isObject && provider.connectCmd ? provider.connectCmd.join(" ") : ""; + disconnectCmd = isObject && provider.disconnectCmd ? provider.disconnectCmd.join(" ") : ""; currentState = "form"; open(); } - Overlay.modal: Rectangle { - color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) - } + parent: Overlay.overlay + x: Math.round((parent.width - width) / 2) + y: Math.round((parent.height - height) / 2) + implicitWidth: Math.min(400, parent.width - Tokens.padding.large * 2) + padding: Tokens.padding.large * 1.5 - onClosed: { - currentState = "selection"; - } + modal: true + closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - SequentialAnimation { - id: transitionToForm + opacity: 0 + scale: 0.7 + enter: Transition { ParallelAnimation { Anim { - target: selectionContent property: "opacity" - to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + from: 0 + to: 1 + type: Anim.Emphasized } - } - - ScriptAction { - script: { - vpnDialog.currentState = "form"; + Anim { + property: "scale" + from: 0.7 + to: 1 + type: Anim.Emphasized } } + } + exit: Transition { ParallelAnimation { Anim { - target: formContent property: "opacity" - to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + from: 1 + to: 0 + type: Anim.EmphasizedSmall + } + Anim { + property: "scale" + from: 1 + to: 0.7 + type: Anim.EmphasizedSmall } } } + Overlay.modal: Rectangle { + color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) + } + + onClosed: { + currentState = "selection"; + } + background: StyledRect { color: Colours.palette.m3surfaceContainerHigh - radius: Appearance.rounding.large + radius: Tokens.rounding.large Elevation { anchors.fill: parent @@ -409,8 +427,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } } @@ -420,8 +437,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } @@ -429,20 +445,19 @@ ColumnLayout { id: selectionContent anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal visible: vpnDialog.currentState === "selection" opacity: vpnDialog.currentState === "selection" ? 1 : 0 Behavior on opacity { Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.EmphasizedSmall } } StyledText { text: qsTr("Add VPN Provider") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -451,27 +466,26 @@ ColumnLayout { text: qsTr("Choose a provider to add") wrapMode: Text.WordWrap color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } TextButton { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true text: qsTr("NetBird") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push({ name: "netbird", displayName: "NetBird", interface: "wt0" }); - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } @@ -483,16 +497,15 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push({ name: "tailscale", displayName: "Tailscale", interface: "tailscale0" }); - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } @@ -504,23 +517,22 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { const providers = []; - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push({ name: "warp", displayName: "Cloudflare WARP", interface: "CloudflareWARP" }); - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } TextButton { Layout.fillWidth: true - text: qsTr("WireGuard (Custom)") + text: qsTr("WireGuard") inactiveColour: Colours.tPalette.m3surfaceContainerHigh inactiveOnColour: Colours.palette.m3onSurface onClicked: { @@ -529,7 +541,17 @@ ColumnLayout { } TextButton { - Layout.topMargin: Appearance.spacing.normal + Layout.fillWidth: true + text: qsTr("Custom") + inactiveColour: Colours.tPalette.m3surfaceContainerHigh + inactiveOnColour: Colours.palette.m3onSurface + onClicked: { + vpnDialog.showAddForm("custom", "Custom VPN"); + } + } + + TextButton { + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true text: qsTr("Cancel") inactiveColour: Colours.palette.m3secondaryContainer @@ -542,30 +564,29 @@ ColumnLayout { id: formContent anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal visible: vpnDialog.currentState === "form" opacity: vpnDialog.currentState === "form" ? 1 : 0 Behavior on opacity { Anim { - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.EmphasizedSmall } } StyledText { text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Display Name") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -573,7 +594,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -586,8 +607,9 @@ ColumnLayout { StyledTextField { id: displayNameField + anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.displayName onTextChanged: vpnDialog.displayName = text @@ -597,11 +619,11 @@ ColumnLayout { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller / 2 + spacing: Tokens.spacing.smaller / 2 StyledText { text: qsTr("Interface (e.g., wg0, torguard)") - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3onSurfaceVariant } @@ -609,7 +631,7 @@ ColumnLayout { Layout.fillWidth: true implicitHeight: 40 color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.small + radius: Tokens.rounding.small border.width: 1 border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) @@ -622,8 +644,9 @@ ColumnLayout { StyledTextField { id: interfaceNameField + anchors.centerIn: parent - width: parent.width - Appearance.padding.normal + width: parent.width - Tokens.padding.normal horizontalAlignment: TextInput.AlignLeft text: vpnDialog.interfaceName onTextChanged: vpnDialog.interfaceName = text @@ -631,10 +654,86 @@ ColumnLayout { } } + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller / 2 + visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") + + StyledText { + text: qsTr("Connect Command (e.g., wg-quick up wg0)") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Tokens.rounding.small + border.width: 1 + border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: connectCmdField + + anchors.centerIn: parent + width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.connectCmd + onTextChanged: vpnDialog.connectCmd = text + } + } + } + + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller / 2 + visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") + + StyledText { + text: qsTr("Disconnect Command (e.g., wg-quick down wg0)") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: 40 + color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) + radius: Tokens.rounding.small + border.width: 1 + border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) + + Behavior on color { + CAnim {} + } + Behavior on border.color { + CAnim {} + } + + StyledTextField { + id: disconnectCmdField + + anchors.centerIn: parent + width: parent.width - Tokens.padding.normal + horizontalAlignment: TextInput.AlignLeft + text: vpnDialog.disconnectCmd + onTextChanged: vpnDialog.disconnectCmd = text + } + } + } + RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { Layout.fillWidth: true @@ -647,40 +746,85 @@ ColumnLayout { TextButton { Layout.fillWidth: true text: qsTr("Save") - enabled: vpnDialog.interfaceName.length > 0 + enabled: { + const hasCommands = vpnDialog.connectCmd.length > 0 || vpnDialog.disconnectCmd.length > 0; + if (hasCommands) { + return vpnDialog.interfaceName.length > 0 && vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; + } + return vpnDialog.interfaceName.length > 0; + } inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer onClicked: { const providers = []; + const hasCommands = vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; const newProvider = { - name: vpnDialog.providerName, displayName: vpnDialog.displayName || vpnDialog.interfaceName, - interface: vpnDialog.interfaceName + enabled: false, + interface: vpnDialog.interfaceName, + name: vpnDialog.providerName }; + if (hasCommands) { + newProvider.connectCmd = vpnDialog.connectCmd.split(" ").filter(s => s.length > 0); + newProvider.disconnectCmd = vpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); + } + if (vpnDialog.editIndex >= 0) { - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { + const oldProvider = GlobalConfig.utilities.vpn.provider[vpnDialog.editIndex]; + if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) { + newProvider.enabled = oldProvider.enabled; + } + + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { if (i === vpnDialog.editIndex) { providers.push(newProvider); } else { - providers.push(Config.utilities.vpn.provider[i]); + providers.push(GlobalConfig.utilities.vpn.provider[i]); } } } else { - for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { - providers.push(Config.utilities.vpn.provider[i]); + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + providers.push(GlobalConfig.utilities.vpn.provider[i]); } providers.push(newProvider); } - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; vpnDialog.closeWithAnimation(); } } } } } + + SequentialAnimation { + id: transitionToForm + + ParallelAnimation { + Anim { + target: selectionContent + property: "opacity" + to: 0 + type: Anim.EmphasizedSmall + } + } + + ScriptAction { + script: { + vpnDialog.currentState = "form"; + } + } + + ParallelAnimation { + Anim { + target: formContent + property: "opacity" + to: 1 + type: Anim.EmphasizedSmall + } + } + } } } diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml index 49d801d9a..064f32a12 100644 --- a/modules/controlcenter/network/VpnSettings.qml +++ b/modules/controlcenter/network/VpnSettings.qml @@ -2,23 +2,23 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Controls -import QtQuick.Layouts ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "vpn_key" @@ -26,7 +26,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("General") description: qsTr("VPN configuration") } @@ -34,32 +34,31 @@ ColumnLayout { SectionContainer { ToggleRow { label: qsTr("VPN enabled") - checked: Config.utilities.vpn.enabled + checked: GlobalConfig.utilities.vpn.enabled toggle.onToggled: { - Config.utilities.vpn.enabled = checked; - Config.save(); + GlobalConfig.utilities.vpn.enabled = checked; } } } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Providers") description: qsTr("Manage VPN providers") } SectionContainer { - contentSpacing: Appearance.spacing.normal + contentSpacing: Tokens.spacing.normal ListView { Layout.fillWidth: true Layout.preferredHeight: contentHeight interactive: false - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller model: ScriptModel { - values: Config.utilities.vpn.provider.map((provider, index) => { + values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { const isObject = typeof provider === "object"; const name = isObject ? (provider.name || "custom") : String(provider); const displayName = isObject ? (provider.displayName || name) : name; @@ -82,19 +81,20 @@ ColumnLayout { required property int index width: ListView.view ? ListView.view.width : undefined + implicitHeight: 60 color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal MaterialIcon { text: modelData.isActive ? "vpn_key" : "vpn_key_off" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline } @@ -109,53 +109,81 @@ ColumnLayout { StyledText { text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface")) - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline } } IconButton { icon: modelData.isActive ? "arrow_downward" : "arrow_upward" - visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1 + visible: !modelData.isActive || GlobalConfig.utilities.vpn.provider.length > 1 onClicked: { - if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) { + const providers = []; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + const p = GlobalConfig.utilities.vpn.provider[i]; + const reconstructed = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: p.enabled + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); + } + + if (modelData.isActive && index < providers.length - 1) { // Move down - const providers = [...Config.utilities.vpn.provider]; const temp = providers[index]; providers[index] = providers[index + 1]; providers[index + 1] = temp; - Config.utilities.vpn.provider = providers; - Config.save(); } else if (!modelData.isActive) { // Make active (move to top) - const providers = [...Config.utilities.vpn.provider]; const provider = providers.splice(index, 1)[0]; providers.unshift(provider); - Config.utilities.vpn.provider = providers; - Config.save(); } + + GlobalConfig.utilities.vpn.provider = providers; } } IconButton { icon: "delete" onClicked: { - const providers = [...Config.utilities.vpn.provider]; - providers.splice(index, 1); - Config.utilities.vpn.provider = providers; - Config.save(); + const providers = []; + for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { + if (i !== index) { + const p = GlobalConfig.utilities.vpn.provider[i]; + const reconstructed = { + name: p.name, + displayName: p.displayName, + interface: p.interface, + enabled: p.enabled + }; + if (p.connectCmd && p.connectCmd.length > 0) { + reconstructed.connectCmd = p.connectCmd; + } + if (p.disconnectCmd && p.disconnectCmd.length > 0) { + reconstructed.disconnectCmd = p.disconnectCmd; + } + providers.push(reconstructed); + } + } + GlobalConfig.utilities.vpn.provider = providers; } } } - - implicitHeight: 60 } } } TextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal text: qsTr("+ Add Provider") inactiveColour: Colours.palette.m3primaryContainer inactiveOnColour: Colours.palette.m3onPrimaryContainer @@ -167,13 +195,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Quick Add") description: qsTr("Add common VPN providers") } SectionContainer { - contentSpacing: Appearance.spacing.smaller + contentSpacing: Tokens.spacing.smaller TextButton { Layout.fillWidth: true @@ -182,14 +210,13 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { - const providers = [...Config.utilities.vpn.provider]; + const providers = [...GlobalConfig.utilities.vpn.provider]; providers.push({ name: "netbird", displayName: "NetBird", interface: "wt0" }); - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; } } @@ -200,14 +227,13 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { - const providers = [...Config.utilities.vpn.provider]; + const providers = [...GlobalConfig.utilities.vpn.provider]; providers.push({ name: "tailscale", displayName: "Tailscale", interface: "tailscale0" }); - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; } } @@ -218,14 +244,13 @@ ColumnLayout { inactiveOnColour: Colours.palette.m3onSurface onClicked: { - const providers = [...Config.utilities.vpn.provider]; + const providers = [...GlobalConfig.utilities.vpn.provider]; providers.push({ name: "warp", displayName: "Cloudflare WARP", interface: "CloudflareWARP" }); - Config.utilities.vpn.provider = providers; - Config.save(); + GlobalConfig.utilities.vpn.provider = providers; } } } diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml index e8777cdf7..e39744caa 100644 --- a/modules/controlcenter/network/WirelessDetails.qml +++ b/modules/controlcenter/network/WirelessDetails.qml @@ -3,15 +3,15 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config import qs.utils -import QtQuick -import QtQuick.Layouts DeviceDetails { id: root @@ -19,66 +19,12 @@ DeviceDetails { required property Session session readonly property var network: root.session.network.active - device: network - - Component.onCompleted: { - updateDeviceDetails(); - checkSavedProfile(); - } - - onNetworkChanged: { - connectionUpdateTimer.stop(); - if (network && network.ssid) { - connectionUpdateTimer.start(); - } - updateDeviceDetails(); - checkSavedProfile(); - } - function checkSavedProfile(): void { if (network && network.ssid) { Nmcli.loadSavedConnections(() => {}); } } - Connections { - target: Nmcli - function onActiveChanged() { - updateDeviceDetails(); - } - function onWirelessDeviceDetailsChanged() { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { - connectionUpdateTimer.stop(); - } - } - } - } - - Timer { - id: connectionUpdateTimer - interval: 500 - repeat: true - running: network && network.ssid - onTriggered: { - if (network) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); - if (isActive) { - if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { - Nmcli.getWirelessDeviceDetails("", () => {}); - } else { - connectionUpdateTimer.stop(); - } - } else { - if (Nmcli.wirelessDeviceDetails !== null) { - Nmcli.wirelessDeviceDetails = null; - } - } - } - } - } - function updateDeviceDetails(): void { if (network && network.ssid) { const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); @@ -92,6 +38,22 @@ DeviceDetails { } } + device: network + + Component.onCompleted: { + updateDeviceDetails(); + checkSavedProfile(); + } + + onNetworkChanged: { + connectionUpdateTimer.stop(); + if (network && network.ssid) { + connectionUpdateTimer.start(); + } + updateDeviceDetails(); + checkSavedProfile(); + } + headerComponent: Component { ConnectionHeader { icon: root.network?.isSecure ? "lock" : "wifi" @@ -102,7 +64,7 @@ DeviceDetails { sections: [ Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection status") @@ -124,8 +86,8 @@ DeviceDetails { TextButton { Layout.fillWidth: true - Layout.topMargin: Appearance.spacing.normal - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.topMargin: Tokens.spacing.normal + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 visible: { if (!root.network || !root.network.ssid) { return false; @@ -150,7 +112,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Network properties") @@ -158,7 +120,7 @@ DeviceDetails { } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("SSID") @@ -193,7 +155,7 @@ DeviceDetails { }, Component { ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SectionHeader { title: qsTr("Connection information") @@ -208,4 +170,44 @@ DeviceDetails { } } ] + + Connections { + function onActiveChanged() { + updateDeviceDetails(); + } + function onWirelessDeviceDetailsChanged() { + if (network && network.ssid) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { + connectionUpdateTimer.stop(); + } + } + } + + target: Nmcli + } + + Timer { + id: connectionUpdateTimer + + interval: 500 + repeat: true + running: network && network.ssid + onTriggered: { + if (network) { + const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); + if (isActive) { + if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { + Nmcli.getWirelessDeviceDetails("", () => {}); + } else { + connectionUpdateTimer.stop(); + } + } else { + if (Nmcli.wirelessDeviceDetails !== null) { + Nmcli.wirelessDeviceDetails = null; + } + } + } + } + } } diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml index 57a155fd7..b6acd0792 100644 --- a/modules/controlcenter/network/WirelessList.qml +++ b/modules/controlcenter/network/WirelessList.qml @@ -3,22 +3,28 @@ pragma ComponentBehavior: Bound import ".." import "../components" import "." +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts DeviceList { id: root required property Session session + function checkSavedProfileForNetwork(ssid: string): void { + if (ssid && ssid.length > 0) { + Nmcli.loadSavedConnections(() => {}); + } + } + title: qsTr("Networks (%1)").arg(Nmcli.networks.length) description: qsTr("All available WiFi networks") activeItem: session.network.active @@ -28,7 +34,7 @@ DeviceList { visible: Nmcli.scanning text: qsTr("Scanning...") color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } @@ -42,11 +48,11 @@ DeviceList { headerComponent: Component { RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { text: qsTr("Settings") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -58,9 +64,9 @@ DeviceList { toggled: Nmcli.wifiEnabled icon: "wifi" accent: "Tertiary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { Nmcli.toggleWifi(null); @@ -71,9 +77,9 @@ DeviceList { toggled: Nmcli.scanning icon: "wifi_find" accent: "Secondary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { Nmcli.rescanWifi(); @@ -84,9 +90,9 @@ DeviceList { toggled: !root.session.network.active icon: "settings" accent: "Primary" - iconSize: Appearance.font.size.normal - horizontalPadding: Appearance.padding.normal - verticalPadding: Appearance.padding.smaller + iconSize: Tokens.font.size.normal + horizontalPadding: Tokens.padding.normal + verticalPadding: Tokens.padding.smaller onClicked: { if (root.session.network.active) @@ -104,12 +110,13 @@ DeviceList { required property var modelData width: ListView.view ? ListView.view.width : undefined + implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StateLayer { - function onClicked(): void { + onClicked: { root.session.network.active = modelData; if (modelData && modelData.ssid) { root.checkSavedProfileForNetwork(modelData.ssid); @@ -123,15 +130,15 @@ DeviceList { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh MaterialIcon { @@ -139,7 +146,7 @@ DeviceList { anchors.centerIn: parent text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: modelData.active ? 1 : 0 color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface } @@ -160,7 +167,7 @@ DeviceList { RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true @@ -175,7 +182,7 @@ DeviceList { return qsTr("Open"); } color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: modelData.active ? 500 : 400 elide: Text.ElideRight } @@ -184,13 +191,13 @@ DeviceList { StyledRect { implicitWidth: implicitHeight - implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) StateLayer { - function onClicked(): void { + onClicked: { if (modelData.active) { Nmcli.disconnectFromNetwork(); } else { @@ -208,8 +215,6 @@ DeviceList { } } } - - implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 } } @@ -219,10 +224,4 @@ DeviceList { checkSavedProfileForNetwork(item.ssid); } } - - function checkSavedProfileForNetwork(ssid: string): void { - if (ssid && ssid.length > 0) { - Nmcli.loadSavedConnections(() => {}); - } - } } diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml index 8150af9cf..436891765 100644 --- a/modules/controlcenter/network/WirelessPane.qml +++ b/modules/controlcenter/network/WirelessPane.qml @@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers -import qs.config -import Quickshell.Widgets -import QtQuick SplitPaneWithDetails { id: root diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml index 7ad5204a4..ac88f9fca 100644 --- a/modules/controlcenter/network/WirelessPasswordDialog.qml +++ b/modules/controlcenter/network/WirelessPasswordDialog.qml @@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound import ".." import "." +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick -import QtQuick.Layouts Item { id: root @@ -29,6 +29,47 @@ Item { } property bool isClosing: false + + function checkConnectionStatus(): void { + if (!root.visible || !connectButton.connecting) { + return; + } + + const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); + + if (isConnected) { + connectionSuccessTimer.start(); + return; + } + + if (Nmcli.pendingConnection === null && connectButton.connecting) { + if (connectionMonitor.repeatCount > 10) { + connectionMonitor.stop(); + connectButton.connecting = false; + connectButton.hasError = true; + connectButton.enabled = true; + connectButton.text = qsTr("Connect"); + passwordContainer.passwordBuffer = ""; + if (root.network && root.network.ssid) { + Nmcli.forgetNetwork(root.network.ssid); + } + } + } + } + + function closeDialog(): void { + if (isClosing) { + return; + } + + isClosing = true; + passwordContainer.passwordBuffer = ""; + connectButton.connecting = false; + connectButton.hasError = false; + connectButton.text = qsTr("Connect"); + connectionMonitor.stop(); + } + visible: session.network.showPasswordDialog || isClosing enabled: session.network.showPasswordDialog && !isClosing focus: enabled @@ -58,12 +99,13 @@ Item { anchors.centerIn: parent implicitWidth: 400 - implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + implicitHeight: content.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surface opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 + Keys.onEscapePressed: closeDialog() Behavior on opacity { Anim {} @@ -94,28 +136,26 @@ Item { } } - Keys.onEscapePressed: closeDialog() - ColumnLayout { id: content anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("Enter password") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -123,14 +163,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { id: statusText Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small + Layout.topMargin: Tokens.spacing.small visible: connectButton.connecting || connectButton.hasError text: { if (connectButton.hasError) { @@ -142,24 +182,31 @@ Item { return ""; } color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small font.weight: 400 wrapMode: Text.WordWrap - Layout.maximumWidth: parent.width - Appearance.padding.large * 2 + Layout.maximumWidth: parent.width - Tokens.padding.large * 2 } Item { id: passwordContainer - Layout.topMargin: Appearance.spacing.large - Layout.fillWidth: true - implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) + property string passwordBuffer: "" + + Layout.topMargin: Tokens.spacing.large + Layout.fillWidth: true + implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) focus: true Keys.onPressed: event => { if (!activeFocus) { forceActiveFocus(); } + if (event.key === Qt.Key_Escape) { + event.accepted = false; + closeDialog(); + } + if (connectButton.hasError && event.text && event.text.length > 0) { connectButton.hasError = false; } @@ -177,15 +224,16 @@ Item { } event.accepted = true; } else if (event.text && event.text.length > 0) { + if (event.key === Qt.Key_Tab) { + event.accepted = false; + return; + } passwordBuffer += event.text; event.accepted = true; } } - property string passwordBuffer: "" - Connections { - target: root.session.network function onShowPasswordDialogChanged(): void { if (root.session.network.showPasswordDialog) { Qt.callLater(() => { @@ -195,10 +243,11 @@ Item { }); } } + + target: root.session.network } Connections { - target: root function onVisibleChanged(): void { if (root.visible) { Qt.callLater(() => { @@ -206,11 +255,13 @@ Item { }); } } + + target: root } StyledRect { anchors.fill: parent - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) border.color: { @@ -237,21 +288,22 @@ Item { } StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - - function onClicked(): void { + onClicked: { passwordContainer.forceActiveFocus(); } + + hoverEnabled: false + cursorShape: Qt.IBeamCursor } StyledText { id: placeholder + anchors.centerIn: parent text: qsTr("Password") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono opacity: passwordContainer.passwordBuffer ? 0 : 1 Behavior on opacity { @@ -266,10 +318,10 @@ Item { anchors.centerIn: parent implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal + implicitHeight: Tokens.font.size.normal orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false model: ScriptModel { @@ -283,7 +335,7 @@ Item { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 + radius: Tokens.rounding.small / 2 opacity: 0 scale: 0 @@ -326,8 +378,7 @@ Item { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } @@ -339,15 +390,15 @@ Item { } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { id: cancelButton Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3secondaryContainer inactiveOnColour: Colours.palette.m3onSecondaryContainer text: qsTr("Cancel") @@ -362,7 +413,7 @@ Item { property bool hasError: false Layout.fillWidth: true - Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 + Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 inactiveColour: Colours.palette.m3primary inactiveOnColour: Colours.palette.m3onPrimary text: qsTr("Connect") @@ -414,40 +465,14 @@ Item { } } - function checkConnectionStatus(): void { - if (!root.visible || !connectButton.connecting) { - return; - } - - const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); - - if (isConnected) { - connectionSuccessTimer.start(); - return; - } - - if (Nmcli.pendingConnection === null && connectButton.connecting) { - if (connectionMonitor.repeatCount > 10) { - connectionMonitor.stop(); - connectButton.connecting = false; - connectButton.hasError = true; - connectButton.enabled = true; - connectButton.text = qsTr("Connect"); - passwordContainer.passwordBuffer = ""; - if (root.network && root.network.ssid) { - Nmcli.forgetNetwork(root.network.ssid); - } - } - } - } - Timer { id: connectionMonitor + + property int repeatCount: 0 + interval: 1000 repeat: true triggeredOnStart: false - property int repeatCount: 0 - onTriggered: { repeatCount++; checkConnectionStatus(); @@ -462,6 +487,7 @@ Item { Timer { id: connectionSuccessTimer + interval: 500 onTriggered: { if (root.visible && Nmcli.active && Nmcli.active.ssid) { @@ -477,7 +503,6 @@ Item { } Connections { - target: Nmcli function onActiveChanged() { if (root.visible) { checkConnectionStatus(); @@ -494,18 +519,7 @@ Item { Nmcli.forgetNetwork(ssid); } } - } - - function closeDialog(): void { - if (isClosing) { - return; - } - isClosing = true; - passwordContainer.passwordBuffer = ""; - connectButton.connecting = false; - connectButton.hasError = false; - connectButton.text = qsTr("Connect"); - connectionMonitor.stop(); + target: Nmcli } } diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml index b4eb391d4..f3f1fd4b7 100644 --- a/modules/controlcenter/network/WirelessSettings.qml +++ b/modules/controlcenter/network/WirelessSettings.qml @@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root required property Session session - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal SettingsHeader { icon: "wifi" @@ -23,7 +23,7 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("WiFi status") description: qsTr("General WiFi settings") } @@ -39,13 +39,13 @@ ColumnLayout { } SectionHeader { - Layout.topMargin: Appearance.spacing.large + Layout.topMargin: Tokens.spacing.large title: qsTr("Network information") description: qsTr("Current network connection") } SectionContainer { - contentSpacing: Appearance.spacing.small / 2 + contentSpacing: Tokens.spacing.small / 2 PropertyRow { label: qsTr("Connected network") diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml new file mode 100644 index 000000000..8f04bc3be --- /dev/null +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -0,0 +1,607 @@ +pragma ComponentBehavior: Bound + +import ".." +import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config +import qs.components +import qs.components.containers +import qs.components.controls +import qs.components.effects +import qs.services + +Item { + id: root + + required property Session session + property string activeSection: "notifications" + + property bool notificationsExpire: GlobalConfig.notifs.expire ?? true + property string notificationsFullscreen: GlobalConfig.notifs.fullscreen ?? "on" + property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false + property int notificationsDefaultExpireTimeout: GlobalConfig.notifs.defaultExpireTimeout ?? 5000 + property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3 + + property int maxToasts: Config.utilities.maxToasts ?? 4 + property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off" + property bool chargingChanged: GlobalConfig.utilities.toasts.chargingChanged ?? true + property bool gameModeChanged: GlobalConfig.utilities.toasts.gameModeChanged ?? true + property bool dndChanged: GlobalConfig.utilities.toasts.dndChanged ?? true + property bool audioOutputChanged: GlobalConfig.utilities.toasts.audioOutputChanged ?? true + property bool audioInputChanged: GlobalConfig.utilities.toasts.audioInputChanged ?? true + property bool capsLockChanged: GlobalConfig.utilities.toasts.capsLockChanged ?? true + property bool numLockChanged: GlobalConfig.utilities.toasts.numLockChanged ?? true + property bool kbLayoutChanged: GlobalConfig.utilities.toasts.kbLayoutChanged ?? true + property bool vpnChanged: GlobalConfig.utilities.toasts.vpnChanged ?? true + property bool nowPlaying: GlobalConfig.utilities.toasts.nowPlaying ?? false + + readonly property var sections: [ + { + id: "notifications", + title: qsTr("Notifications"), + description: qsTr("Popup behavior"), + icon: "notifications" + }, + { + id: "toastSettings", + title: qsTr("Toast Settings"), + description: qsTr("Toast visibility"), + icon: "toast" + }, + { + id: "toastEvents", + title: qsTr("Toast Events"), + description: qsTr("Event triggers"), + icon: "rule" + } + ] + + function componentForSection(sectionId) { + switch (sectionId) { + case "toastSettings": + return toastSettingsComponent; + case "toastEvents": + return toastEventsComponent; + case "notifications": + default: + return notificationsComponent; + } + } + + function saveConfig(): void { + GlobalConfig.notifs.expire = root.notificationsExpire; + GlobalConfig.notifs.fullscreen = root.notificationsFullscreen; + GlobalConfig.notifs.openExpanded = root.notificationsOpenExpanded; + GlobalConfig.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; + GlobalConfig.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; + + GlobalConfig.utilities.maxToasts = root.maxToasts; + GlobalConfig.utilities.toasts.fullscreen = root.toastsFullscreen; + GlobalConfig.utilities.toasts.chargingChanged = root.chargingChanged; + GlobalConfig.utilities.toasts.gameModeChanged = root.gameModeChanged; + GlobalConfig.utilities.toasts.dndChanged = root.dndChanged; + GlobalConfig.utilities.toasts.audioOutputChanged = root.audioOutputChanged; + GlobalConfig.utilities.toasts.audioInputChanged = root.audioInputChanged; + GlobalConfig.utilities.toasts.capsLockChanged = root.capsLockChanged; + GlobalConfig.utilities.toasts.numLockChanged = root.numLockChanged; + GlobalConfig.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; + GlobalConfig.utilities.toasts.vpnChanged = root.vpnChanged; + GlobalConfig.utilities.toasts.nowPlaying = root.nowPlaying; + } + + anchors.fill: parent + + SplitPaneLayout { + anchors.fill: parent + leftWidthRatio: 0.32 + leftMinimumWidth: 300 + + leftContent: Component { + StyledFlickable { + id: leftFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } + + ColumnLayout { + id: leftContentLayout + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller + + StyledText { + text: qsTr("Notifications") + font.pointSize: Tokens.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + Repeater { + model: root.sections + + delegate: SectionNavButton { + required property var modelData + + Layout.fillWidth: true + section: modelData + active: root.activeSection === modelData.id + onClicked: root.activeSection = modelData.id + } + } + } + } + } + + rightContent: Component { + Item { + id: rightPaneItem + + property string paneId: root.activeSection + property Component targetComponent: root.componentForSection(root.activeSection) + property Component nextComponent: root.componentForSection(root.activeSection) + + onPaneIdChanged: { + nextComponent = root.componentForSection(root.activeSection); + } + + Loader { + id: rightLoader + anchors.fill: parent + asynchronous: true + opacity: 1 + scale: 1 + transformOrigin: Item.Center + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ] + } + } + } + } + } + + Component { + id: notificationsComponent + + SectionPage { + title: qsTr("Notifications") + subtitle: qsTr("Configure notification popup behavior.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SplitButtonRow { + id: notificationsFullscreenSelector + + function syncActiveItem(): void { + active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem; + } + + label: qsTr("Show in fullscreen") + menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem] + + Component.onCompleted: syncActiveItem() + + Connections { + function onNotificationsFullscreenChanged(): void { + notificationsFullscreenSelector.syncActiveItem(); + } + + target: root + } + + MenuItem { + id: notificationsFullscreenOffItem + + text: qsTr("Off") + icon: "notifications_off" + activeText: qsTr("Off") + onClicked: { + root.notificationsFullscreen = "off"; + root.saveConfig(); + } + } + + MenuItem { + id: notificationsFullscreenOnItem + + text: qsTr("On") + icon: "notifications" + activeText: qsTr("On") + onClicked: { + root.notificationsFullscreen = "on"; + root.saveConfig(); + } + } + } + + SwitchRow { + label: qsTr("Expire automatically") + checked: root.notificationsExpire + onToggled: checked => { + root.notificationsExpire = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Open expanded") + checked: root.notificationsOpenExpanded + onToggled: checked => { + root.notificationsOpenExpanded = checked; + root.saveConfig(); + } + } + + SpinBoxRow { + label: qsTr("Default timeout") + value: root.notificationsDefaultExpireTimeout + min: 1000 + max: 60000 + step: 500 + onValueModified: value => { + root.notificationsDefaultExpireTimeout = value; + root.saveConfig(); + } + } + + SpinBoxRow { + label: qsTr("Group preview count") + value: root.notificationsGroupPreviewNum + min: 1 + max: 10 + step: 1 + onValueModified: value => { + root.notificationsGroupPreviewNum = value; + root.saveConfig(); + } + } + } + } + } + + Component { + id: toastSettingsComponent + + SectionPage { + title: qsTr("Toast Settings") + subtitle: qsTr("Control when toast notifications are shown.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SplitButtonRow { + id: toastFullscreenSelector + + function syncActiveItem(): void { + if (root.toastsFullscreen === "all") { + active = toastFullscreenAllItem; + return; + } + + if (root.toastsFullscreen === "important") { + active = toastFullscreenImportantItem; + return; + } + + active = toastFullscreenOffItem; + } + + Layout.fillWidth: true + z: expanded ? 100 : 0 + label: qsTr("Show in fullscreen") + menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem] + + Component.onCompleted: syncActiveItem() + Connections { + function onToastsFullscreenChanged(): void { + toastFullscreenSelector.syncActiveItem(); + } + + target: root + } + + MenuItem { + id: toastFullscreenOffItem + text: qsTr("Off") + icon: "notifications_off" + activeText: qsTr("Off") + onClicked: { + root.toastsFullscreen = "off"; + root.saveConfig(); + } + } + + MenuItem { + id: toastFullscreenImportantItem + text: qsTr("Important") + icon: "priority_high" + activeText: qsTr("Important") + onClicked: { + root.toastsFullscreen = "important"; + root.saveConfig(); + } + } + + MenuItem { + id: toastFullscreenAllItem + text: qsTr("On") + icon: "notifications" + activeText: qsTr("On") + onClicked: { + root.toastsFullscreen = "all"; + root.saveConfig(); + } + } + } + + SpinBoxRow { + Layout.fillWidth: true + label: qsTr("Visible toasts") + value: root.maxToasts + min: 1 + max: 10 + step: 1 + onValueModified: value => { + root.maxToasts = value; + root.saveConfig(); + } + } + } + } + } + + Component { + id: toastEventsComponent + + SectionPage { + title: qsTr("Toast Events") + subtitle: qsTr("Choose which system events create toasts.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + GridLayout { + Layout.fillWidth: true + columns: 2 + columnSpacing: Tokens.spacing.normal + rowSpacing: Tokens.spacing.normal + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Charging changes") + checked: root.chargingChanged + onToggled: checked => { + root.chargingChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Game mode changes") + checked: root.gameModeChanged + onToggled: checked => { + root.gameModeChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Do not disturb") + checked: root.dndChanged + onToggled: checked => { + root.dndChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Audio output changes") + checked: root.audioOutputChanged + onToggled: checked => { + root.audioOutputChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Audio input changes") + checked: root.audioInputChanged + onToggled: checked => { + root.audioInputChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Caps lock changes") + checked: root.capsLockChanged + onToggled: checked => { + root.capsLockChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Num lock changes") + checked: root.numLockChanged + onToggled: checked => { + root.numLockChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Keyboard layout changes") + checked: root.kbLayoutChanged + onToggled: checked => { + root.kbLayoutChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("VPN changes") + checked: root.vpnChanged + onToggled: checked => { + root.vpnChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Now playing") + checked: root.nowPlaying + onToggled: checked => { + root.nowPlaying = checked; + root.saveConfig(); + } + } + } + } + } + } + + component SectionPage: StyledFlickable { + id: sectionPage + + required property string title + property string subtitle: "" + default property alias contentItems: contentLayout.data + + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sectionPage + } + + ColumnLayout { + id: contentLayout + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true + text: sectionPage.title + font.pointSize: Tokens.font.size.extraLarge + font.weight: 600 + } + + StyledText { + Layout.fillWidth: true + Layout.bottomMargin: Tokens.spacing.small + text: sectionPage.subtitle + color: Colours.palette.m3outline + visible: text.length > 0 + wrapMode: Text.WordWrap + } + } + } + + component SectionNavButton: StyledRect { + id: navButton + + required property var section + property bool active: false + + signal clicked + + implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 + color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Tokens.rounding.normal + + Behavior on color { + CAnim {} + } + + StateLayer { + onClicked: navButton.clicked() + } + + RowLayout { + id: navRow + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: navButton.section.icon + fill: navButton.active ? 1 : 0 + color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.large + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: navButton.section.title + font.weight: navButton.active ? 500 : 400 + elide: Text.ElideRight + maximumLineCount: 1 + } + + StyledText { + Layout.fillWidth: true + text: navButton.section.description + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: "chevron_right" + opacity: navButton.active ? 1 : 0 + color: Colours.palette.m3primary + } + } + } +} diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml index 8678672df..9b16bfe59 100644 --- a/modules/controlcenter/state/BluetoothState.qml +++ b/modules/controlcenter/state/BluetoothState.qml @@ -1,5 +1,5 @@ -import Quickshell.Bluetooth import QtQuick +import Quickshell.Bluetooth QtObject { id: root diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index d12d17449..c80769ac5 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -2,24 +2,29 @@ pragma ComponentBehavior: Bound import ".." import "../components" +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config import qs.components +import qs.components.containers import qs.components.controls import qs.components.effects -import qs.components.containers import qs.services -import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root required property Session session + property string activeSection: "statusIcons" + property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false + property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false property bool clockShowIcon: Config.bar.clock.showIcon ?? true + property bool clockBackground: Config.bar.clock.background ?? false + property bool clockShowDate: Config.bar.clock.showDate ?? false property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true property int dragThreshold: Config.bar.dragThreshold ?? 20 @@ -38,56 +43,131 @@ Item { property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false - property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true + property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 + property bool workspacesPerMonitor: GlobalConfig.bar.workspaces.perMonitorWorkspaces ?? true property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true property bool scrollVolume: Config.bar.scrollActions.volume ?? true property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true property bool popoutTray: Config.bar.popouts.tray ?? true property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true - - anchors.fill: parent - - Component.onCompleted: { - if (Config.bar.entries) { - entriesModel.clear(); - for (let i = 0; i < Config.bar.entries.length; i++) { - const entry = Config.bar.entries[i]; - entriesModel.append({ - id: entry.id, - enabled: entry.enabled !== false - }); - } + property list monitorNames: Hypr.monitorNames() + property list excludedScreens: Config.bar.excludedScreens ?? [] + + readonly property var sections: [ + { + id: "statusIcons", + title: qsTr("Status Icons"), + description: qsTr("Bar indicator buttons"), + icon: "info" + }, + { + id: "workspaces", + title: qsTr("Workspaces"), + description: qsTr("Workspace button layout"), + icon: "workspaces" + }, + { + id: "scrollActions", + title: qsTr("Scroll Actions"), + description: qsTr("Wheel shortcuts"), + icon: "swap_vert" + }, + { + id: "clock", + title: qsTr("Clock"), + description: qsTr("Date and icon display"), + icon: "schedule" + }, + { + id: "behavior", + title: qsTr("Bar Behavior"), + description: qsTr("Visibility and dragging"), + icon: "dock_to_left" + }, + { + id: "activeWindow", + title: qsTr("Active Window"), + description: qsTr("Window title entry"), + icon: "select_window" + }, + { + id: "popouts", + title: qsTr("Popouts"), + description: qsTr("Hover panels"), + icon: "open_in_new" + }, + { + id: "tray", + title: qsTr("Tray Settings"), + description: qsTr("System tray style"), + icon: "apps" + }, + { + id: "monitors", + title: qsTr("Monitors"), + description: qsTr("Per-screen visibility"), + icon: "monitor" + } + ] + + function componentForSection(sectionId) { + switch (sectionId) { + case "workspaces": + return workspacesComponent; + case "scrollActions": + return scrollActionsComponent; + case "clock": + return clockComponent; + case "behavior": + return behaviorComponent; + case "activeWindow": + return activeWindowComponent; + case "popouts": + return popoutsComponent; + case "tray": + return trayComponent; + case "monitors": + return monitorsComponent; + case "statusIcons": + default: + return statusIconsComponent; } } function saveConfig(entryIndex, entryEnabled) { - Config.bar.clock.showIcon = root.clockShowIcon; - Config.bar.persistent = root.persistent; - Config.bar.showOnHover = root.showOnHover; - Config.bar.dragThreshold = root.dragThreshold; - Config.bar.status.showAudio = root.showAudio; - Config.bar.status.showMicrophone = root.showMicrophone; - Config.bar.status.showKbLayout = root.showKbLayout; - Config.bar.status.showNetwork = root.showNetwork; - Config.bar.status.showWifi = root.showWifi; - Config.bar.status.showBluetooth = root.showBluetooth; - Config.bar.status.showBattery = root.showBattery; - Config.bar.status.showLockStatus = root.showLockStatus; - Config.bar.tray.background = root.trayBackground; - Config.bar.tray.compact = root.trayCompact; - Config.bar.tray.recolour = root.trayRecolour; - Config.bar.workspaces.shown = root.workspacesShown; - Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; - Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; - Config.bar.workspaces.showWindows = root.workspacesShowWindows; - Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; - Config.bar.scrollActions.workspaces = root.scrollWorkspaces; - Config.bar.scrollActions.volume = root.scrollVolume; - Config.bar.scrollActions.brightness = root.scrollBrightness; - Config.bar.popouts.activeWindow = root.popoutActiveWindow; - Config.bar.popouts.tray = root.popoutTray; - Config.bar.popouts.statusIcons = root.popoutStatusIcons; + GlobalConfig.bar.activeWindow.compact = root.activeWindowCompact; + GlobalConfig.bar.activeWindow.inverted = root.activeWindowInverted; + GlobalConfig.bar.clock.background = root.clockBackground; + GlobalConfig.bar.clock.showDate = root.clockShowDate; + GlobalConfig.bar.clock.showIcon = root.clockShowIcon; + GlobalConfig.bar.persistent = root.persistent; + GlobalConfig.bar.showOnHover = root.showOnHover; + GlobalConfig.bar.dragThreshold = root.dragThreshold; + GlobalConfig.bar.status.showAudio = root.showAudio; + GlobalConfig.bar.status.showMicrophone = root.showMicrophone; + GlobalConfig.bar.status.showKbLayout = root.showKbLayout; + GlobalConfig.bar.status.showNetwork = root.showNetwork; + GlobalConfig.bar.status.showWifi = root.showWifi; + GlobalConfig.bar.status.showBluetooth = root.showBluetooth; + GlobalConfig.bar.status.showBattery = root.showBattery; + GlobalConfig.bar.status.showLockStatus = root.showLockStatus; + GlobalConfig.bar.tray.background = root.trayBackground; + GlobalConfig.bar.tray.compact = root.trayCompact; + GlobalConfig.bar.tray.recolour = root.trayRecolour; + GlobalConfig.bar.workspaces.shown = root.workspacesShown; + GlobalConfig.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; + GlobalConfig.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; + GlobalConfig.bar.workspaces.showWindows = root.workspacesShowWindows; + GlobalConfig.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; + GlobalConfig.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; + GlobalConfig.bar.scrollActions.workspaces = root.scrollWorkspaces; + GlobalConfig.bar.scrollActions.volume = root.scrollVolume; + GlobalConfig.bar.scrollActions.brightness = root.scrollBrightness; + GlobalConfig.bar.popouts.activeWindow = root.popoutActiveWindow; + GlobalConfig.bar.popouts.tray = root.popoutTray; + GlobalConfig.bar.popouts.statusIcons = root.popoutStatusIcons; + GlobalConfig.bar.excludedScreens = root.excludedScreens; const entries = []; for (let i = 0; i < entriesModel.count; i++) { @@ -101,547 +181,718 @@ Item { enabled: enabled }); } - Config.bar.entries = entries; - Config.save(); + GlobalConfig.bar.entries = entries; + } + + anchors.fill: parent + + Component.onCompleted: { + if (Config.bar.entries) { + entriesModel.clear(); + for (let i = 0; i < Config.bar.entries.length; i++) { + const entry = Config.bar.entries[i]; + entriesModel.append({ + id: entry.id, + enabled: entry.enabled !== false + }); + } + } } ListModel { id: entriesModel } - ClippingRectangle { - id: taskbarClippingRect + SplitPaneLayout { anchors.fill: parent - anchors.margins: Appearance.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Appearance.padding.normal - - radius: taskbarBorder.innerRadius - color: "transparent" + leftWidthRatio: 0.32 + leftMinimumWidth: 300 - Loader { - id: taskbarLoader + leftContent: Component { + StyledFlickable { + id: leftFlickable - anchors.fill: parent - anchors.margins: Appearance.padding.large + Appearance.padding.normal - anchors.leftMargin: Appearance.padding.large - anchors.rightMargin: Appearance.padding.large + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContentLayout.height - sourceComponent: taskbarContentComponent - } - } + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } - InnerBorder { - id: taskbarBorder - leftThickness: 0 - rightThickness: Appearance.padding.normal - } + ColumnLayout { + id: leftContentLayout - Component { - id: taskbarContentComponent + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.normal - StyledFlickable { - id: sidebarFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller - StyledScrollBar.vertical: StyledScrollBar { - flickable: sidebarFlickable - } + StyledText { + text: qsTr("Taskbar") + font.pointSize: Tokens.font.size.large + font.weight: 500 + } - ColumnLayout { - id: sidebarLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + Item { + Layout.fillWidth: true + } + } - spacing: Appearance.spacing.normal + Repeater { + model: root.sections - RowLayout { - spacing: Appearance.spacing.smaller + delegate: SectionNavButton { + required property var modelData - StyledText { - text: qsTr("Taskbar") - font.pointSize: Appearance.font.size.large - font.weight: 500 + Layout.fillWidth: true + section: modelData + active: root.activeSection === modelData.id + onClicked: root.activeSection = modelData.id + } } } + } + } - SectionContainer { - Layout.fillWidth: true - alignTop: true + rightContent: Component { + Item { + id: rightPaneItem - StyledText { - text: qsTr("Status Icons") - font.pointSize: Appearance.font.size.normal - } + property string paneId: root.activeSection + property Component targetComponent: root.componentForSection(root.activeSection) + property Component nextComponent: root.componentForSection(root.activeSection) - ConnectedButtonGroup { - rootItem: root + onPaneIdChanged: { + nextComponent = root.componentForSection(root.activeSection); + } - options: [ - { - label: qsTr("Speakers"), - propertyName: "showAudio", - onToggled: function (checked) { - root.showAudio = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Microphone"), - propertyName: "showMicrophone", - onToggled: function (checked) { - root.showMicrophone = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Keyboard"), - propertyName: "showKbLayout", - onToggled: function (checked) { - root.showKbLayout = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Network"), - propertyName: "showNetwork", - onToggled: function (checked) { - root.showNetwork = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Wifi"), - propertyName: "showWifi", - onToggled: function (checked) { - root.showWifi = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Bluetooth"), - propertyName: "showBluetooth", - onToggled: function (checked) { - root.showBluetooth = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Battery"), - propertyName: "showBattery", - onToggled: function (checked) { - root.showBattery = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Capslock"), - propertyName: "showLockStatus", - onToggled: function (checked) { - root.showLockStatus = checked; - root.saveConfig(); - } + Loader { + id: rightLoader + + anchors.fill: parent + asynchronous: true + opacity: 1 + scale: 1 + transformOrigin: Item.Center + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent } ] } } + } + } + } - RowLayout { - id: mainRowLayout - Layout.fillWidth: true - spacing: Appearance.spacing.normal + Component { + id: statusIconsComponent + + SectionPage { + title: qsTr("Status Icons") + subtitle: qsTr("Choose which status controls appear in the taskbar.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ConnectedButtonGroup { + rootItem: root + options: [ + { + label: qsTr("Speakers"), + propertyName: "showAudio", + onToggled: function (checked) { + root.showAudio = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Microphone"), + propertyName: "showMicrophone", + onToggled: function (checked) { + root.showMicrophone = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Keyboard"), + propertyName: "showKbLayout", + onToggled: function (checked) { + root.showKbLayout = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Network"), + propertyName: "showNetwork", + onToggled: function (checked) { + root.showNetwork = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Wifi"), + propertyName: "showWifi", + onToggled: function (checked) { + root.showWifi = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Bluetooth"), + propertyName: "showBluetooth", + onToggled: function (checked) { + root.showBluetooth = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Battery"), + propertyName: "showBattery", + onToggled: function (checked) { + root.showBattery = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Capslock"), + propertyName: "showLockStatus", + onToggled: function (checked) { + root.showLockStatus = checked; + root.saveConfig(); + } + } + ] + } + } + } + } - ColumnLayout { - id: leftColumnLayout - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + Component { + id: workspacesComponent + + SectionPage { + title: qsTr("Workspaces") + subtitle: qsTr("Tune workspace buttons and window indicators.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SpinSettingRow { + label: qsTr("Shown") + min: 1 + max: 20 + value: root.workspacesShown + onModified: value => { + root.workspacesShown = value; + root.saveConfig(); + } + } - SectionContainer { - Layout.fillWidth: true - alignTop: true + SwitchRow { + label: qsTr("Active indicator") + checked: root.workspacesActiveIndicator + onToggled: checked => { + root.workspacesActiveIndicator = checked; + root.saveConfig(); + } + } - StyledText { - text: qsTr("Workspaces") - font.pointSize: Appearance.font.size.normal - } + SwitchRow { + label: qsTr("Occupied background") + checked: root.workspacesOccupiedBg + onToggled: checked => { + root.workspacesOccupiedBg = checked; + root.saveConfig(); + } + } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShownRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Shown") - } + SwitchRow { + label: qsTr("Show windows") + checked: root.workspacesShowWindows + onToggled: checked => { + root.workspacesShowWindows = checked; + root.saveConfig(); + } + } - CustomSpinBox { - min: 1 - max: 20 - value: root.workspacesShown - onValueModified: value => { - root.workspacesShown = value; - root.saveConfig(); - } - } - } - } + SpinSettingRow { + label: qsTr("Max window icons") + min: 0 + max: 20 + value: root.workspacesMaxWindowIcons + onModified: value => { + root.workspacesMaxWindowIcons = value; + root.saveConfig(); + } + } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesActiveIndicatorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Active indicator") - } + SwitchRow { + label: qsTr("Per monitor workspaces") + checked: root.workspacesPerMonitor + onToggled: checked => { + root.workspacesPerMonitor = checked; + root.saveConfig(); + } + } + } + } + } - StyledSwitch { - checked: root.workspacesActiveIndicator - onToggled: { - root.workspacesActiveIndicator = checked; - root.saveConfig(); - } - } - } + Component { + id: scrollActionsComponent + + SectionPage { + title: qsTr("Scroll Actions") + subtitle: qsTr("Choose what responds to wheel input on the bar.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ConnectedButtonGroup { + rootItem: root + options: [ + { + label: qsTr("Workspaces"), + propertyName: "scrollWorkspaces", + onToggled: function (checked) { + root.scrollWorkspaces = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Volume"), + propertyName: "scrollVolume", + onToggled: function (checked) { + root.scrollVolume = checked; + root.saveConfig(); } + }, + { + label: qsTr("Brightness"), + propertyName: "scrollBrightness", + onToggled: function (checked) { + root.scrollBrightness = checked; + root.saveConfig(); + } + } + ] + } + } + } + } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesOccupiedBgRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Occupied background") - } + Component { + id: clockComponent + + SectionPage { + title: qsTr("Clock") + subtitle: qsTr("Adjust the clock entry shown on the bar.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SwitchRow { + label: qsTr("Background") + checked: root.clockBackground + onToggled: checked => { + root.clockBackground = checked; + root.saveConfig(); + } + } - StyledSwitch { - checked: root.workspacesOccupiedBg - onToggled: { - root.workspacesOccupiedBg = checked; - root.saveConfig(); - } - } - } - } + SwitchRow { + label: qsTr("Show date") + checked: root.clockShowDate + onToggled: checked => { + root.clockShowDate = checked; + root.saveConfig(); + } + } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShowWindowsRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show windows") - } + SwitchRow { + label: qsTr("Show clock icon") + checked: root.clockShowIcon + onToggled: checked => { + root.clockShowIcon = checked; + root.saveConfig(); + } + } + } + } + } - StyledSwitch { - checked: root.workspacesShowWindows - onToggled: { - root.workspacesShowWindows = checked; - root.saveConfig(); - } - } - } - } + Component { + id: behaviorComponent + + SectionPage { + title: qsTr("Bar Behavior") + subtitle: qsTr("Control when the bar appears and how drag reveal feels.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SwitchRow { + label: qsTr("Persistent") + checked: root.persistent + onToggled: checked => { + root.persistent = checked; + root.saveConfig(); + } + } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 - radius: Appearance.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesPerMonitorRow - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Per monitor workspaces") - } + SwitchRow { + label: qsTr("Show on hover") + checked: root.showOnHover + onToggled: checked => { + root.showOnHover = checked; + root.saveConfig(); + } + } + } - StyledSwitch { - checked: root.workspacesPerMonitor - onToggled: { - root.workspacesPerMonitor = checked; - root.saveConfig(); - } - } - } - } - } + SectionContainer { + Layout.fillWidth: true + contentSpacing: Tokens.spacing.normal - SectionContainer { - Layout.fillWidth: true - alignTop: true + SliderInput { + Layout.fillWidth: true + label: qsTr("Drag threshold") + value: root.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { + bottom: 0 + top: 100 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + onValueModified: newValue => { + root.dragThreshold = Math.round(newValue); + root.saveConfig(); + } + } + } + } + } - StyledText { - text: qsTr("Scroll Actions") - font.pointSize: Appearance.font.size.normal - } + Component { + id: activeWindowComponent + + SectionPage { + title: qsTr("Active Window") + subtitle: qsTr("Configure the active window entry in the taskbar.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SwitchRow { + label: qsTr("Compact") + checked: root.activeWindowCompact + onToggled: checked => { + root.activeWindowCompact = checked; + root.saveConfig(); + } + } - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Workspaces"), - propertyName: "scrollWorkspaces", - onToggled: function (checked) { - root.scrollWorkspaces = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Volume"), - propertyName: "scrollVolume", - onToggled: function (checked) { - root.scrollVolume = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Brightness"), - propertyName: "scrollBrightness", - onToggled: function (checked) { - root.scrollBrightness = checked; - root.saveConfig(); - } - } - ] - } - } + SwitchRow { + label: qsTr("Inverted") + checked: root.activeWindowInverted + onToggled: checked => { + root.activeWindowInverted = checked; + root.saveConfig(); } + } + } + } + } - ColumnLayout { - id: middleColumnLayout - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + Component { + id: popoutsComponent + + SectionPage { + title: qsTr("Popouts") + subtitle: qsTr("Select which taskbar entries open hover popouts.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + SwitchRow { + label: qsTr("Active window") + checked: root.popoutActiveWindow + onToggled: checked => { + root.popoutActiveWindow = checked; + root.saveConfig(); + } + } - SectionContainer { - Layout.fillWidth: true - alignTop: true + SwitchRow { + label: qsTr("Tray") + checked: root.popoutTray + onToggled: checked => { + root.popoutTray = checked; + root.saveConfig(); + } + } - StyledText { - text: qsTr("Clock") - font.pointSize: Appearance.font.size.normal - } + SwitchRow { + label: qsTr("Status icons") + checked: root.popoutStatusIcons + onToggled: checked => { + root.popoutStatusIcons = checked; + root.saveConfig(); + } + } + } + } + } - SwitchRow { - label: qsTr("Show clock icon") - checked: root.clockShowIcon - onToggled: checked => { - root.clockShowIcon = checked; - root.saveConfig(); - } + Component { + id: trayComponent + + SectionPage { + title: qsTr("Tray Settings") + subtitle: qsTr("Change the system tray presentation.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ConnectedButtonGroup { + rootItem: root + options: [ + { + label: qsTr("Background"), + propertyName: "trayBackground", + onToggled: function (checked) { + root.trayBackground = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Compact"), + propertyName: "trayCompact", + onToggled: function (checked) { + root.trayCompact = checked; + root.saveConfig(); + } + }, + { + label: qsTr("Recolour"), + propertyName: "trayRecolour", + onToggled: function (checked) { + root.trayRecolour = checked; + root.saveConfig(); } } + ] + } + } + } + } - SectionContainer { - Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Bar Behavior") - font.pointSize: Appearance.font.size.normal - } + Component { + id: monitorsComponent + + SectionPage { + title: qsTr("Monitors") + subtitle: qsTr("Choose which monitors show the taskbar.") + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + ConnectedButtonGroup { + rootItem: root + rows: Math.max(1, Math.ceil(root.monitorNames.length / 3)) + options: root.monitorNames.map(e => ({ + label: qsTr(e), + propertyName: `monitor${e}`, + onToggled: function (_) { + const screens = []; + for (const screen of root.excludedScreens) + screens.push(screen); + + const addedBack = screens.includes(e); + if (addedBack) { + const index = screens.indexOf(e); + if (index !== -1) + screens.splice(index, 1); + } else if (!screens.includes(e)) { + screens.push(e); + } - SwitchRow { - label: qsTr("Persistent") - checked: root.persistent - onToggled: checked => { - root.persistent = checked; + root.excludedScreens = screens; root.saveConfig(); - } - } + }, + state: !Strings.testRegexList(root.excludedScreens, e) + })) + } + } + } + } - SwitchRow { - label: qsTr("Show on hover") - checked: root.showOnHover - onToggled: checked => { - root.showOnHover = checked; - root.saveConfig(); - } - } + component SectionPage: StyledFlickable { + id: sectionPage - SectionContainer { - contentSpacing: Appearance.spacing.normal + required property string title + property string subtitle: "" + default property alias contentItems: contentLayout.data - SliderInput { - Layout.fillWidth: true + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height - label: qsTr("Drag threshold") - value: root.dragThreshold - from: 0 - to: 100 - suffix: "px" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) + StyledScrollBar.vertical: StyledScrollBar { + flickable: sectionPage + } - onValueModified: newValue => { - root.dragThreshold = Math.round(newValue); - root.saveConfig(); - } - } - } - } - } + ColumnLayout { + id: contentLayout - ColumnLayout { - id: rightColumnLayout - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Appearance.spacing.normal + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Tokens.spacing.normal - SectionContainer { - Layout.fillWidth: true - alignTop: true + StyledText { + Layout.fillWidth: true + text: sectionPage.title + font.pointSize: Tokens.font.size.extraLarge + font.weight: 600 + } - StyledText { - text: qsTr("Popouts") - font.pointSize: Appearance.font.size.normal - } + StyledText { + Layout.fillWidth: true + Layout.bottomMargin: Tokens.spacing.small + text: sectionPage.subtitle + color: Colours.palette.m3outline + visible: text.length > 0 + wrapMode: Text.WordWrap + } + } + } - SwitchRow { - label: qsTr("Active window") - checked: root.popoutActiveWindow - onToggled: checked => { - root.popoutActiveWindow = checked; - root.saveConfig(); - } - } + component SectionNavButton: StyledRect { + id: navButton - SwitchRow { - label: qsTr("Tray") - checked: root.popoutTray - onToggled: checked => { - root.popoutTray = checked; - root.saveConfig(); - } - } + required property var section + property bool active: false - SwitchRow { - label: qsTr("Status icons") - checked: root.popoutStatusIcons - onToggled: checked => { - root.popoutStatusIcons = checked; - root.saveConfig(); - } - } - } + signal clicked - SectionContainer { - Layout.fillWidth: true - alignTop: true + implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 + color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Tokens.rounding.normal - StyledText { - text: qsTr("Tray Settings") - font.pointSize: Appearance.font.size.normal - } + Behavior on color { + CAnim {} + } - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Background"), - propertyName: "trayBackground", - onToggled: function (checked) { - root.trayBackground = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Compact"), - propertyName: "trayCompact", - onToggled: function (checked) { - root.trayCompact = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Recolour"), - propertyName: "trayRecolour", - onToggled: function (checked) { - root.trayRecolour = checked; - root.saveConfig(); - } - } - ] - } - } - } + StateLayer { + onClicked: navButton.clicked() + } + + RowLayout { + id: navRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: navButton.section.icon + fill: navButton.active ? 1 : 0 + color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.large + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: navButton.section.title + font.weight: navButton.active ? 500 : 400 + elide: Text.ElideRight + maximumLineCount: 1 } + + StyledText { + Layout.fillWidth: true + text: navButton.section.description + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: "chevron_right" + opacity: navButton.active ? 1 : 0 + color: Colours.palette.m3primary + } + } + } + + component SpinSettingRow: StyledRect { + id: spinRow + + required property string label + property int min: 0 + property int max: 100 + property int value: 0 + + signal modified(int value) + + Layout.fillWidth: true + implicitHeight: rowLayout.implicitHeight + Tokens.padding.large * 2 + radius: Tokens.rounding.normal + color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + + RowLayout { + id: rowLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true + text: spinRow.label + } + + CustomSpinBox { + min: spinRow.min + max: spinRow.max + value: spinRow.value + onValueModified: value => spinRow.modified(value) } } } diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml index f95b7d782..95d2a08a7 100644 --- a/modules/dashboard/Content.qml +++ b/modules/dashboard/Content.qml @@ -1,19 +1,59 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.components.filedialog -import qs.config -import Quickshell -import Quickshell.Widgets import QtQuick import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config +import qs.components +import qs.components.filedialog Item { id: root - required property PersistentProperties visibilities - required property PersistentProperties state + required property DrawerVisibilities visibilities + readonly property bool needsKeyboard: { + const count = repeater.count; + for (let i = 0; i < count; i++) { + const item = repeater.itemAt(i) as Loader; + if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) + return true; + } + return false; + } + required property DashboardState dashState required property FileDialog facePicker + + readonly property var dashboardTabs: { + const allTabs = [ + { + component: dashComponent, + iconName: "dashboard", + text: qsTr("Dashboard"), + enabled: Config.dashboard.showDashboard + }, + { + component: mediaComponent, + iconName: "queue_music", + text: qsTr("Media"), + enabled: Config.dashboard.showMedia + }, + { + component: performanceComponent, + iconName: "speed", + text: qsTr("Performance"), + enabled: Config.dashboard.showPerformance && (Config.dashboard.performance.showCpu || Config.dashboard.performance.showGpu || Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork || Config.dashboard.performance.showBattery) + }, + { + component: weatherComponent, + iconName: "cloud", + text: qsTr("Weather"), + enabled: Config.dashboard.showWeather + } + ]; + return allTabs.filter(tab => tab.enabled); + } + readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2 @@ -26,11 +66,12 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Appearance.padding.normal - anchors.margins: Appearance.padding.large + anchors.topMargin: Tokens.padding.normal + anchors.margins: Tokens.padding.large nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 - state: root.state + dashState: root.dashState + tabs: root.dashboardTabs } ClippingRectangle { @@ -40,84 +81,116 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: "transparent" Flickable { id: view - readonly property int currentIndex: root.state.currentTab - readonly property Item currentItem: row.children[currentIndex] + readonly property int currentIndex: root.dashState.currentTab + readonly property Item currentItem: { + repeater.count; // Trigger update on count change + return repeater.itemAt(currentIndex); + } anchors.fill: parent flickableDirection: Flickable.HorizontalFlick - implicitWidth: currentItem.implicitWidth - implicitHeight: currentItem.implicitHeight + implicitWidth: currentItem?.implicitWidth ?? 0 + implicitHeight: currentItem?.implicitHeight ?? 0 - contentX: currentItem.x + contentX: currentItem?.x ?? 0 contentWidth: row.implicitWidth contentHeight: row.implicitHeight onContentXChanged: { - if (!moving) + if (!moving || !currentItem) return; const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 2) - root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); + root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 2) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); } onDragEnded: { + if (!currentItem) + return; + const x = contentX - currentItem.x; if (x > currentItem.implicitWidth / 10) - root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); + root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); else if (x < -currentItem.implicitWidth / 10) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); + root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); else - contentX = Qt.binding(() => currentItem.x); + contentX = Qt.binding(() => currentItem?.x ?? 0); } RowLayout { id: row - Pane { - index: 0 - sourceComponent: Dash { - visibilities: root.visibilities - state: root.state - facePicker: root.facePicker + Repeater { + id: repeater + + model: ScriptModel { + values: root.dashboardTabs } - } - Pane { - index: 1 - sourceComponent: Media { - visibilities: root.visibilities + delegate: Loader { + id: paneLoader + + required property int index + required property var modelData + + Layout.alignment: Qt.AlignTop + + sourceComponent: modelData.component + + Component.onCompleted: active = Qt.binding(() => { + if (index === view.currentIndex) + return true; + const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); + const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); + return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); + }) } } + } - Pane { - index: 2 - sourceComponent: Performance {} - } + Component { + id: dashComponent - Pane { - index: 3 - sourceComponent: Weather {} + Dash { + visibilities: root.visibilities + dashState: root.dashState + facePicker: root.facePicker } + } + + Component { + id: mediaComponent - Pane { - index: 4 - sourceComponent: Monitors {} + MediaWrapper { + visibilities: root.visibilities } } + Component { + id: performanceComponent + + Performance {} + } + + Component { + id: weatherComponent + + WeatherTab {} + } + Behavior on contentX { Anim {} } @@ -126,32 +199,13 @@ Item { Behavior on implicitWidth { Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.EmphasizedLarge } } Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.EmphasizedLarge } } - - component Pane: Loader { - id: pane - - required property int index - - Layout.alignment: Qt.AlignTop - - Component.onCompleted: active = Qt.binding(() => { - // Always keep current tab loaded - if (pane.index === view.currentIndex) - return true; - const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); - const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); - return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); - }) - } } diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml index 71e224fbe..6785ede8a 100644 --- a/modules/dashboard/Dash.qml +++ b/modules/dashboard/Dash.qml @@ -1,20 +1,19 @@ +import "dash" +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.filedialog import qs.services -import qs.config -import "dash" -import Quickshell -import QtQuick.Layouts GridLayout { id: root - required property PersistentProperties visibilities - required property PersistentProperties state + required property DrawerVisibilities visibilities + required property DashboardState dashState required property FileDialog facePicker - rowSpacing: Appearance.spacing.normal - columnSpacing: Appearance.spacing.normal + rowSpacing: Tokens.spacing.normal + columnSpacing: Tokens.spacing.normal Rect { Layout.column: 2 @@ -22,13 +21,12 @@ GridLayout { Layout.preferredWidth: user.implicitWidth Layout.preferredHeight: user.implicitHeight - radius: Appearance.rounding.large + radius: Tokens.rounding.large User { id: user visibilities: root.visibilities - state: root.state facePicker: root.facePicker } } @@ -36,12 +34,12 @@ GridLayout { Rect { Layout.row: 0 Layout.columnSpan: 2 - Layout.preferredWidth: Config.dashboard.sizes.weatherWidth + Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth Layout.fillHeight: true - radius: Appearance.rounding.large * 1.5 + radius: Tokens.rounding.large * 1.5 - Weather {} + SmallWeather {} } Rect { @@ -49,7 +47,7 @@ GridLayout { Layout.preferredWidth: dateTime.implicitWidth Layout.fillHeight: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal DateTime { id: dateTime @@ -63,12 +61,12 @@ GridLayout { Layout.fillWidth: true Layout.preferredHeight: calendar.implicitHeight - radius: Appearance.rounding.large + radius: Tokens.rounding.large Calendar { id: calendar - state: root.state + dashState: root.dashState } } @@ -78,7 +76,7 @@ GridLayout { Layout.preferredWidth: resources.implicitWidth Layout.fillHeight: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Resources { id: resources @@ -92,7 +90,7 @@ GridLayout { Layout.preferredWidth: media.implicitWidth Layout.fillHeight: true - radius: Appearance.rounding.large * 2 + radius: Tokens.rounding.large * 2 Media { id: media diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml new file mode 100644 index 000000000..1e5461f56 --- /dev/null +++ b/modules/dashboard/LyricMenu.qml @@ -0,0 +1,412 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.services + +StyledRect { + id: root + + required property real contentHeight + + function searchCandidates(title, artist) { + LyricsService.currentRequestId++; + LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); + } + + implicitHeight: contentHeight + + radius: Tokens.rounding.large + color: Colours.tPalette.m3surfaceContainer + + Loader { + asynchronous: true + anchors.fill: parent + active: root.height > 0 + + sourceComponent: ColumnLayout { + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal + + // Header: icon, backend selector, refresh, toggle + RowLayout { + Layout.fillWidth: true + spacing: Tokens.padding.small + + MaterialIcon { + text: "lyrics" + fill: 1 + color: Colours.palette.m3primary + font.pointSize: Tokens.spacing.large + } + + Rectangle { + Layout.preferredHeight: 24 + Layout.preferredWidth: 80 + radius: Tokens.rounding.small + color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) + + StyledText { + anchors.centerIn: parent + text: LyricsService.preferredBackend + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3primary + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + const backends = ["Auto", "Local", "NetEase"]; + const currentIndex = backends.indexOf(LyricsService.preferredBackend); + const nextIndex = (currentIndex + 1) % backends.length; + LyricsService.preferredBackend = backends[nextIndex]; + LyricsService.loadLyrics(); + } + } + } + + Rectangle { + Layout.preferredHeight: 24 + Layout.preferredWidth: 60 + radius: Tokens.rounding.small + visible: LyricsService.preferredBackend === "Auto" + color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) + + StyledText { + anchors.centerIn: parent + text: LyricsService.backend + font.pointSize: Tokens.font.size.small + color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary + } + } + + Item { + Layout.fillWidth: true + } + + IconButton { + icon: "refresh" + type: IconButton.Text + onClicked: LyricsService.loadLyrics() + } + + StyledSwitch { + checked: LyricsService.lyricsVisible + onToggled: LyricsService.toggleVisibility() + } + } + + StyledText { + Layout.fillWidth: true + text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 + } + + // Local file info (shown in Local mode) + Rectangle { + Layout.fillWidth: true + Layout.preferredHeight: 48 + visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 + radius: Tokens.rounding.small + color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) + + ColumnLayout { + anchors.fill: parent + anchors.margins: Tokens.padding.small + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: { + const path = LyricsService.loadedLocalFile; + const parts = path.split('/'); + return parts[parts.length - 1]; + } + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3tertiary + elide: Text.ElideMiddle + } + + StyledText { + Layout.fillWidth: true + text: { + const path = LyricsService.loadedLocalFile; + const parts = path.split('/'); + if (parts.length > 2) { + return parts.slice(-3, -1).join('/'); + } + return ""; + } + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3outline + elide: Text.ElideMiddle + } + } + } + + // Candidates list + Loader { + Layout.fillWidth: true + Layout.fillHeight: true + + active: LyricsService.preferredBackend !== "Local" + + sourceComponent: ListView { + id: candidatesView + + model: LyricsService.candidatesModel + clip: true + spacing: Tokens.spacing.small + visible: LyricsService.candidatesModel.count > 0 + opacity: visible ? 1 : 0 + + delegate: Item { + id: delegateRoot + + required property real id + required property string title + required property string artist + + property bool hovered: false + property bool pressed: false + + width: ListView.view.width * 0.98 + height: 70 + + anchors.horizontalCenter: parent?.horizontalCenter + scale: hovered ? 1.02 : 1.0 + + Behavior on scale { + NumberAnimation { + duration: Tokens.anim.durations.small + easing.type: Easing.OutCubic + } + } + + Rectangle { + id: background + + anchors.fill: parent + radius: Tokens.rounding.small + + color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) + + border.width: delegateRoot.hovered ? 1 : 0 + border.color: Colours.palette.m3primary + + Behavior on color { + ColorAnimation { + duration: Tokens.anim.durations.small + } + } + Behavior on border.width { + NumberAnimation { + duration: Tokens.anim.durations.small + } + } + } + + MouseArea { + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + + onEntered: delegateRoot.hovered = true + onExited: delegateRoot.hovered = false + onPressed: delegateRoot.pressed = true + onReleased: delegateRoot.pressed = false + onClicked: LyricsService.selectCandidate(delegateRoot.id) + } + + Row { + anchors.fill: parent + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.small + + // Active indicator bar + Rectangle { + width: 4 + height: parent.height * 0.6 + radius: 2 + anchors.verticalCenter: parent.verticalCenter + color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" + + Behavior on color { + ColorAnimation { + duration: Tokens.anim.durations.small + } + } + } + + Column { + anchors.verticalCenter: parent.verticalCenter + width: parent.width - 30 + spacing: 4 + + Text { + text: delegateRoot.title + font.pointSize: Tokens.font.size.normal + font.bold: true + color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface + width: parent.width + elide: Text.ElideRight + + Behavior on color { + ColorAnimation { + duration: Tokens.anim.durations.small + } + } + } + + Text { + text: delegateRoot.artist + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + elide: Text.ElideRight + } + } + } + } + } + } + + Item { + Layout.fillHeight: true + visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local" + } + + // Manual search + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.padding.small + + StyledText { + Layout.fillWidth: true + text: "Manual Search" + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + elide: Text.ElideRight + } + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.padding.small + + StyledInputField { + id: searchTitle + + Layout.fillWidth: true + horizontalAlignment: TextInput.AlignLeft + + Binding { + target: searchTitle + property: "text" + value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") + } + } + + StyledInputField { + id: searchArtist + + Layout.fillWidth: true + horizontalAlignment: TextInput.AlignLeft + + Binding { + target: searchArtist + property: "text" + value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") + } + } + + IconButton { + icon: "search" + onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) + } + } + } + + // Offset controls + RowLayout { + Layout.fillWidth: true + spacing: Tokens.padding.small + + MaterialIcon { + text: "contrast_square" + font.pointSize: Tokens.font.size.large + color: Colours.palette.m3secondary + } + + StyledText { + text: "Offset" + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.normal + } + + Item { + Layout.fillWidth: true + } + + IconButton { + icon: "remove" + type: IconButton.Text + onClicked: { + LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); + LyricsService.savePrefs(); + } + } + + TextInput { + id: offsetInput + + horizontalAlignment: TextInput.AlignHCenter + color: Colours.palette.m3secondary + font.pointSize: Tokens.font.size.normal + selectByMouse: true + text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" + onEditingFinished: { + let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); + let val = parseFloat(cleaned); + if (!isNaN(val)) { + LyricsService.offset = parseFloat(val.toFixed(1)); + LyricsService.savePrefs(); + } else { + offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; + } + } + + Binding { + target: offsetInput + property: "text" + value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" + when: !offsetInput.activeFocus + } + + Connections { + function onCurrentRequestIdChanged() { + offsetInput.focus = false; + } + + target: LyricsService + } + } + + IconButton { + icon: "add" + type: IconButton.Text + onClicked: { + LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); + LyricsService.savePrefs(); + } + } + } + } + } +} diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml new file mode 100644 index 000000000..c0188304d --- /dev/null +++ b/modules/dashboard/LyricsView.qml @@ -0,0 +1,112 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Caelestia.Config +import qs.components +import qs.components.containers +import qs.services + +StyledListView { + id: root + + readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 + + clip: true + model: LyricsService.model + currentIndex: LyricsService.currentIndex + visible: lyricsActuallyVisible || hideTimer.running + preferredHighlightBegin: height / 2 - 30 + preferredHighlightEnd: height / 2 + 30 + highlightRangeMode: ListView.ApplyRange + highlightFollowsCurrentItem: true + highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Tokens.anim.durations.normal + layer.enabled: true + layer.effect: ShaderEffect { + required property Item source + property real fadeMargin: 0.5 + + fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") + } + onLyricsActuallyVisibleChanged: { + if (!lyricsActuallyVisible) + hideTimer.restart(); + } + onModelChanged: { + if (model && model.count > 0) { + Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); + } + } + delegate: Item { + id: delegateRoot + + required property string lyricLine + required property real time + required property int index + readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 + property bool isCurrent: ListView.isCurrentItem + + width: ListView.view.width + height: hasContent ? (lyricText.contentHeight + Tokens.spacing.large) : 0 + + MultiEffect { + id: effect + + anchors.fill: lyricText + source: lyricText + scale: lyricText.scale + enabled: delegateRoot.isCurrent + visible: delegateRoot.isCurrent + + blurEnabled: true + blur: 0.4 + + shadowEnabled: true + shadowColor: Colours.palette.m3primary + shadowOpacity: 0.5 + shadowBlur: 0.6 + shadowHorizontalOffset: 0 + shadowVerticalOffset: 0 + + autoPaddingEnabled: true + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) + } + + Text { + id: lyricText + + text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" + width: parent.width * 0.85 + anchors.centerIn: parent + horizontalAlignment: Text.AlignHCenter + wrapMode: Text.WordWrap + font.pointSize: Tokens.font.size.normal + color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font.bold: delegateRoot.isCurrent + scale: delegateRoot.isCurrent ? 1.15 : 1.0 + + Behavior on color { + CAnim { + duration: Tokens.anim.durations.small + } + } + Behavior on scale { + Anim { + type: Anim.StandardSmall + } + } + } + } + + Timer { + id: hideTimer + + interval: 300 // long enough to bridge the track switch gap + running: false + repeat: false + } +} diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml index 37d12263a..367d9e98a 100644 --- a/modules/dashboard/Media.qml +++ b/modules/dashboard/Media.qml @@ -1,27 +1,33 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import Quickshell +import Quickshell.Services.Mpris +import Caelestia.Config +import Caelestia.Services import qs.components -import qs.components.effects import qs.components.controls import qs.services import qs.utils -import qs.config -import Caelestia.Services -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Mpris -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities + readonly property bool needsKeyboard: lyricMenuOpen + + readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Tokens.sizes.dashboard.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 + readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight + + property bool lyricMenuOpen: false + property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 + property bool lyricsShowingDebounced: false property real playerProgress: { const active = Players.active; - return active?.length ? active.position / active.length : 0; + return active?.length ? (active.position % active.length) / active.length : 0; } function lengthStr(length: int): string { @@ -37,21 +43,54 @@ Item { return `${mins}:${secs}`; } - implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 - implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 + onLyricsShowingChanged: { + if (lyricsShowing) { + lyricsHideDelay.stop(); + lyricsShowingDebounced = true; + } else { + lyricsHideDelay.restart(); + } + } + + implicitWidth: cover.implicitWidth + Tokens.sizes.dashboard.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 + implicitHeight: nonAnimHeight + + Behavior on implicitHeight { + Anim {} + } Behavior on playerProgress { Anim { - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } } Timer { running: Players.active?.isPlaying ?? false - interval: Config.dashboard.mediaUpdateInterval + interval: GlobalConfig.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true - onTriggered: Players.active?.positionChanged() + onTriggered: { + if (!Players.active) + return; + LyricsService.updatePosition(); + Players.active?.positionChanged(); + } + } + + Timer { + id: lyricsHideDelay + + interval: 300 + repeat: false + } + + Connections { + function onTriggered() { + root.lyricsShowingDebounced = false; + } + + target: lyricsHideDelay } ServiceRef { @@ -67,12 +106,12 @@ Item { readonly property real centerX: width / 2 readonly property real centerY: height / 2 - readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small - readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small + readonly property real innerX: cover.implicitWidth / 2 + Tokens.spacing.small + readonly property real innerY: cover.implicitHeight / 2 + Tokens.spacing.small property color colour: Colours.palette.m3primary anchors.fill: cover - anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize + anchors.margins: -Tokens.sizes.dashboard.mediaVisualiserSize asynchronous: true preferredRendererType: Shape.CurveRenderer @@ -83,7 +122,7 @@ Item { id: visualiserBars model: Array.from({ - length: Config.services.visualiserBars + length: GlobalConfig.services.visualiserBars }, (_, i) => i) ShapePath { @@ -92,13 +131,13 @@ Item { required property int modelData readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) - readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars - readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize + readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars + readonly property real magnitude: value * root.Tokens.sizes.dashboard.mediaVisualiserSize readonly property real cos: Math.cos(angle) readonly property real sin: Math.sin(angle) - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4 + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 strokeColor: Colours.palette.m3primary startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos @@ -120,10 +159,10 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize + anchors.leftMargin: Tokens.padding.large + Tokens.sizes.dashboard.mediaVisualiserSize - implicitWidth: Config.dashboard.sizes.mediaCoverArtSize - implicitHeight: Config.dashboard.sizes.mediaCoverArtSize + implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize + implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize color: Colours.tPalette.m3surfaceContainerHigh radius: Infinity @@ -142,11 +181,20 @@ Item { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" + source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } + + MouseArea { + anchors.fill: parent + onClicked: { + LyricsService.toggleVisibility(); + } + } } } @@ -155,9 +203,9 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: visualiser.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { id: title @@ -169,7 +217,7 @@ Item { horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } @@ -184,7 +232,7 @@ Item { visible: !!Players.active text: Players.active?.trackAlbum || qsTr("Unknown album") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } @@ -202,19 +250,34 @@ Item { wrapMode: Players.active ? Text.NoWrap : Text.WordWrap } + LyricsView { + id: lyricsViewInDetails + + Layout.fillWidth: true + Layout.preferredHeight: 200 + } + RowLayout { id: controls Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.small - Layout.bottomMargin: Appearance.spacing.smaller + Layout.topMargin: Tokens.spacing.small + Layout.bottomMargin: Tokens.spacing.smaller - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small + + PlayerControl { + type: IconButton.Text + icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" + font.pointSize: Math.round(Tokens.font.size.large) + disabled: !Players.active?.shuffleSupported + onClicked: Players.active.shuffle = !Players.active?.shuffle + } PlayerControl { type: IconButton.Text icon: "skip_previous" - font.pointSize: Math.round(Appearance.font.size.large * 1.5) + font.pointSize: Math.round(Tokens.font.size.large * 1.5) disabled: !Players.active?.canGoPrevious onClicked: Players.active?.previous() } @@ -223,9 +286,9 @@ Item { icon: Players.active?.isPlaying ? "pause" : "play_arrow" label.animate: true toggle: true - padding: Appearance.padding.small / 2 + padding: Tokens.padding.small / 2 checked: Players.active?.isPlaying ?? false - font.pointSize: Math.round(Appearance.font.size.large * 1.5) + font.pointSize: Math.round(Tokens.font.size.large * 1.5) disabled: !Players.active?.canTogglePlaying onClicked: Players.active?.togglePlaying() } @@ -233,10 +296,17 @@ Item { PlayerControl { type: IconButton.Text icon: "skip_next" - font.pointSize: Math.round(Appearance.font.size.large * 1.5) + font.pointSize: Math.round(Tokens.font.size.large * 1.5) disabled: !Players.active?.canGoNext onClicked: Players.active?.next() } + + PlayerControl { + type: IconButton.Text + icon: "lyrics" + font.pointSize: Math.round(Tokens.font.size.large) + onClicked: root.lyricMenuOpen = !root.lyricMenuOpen + } } StyledSlider { @@ -244,7 +314,7 @@ Item { enabled: !!Players.active implicitWidth: 280 - implicitHeight: Appearance.padding.normal * 3 + implicitHeight: Tokens.padding.normal * 3 onMoved: { const active = Players.active; @@ -260,9 +330,6 @@ Item { } CustomMouseArea { - anchors.fill: parent - acceptedButtons: Qt.NoButton - function onWheel(event: WheelEvent) { const active = Players.active; if (!active?.canSeek || !active?.positionSupported) @@ -274,6 +341,9 @@ Item { active.position = Math.max(0, Math.min(active.length, active.position + delta)); }); } + + anchors.fill: parent + acceptedButtons: Qt.NoButton } } @@ -286,9 +356,9 @@ Item { anchors.left: parent.left - text: root.lengthStr(Players.active?.position ?? -1) + text: root.lengthStr(Players.active ? Players.active.position % Players.active.length : -1) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledText { @@ -298,105 +368,146 @@ Item { text: root.lengthStr(Players.active?.length ?? -1) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } + } - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small + ColumnLayout { + id: leftSection - PlayerControl { - type: IconButton.Text - icon: "move_up" - inactiveOnColour: Colours.palette.m3secondary - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canRaise - onClicked: { - Players.active?.raise(); - root.visibilities.dashboard = false; - } + anchors.verticalCenter: parent.verticalCenter + anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 + anchors.left: details.right + anchors.leftMargin: Tokens.spacing.normal + + visible: lyricMenu.height === 0 || opacity > 0 + opacity: lyricMenu.height === 0 ? 1 : 0 + + Behavior on opacity { + NumberAnimation { + duration: Tokens.anim.durations.normal + easing.type: Easing.OutCubic } + } + + Item { + id: bongocat - SplitButton { - id: playerSelector + implicitWidth: visualiser.width + implicitHeight: visualiser.height - disabled: !Players.list.length - active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null - menu.onItemSelected: item => Players.manualActive = item.modelData + AnimatedImage { + anchors.centerIn: parent - menuItems: playerList.instances - fallbackIcon: "music_off" - fallbackText: qsTr("No players") + width: visualiser.width * 0.75 + height: visualiser.height * 0.75 - label.Layout.maximumWidth: slider.implicitWidth * 0.28 - label.elide: Text.ElideRight + playing: Players.active?.isPlaying ?? false + speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type + source: Paths.absolutePath(Config.paths.mediaGif) + asynchronous: true + fillMode: AnimatedImage.PreserveAspectFit + } + } + } - stateLayer.disabled: true - menuOnTop: true + LyricMenu { + id: lyricMenu - Variants { - id: playerList + anchors.top: parent.top + anchors.left: details.right + anchors.right: parent.right + anchors.leftMargin: Tokens.spacing.normal - model: Players.list + contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Tokens.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight - MenuItem { - required property MprisPlayer modelData + visible: root.lyricMenuOpen || height > 0 + height: root.lyricMenuOpen ? implicitHeight : 0 + clip: true - icon: modelData === Players.active ? "check" : "" - text: Players.getIdentity(modelData) - activeIcon: "animated_images" - } - } + Behavior on height { + NumberAnimation { + duration: Tokens.anim.durations.normal + easing.type: Easing.OutCubic } + } + } - PlayerControl { - type: IconButton.Text - icon: "delete" - inactiveOnColour: Colours.palette.m3error - padding: Appearance.padding.small - font.pointSize: Appearance.font.size.large - disabled: !Players.active?.canQuit - onClicked: Players.active?.quit() + RowLayout { + id: playerChanger + + parent: !root.lyricsShowingDebounced ? details : leftSection + Layout.alignment: Qt.AlignHCenter + spacing: Tokens.spacing.small + + PlayerControl { + type: IconButton.Text + icon: "move_up" + inactiveOnColour: Colours.palette.m3secondary + padding: Tokens.padding.small + font.pointSize: Tokens.font.size.large + disabled: !Players.active?.canRaise + onClicked: { + Players.active?.raise(); + root.visibilities.dashboard = false; } } - } - Item { - id: bongocat + SplitButton { + id: playerSelector - anchors.verticalCenter: parent.verticalCenter - anchors.left: details.right - anchors.leftMargin: Appearance.spacing.normal + disabled: !Players.list.length + active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null + menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData - implicitWidth: visualiser.width - implicitHeight: visualiser.height + menuItems: playerList.instances + fallbackIcon: "music_off" + fallbackText: qsTr("No players") - AnimatedImage { - anchors.centerIn: parent + label.Layout.maximumWidth: slider.implicitWidth * 0.28 + label.elide: Text.ElideRight - width: visualiser.width * 0.75 - height: visualiser.height * 0.75 + stateLayer.disabled: true + menuOnTop: true - playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / 300 - source: Paths.absolutePath(Config.paths.mediaGif) - asynchronous: true - fillMode: AnimatedImage.PreserveAspectFit + Variants { + id: playerList + + model: Players.list + + PlayerItem {} + } } + + PlayerControl { + type: IconButton.Text + icon: "delete" + inactiveOnColour: Colours.palette.m3error + padding: Tokens.padding.small + font.pointSize: Tokens.font.size.large + disabled: !Players.active?.canQuit + onClicked: Players.active?.quit() + } + } + + component PlayerItem: MenuItem { + required property MprisPlayer modelData + + icon: modelData === Players.active ? "check" : "" + text: Players.getIdentity(modelData) + activeIcon: "animated_images" } component PlayerControl: IconButton { - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) - radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2 - radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial - radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) + radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : implicitHeight / 2 + radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial + radiusAnim.easing: Tokens.anim.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/dashboard/MediaWrapper.qml b/modules/dashboard/MediaWrapper.qml new file mode 100644 index 000000000..b03a11ee8 --- /dev/null +++ b/modules/dashboard/MediaWrapper.qml @@ -0,0 +1,13 @@ +import QtQuick + +Item { + property alias visibilities: media.visibilities + readonly property alias needsKeyboard: media.needsKeyboard + + implicitWidth: media.implicitWidth + implicitHeight: media.nonAnimHeight + + Media { + id: media + } +} diff --git a/modules/dashboard/Monitors.qml b/modules/dashboard/Monitors.qml index e1bfd4a0c..8af113063 100644 --- a/modules/dashboard/Monitors.qml +++ b/modules/dashboard/Monitors.qml @@ -1,7 +1,7 @@ import qs.components import qs.components.controls import qs.services -import qs.config +import Caelestia.Config import Quickshell import QtQuick import QtQuick.Layouts @@ -9,15 +9,15 @@ import QtQuick.Layouts ColumnLayout { id: root - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large RowLayout { Layout.fillWidth: true - Layout.margins: Appearance.padding.normal + Layout.margins: Tokens.padding.normal StyledText { text: qsTr("Monitors") - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge Layout.fillWidth: true } @@ -40,7 +40,7 @@ ColumnLayout { id: monitorsLayout anchors.left: parent.left anchors.right: parent.right - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Repeater { model: Hyprctl.monitors @@ -48,9 +48,9 @@ ColumnLayout { delegate: StyledRect { id: monitorDelegate Layout.fillWidth: true - implicitHeight: monitorContent.implicitHeight + Appearance.padding.large * 2 + implicitHeight: monitorContent.implicitHeight + Tokens.padding.large * 2 color: Colours.tPalette.m3surfaceContainerHigh - radius: Appearance.rounding.large + radius: Tokens.rounding.large readonly property var mon: modelData readonly property var brightnessMon: Brightness.getMonitor(mon.name) @@ -58,8 +58,8 @@ ColumnLayout { ColumnLayout { id: monitorContent anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.medium + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.medium RowLayout { Layout.fillWidth: true @@ -72,13 +72,13 @@ ColumnLayout { spacing: 0 StyledText { text: `${mon.name} - ${mon.make} ${mon.model}` - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large Layout.fillWidth: true } StyledText { text: `${mon.width}x${mon.height}@${(mon.refreshRate ?? 0).toFixed(2)}Hz` color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } StyledText { @@ -94,7 +94,7 @@ ColumnLayout { MaterialIcon { text: "brightness_medium" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledSlider { @@ -115,7 +115,7 @@ ColumnLayout { MaterialIcon { text: "zoom_in" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledSlider { @@ -135,7 +135,7 @@ ColumnLayout { // Refresh Rate RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Refresh Rate") @@ -155,7 +155,7 @@ ColumnLayout { // Rotation RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Rotation") @@ -182,7 +182,7 @@ ColumnLayout { // Arrangement RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { text: qsTr("Position relative to:") diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml index 5e00d8920..c93499ca6 100644 --- a/modules/dashboard/Performance.qml +++ b/modules/dashboard/Performance.qml @@ -1,227 +1,826 @@ +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Services.UPower +import Caelestia.Config +import Caelestia.Internal import qs.components import qs.components.misc import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts -RowLayout { +Item { id: root - readonly property int padding: Appearance.padding.large + readonly property int minWidth: 400 + 400 + Tokens.spacing.normal + 120 + Tokens.padding.large * 2 function displayTemp(temp: real): string { - return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`; + return `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}`; } - spacing: Appearance.spacing.large * 3 + implicitWidth: Math.max(minWidth, content.implicitWidth) + implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight - Ref { - service: SystemUsage - } + StyledRect { + id: placeholder + + anchors.centerIn: parent + width: 400 + height: 350 + radius: Tokens.rounding.large + color: Colours.tPalette.m3surfaceContainer + visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) - Resource { - Layout.alignment: Qt.AlignVCenter - Layout.topMargin: root.padding - Layout.bottomMargin: root.padding - Layout.leftMargin: root.padding * 2 + ColumnLayout { + anchors.centerIn: parent + spacing: Tokens.spacing.normal - value1: Math.min(1, SystemUsage.gpuTemp / 90) - value2: SystemUsage.gpuPerc + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: "tune" + font.pointSize: Tokens.font.size.extraLarge * 2 + color: Colours.palette.m3onSurfaceVariant + } - label1: root.displayTemp(SystemUsage.gpuTemp) - label2: `${Math.round(SystemUsage.gpuPerc * 100)}%` + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("No widgets enabled") + font.pointSize: Tokens.font.size.large + color: Colours.palette.m3onSurface + } - sublabel1: qsTr("GPU temp") - sublabel2: qsTr("Usage") + StyledText { + Layout.alignment: Qt.AlignHCenter + text: qsTr("Enable widgets in dashboard settings") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } } - Resource { - Layout.alignment: Qt.AlignVCenter - Layout.topMargin: root.padding - Layout.bottomMargin: root.padding + RowLayout { + id: content - primary: true + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.normal + visible: !placeholder.visible - value1: Math.min(1, SystemUsage.cpuTemp / 90) - value2: SystemUsage.cpuPerc + Ref { + service: SystemUsage + } + + ColumnLayout { + id: mainColumn + + Layout.fillWidth: true + spacing: Tokens.spacing.normal + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") + + HeroCard { + Layout.fillWidth: true + Layout.minimumWidth: 400 + Layout.preferredHeight: 150 + visible: Config.dashboard.performance.showCpu + icon: "memory" + title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU") + mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%` + mainLabel: qsTr("Usage") + secondaryValue: root.displayTemp(SystemUsage.cpuTemp) + secondaryLabel: qsTr("Temp") + usage: SystemUsage.cpuPerc + temperature: SystemUsage.cpuTemp + accentColor: Colours.palette.m3primary + } + + HeroCard { + Layout.fillWidth: true + Layout.minimumWidth: 400 + Layout.preferredHeight: 150 + visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE" + icon: "desktop_windows" + title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU") + mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%` + mainLabel: qsTr("Usage") + secondaryValue: root.displayTemp(SystemUsage.gpuTemp) + secondaryLabel: qsTr("Temp") + usage: SystemUsage.gpuPerc + temperature: SystemUsage.gpuTemp + accentColor: Colours.palette.m3secondary + } + } - label1: root.displayTemp(SystemUsage.cpuTemp) - label2: `${Math.round(SystemUsage.cpuPerc * 100)}%` + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork + + GaugeCard { + Layout.minimumWidth: 250 + Layout.preferredHeight: 220 + Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork + icon: "memory_alt" + title: qsTr("Memory") + percentage: SystemUsage.memPerc + subtitle: { + const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed); + const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal); + return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; + } + accentColor: Colours.palette.m3tertiary + visible: Config.dashboard.performance.showMemory + } + + StorageGaugeCard { + Layout.minimumWidth: 250 + Layout.preferredHeight: 220 + Layout.fillWidth: !Config.dashboard.performance.showNetwork + visible: Config.dashboard.performance.showStorage + } + + NetworkCard { + Layout.fillWidth: true + Layout.minimumWidth: 200 + Layout.preferredHeight: 220 + visible: Config.dashboard.performance.showNetwork + } + } + } - sublabel1: qsTr("CPU temp") - sublabel2: qsTr("Usage") + BatteryTank { + Layout.preferredWidth: 120 + Layout.preferredHeight: mainColumn.implicitHeight + visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery + } } - Resource { - Layout.alignment: Qt.AlignVCenter - Layout.topMargin: root.padding - Layout.bottomMargin: root.padding - Layout.rightMargin: root.padding * 3 + component BatteryTank: StyledClippingRect { + id: batteryTank + + property real percentage: UPower.displayDevice.percentage + property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging + property color accentColor: Colours.palette.m3primary + property real animatedPercentage: 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.large + Component.onCompleted: animatedPercentage = percentage + onPercentageChanged: animatedPercentage = percentage + + // Background Fill + StyledRect { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + height: parent.height * batteryTank.animatedPercentage + color: Qt.alpha(batteryTank.accentColor, 0.15) + } - value1: SystemUsage.memPerc - value2: SystemUsage.storagePerc + ColumnLayout { + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small + + // Header Section + ColumnLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: { + if (!UPower.displayDevice.isLaptopBattery) { + if (PowerProfiles.profile === PowerProfile.PowerSaver) + return "energy_savings_leaf"; + + if (PowerProfiles.profile === PowerProfile.Performance) + return "rocket_launch"; + + return "balance"; + } + if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) + return "battery_full"; + + const perc = UPower.displayDevice.percentage; + const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); + if (perc >= 0.99) + return "battery_full"; + + let level = Math.floor(perc * 7); + if (charging && (level === 4 || level === 1)) + level--; + + return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; + } + font.pointSize: Tokens.font.size.large + color: batteryTank.accentColor + } + + StyledText { + Layout.fillWidth: true + text: qsTr("Battery") + font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurface + } + } - label1: { - const fmt = SystemUsage.formatKib(SystemUsage.memUsed); - return `${+fmt.value.toFixed(1)}${fmt.unit}`; + Item { + Layout.fillHeight: true + } + + // Bottom Info Section + ColumnLayout { + Layout.fillWidth: true + spacing: -4 + + StyledText { + Layout.alignment: Qt.AlignRight + text: `${Math.round(batteryTank.percentage * 100)}%` + font.pointSize: Tokens.font.size.extraLarge + font.weight: Font.Medium + color: batteryTank.accentColor + } + + StyledText { + Layout.alignment: Qt.AlignRight + text: { + if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) + return qsTr("Full"); + + if (batteryTank.isCharging) + return qsTr("Charging"); + + const s = UPower.displayDevice.timeToEmpty; + if (s === 0) + return qsTr("..."); + + const hr = Math.floor(s / 3600); + const min = Math.floor((s % 3600) / 60); + if (hr > 0) + return `${hr}h ${min}m`; + + return `${min}m`; + } + font.pointSize: Tokens.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + } + } + + Behavior on animatedPercentage { + Anim { + type: Anim.StandardLarge + } } - label2: { - const fmt = SystemUsage.formatKib(SystemUsage.storageUsed); - return `${Math.floor(fmt.value)}${fmt.unit}`; + } + + component CardHeader: RowLayout { + property string icon + property string title + property color accentColor: Colours.palette.m3primary + + Layout.fillWidth: true + spacing: Tokens.spacing.small + + MaterialIcon { + text: parent.icon + fill: 1 + color: parent.accentColor + font.pointSize: Tokens.spacing.large } - sublabel1: qsTr("Memory") - sublabel2: qsTr("Storage") + StyledText { + Layout.fillWidth: true + text: parent.title + font.pointSize: Tokens.font.size.normal + elide: Text.ElideRight + } } - component Resource: Item { - id: res + component ProgressBar: StyledRect { + id: progressBar + + property real value: 0 + property color fgColor: Colours.palette.m3primary + property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + property real animatedValue: 0 + + color: bgColor + radius: Tokens.rounding.full + Component.onCompleted: animatedValue = value + onValueChanged: animatedValue = value + + StyledRect { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + width: parent.width * progressBar.animatedValue + color: progressBar.fgColor + radius: Tokens.rounding.full + } - required property real value1 - required property real value2 - required property string sublabel1 - required property string sublabel2 - required property string label1 - required property string label2 + Behavior on animatedValue { + Anim { + type: Anim.StandardLarge + } + } + } - property bool primary - readonly property real primaryMult: primary ? 1.2 : 1 + component HeroCard: StyledClippingRect { + id: heroCard + + property string icon + property string title + property string mainValue + property string mainLabel + property string secondaryValue + property string secondaryLabel + property real usage: 0 + property real temperature: 0 + property color accentColor: Colours.palette.m3primary + readonly property real maxTemp: 100 + readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp)) + property real animatedUsage: 0 + property real animatedTemp: 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.large + Component.onCompleted: { + animatedUsage = usage; + animatedTemp = tempProgress; + } + onUsageChanged: animatedUsage = usage + onTempProgressChanged: animatedTemp = tempProgress + + StyledRect { + anchors.left: parent.left + anchors.top: parent.top + anchors.bottom: parent.bottom + implicitWidth: parent.width * heroCard.animatedUsage + color: Qt.alpha(heroCard.accentColor, 0.15) + } - readonly property real thickness: Config.dashboard.sizes.resourceProgessThickness * primaryMult + CardHeader { + anchors.left: parent.left + anchors.top: parent.top + anchors.leftMargin: Tokens.padding.large + anchors.topMargin: Math.round(Tokens.padding.large * 1.2) - property color fg1: Colours.palette.m3primary - property color fg2: Colours.palette.m3secondary - property color bg1: Colours.palette.m3primaryContainer - property color bg2: Colours.palette.m3secondaryContainer + width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Tokens.spacing.normal + icon: heroCard.icon + title: heroCard.title + accentColor: heroCard.accentColor + } - implicitWidth: Config.dashboard.sizes.resourceSize * primaryMult - implicitHeight: Config.dashboard.sizes.resourceSize * primaryMult + Column { + anchors.left: parent.left + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: Math.round(Tokens.padding.large * 1.2) + anchors.bottomMargin: Math.round(Tokens.padding.large * 1.3) + + spacing: Tokens.spacing.small + + Row { + spacing: Tokens.spacing.small + + StyledText { + text: heroCard.secondaryValue + font.pointSize: Tokens.font.size.normal + font.weight: Font.Medium + } + + StyledText { + text: heroCard.secondaryLabel + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + anchors.baseline: parent.children[0].baseline + } + } - onValue1Changed: canvas.requestPaint() - onValue2Changed: canvas.requestPaint() - onFg1Changed: canvas.requestPaint() - onFg2Changed: canvas.requestPaint() - onBg1Changed: canvas.requestPaint() - onBg2Changed: canvas.requestPaint() + ProgressBar { + implicitWidth: parent.width * 0.5 + implicitHeight: 6 + value: heroCard.tempProgress + fgColor: heroCard.accentColor + bgColor: Qt.alpha(heroCard.accentColor, 0.2) + } + } Column { - anchors.centerIn: parent + id: usageColumn + + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.large + anchors.rightMargin: 32 + spacing: 0 StyledText { - anchors.horizontalCenter: parent.horizontalCenter + id: usageLabel - text: res.label1 - font.pointSize: Appearance.font.size.extraLarge * res.primaryMult + anchors.right: parent.right + text: heroCard.mainLabel + font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant } StyledText { - anchors.horizontalCenter: parent.horizontalCenter + anchors.right: parent.right + text: heroCard.mainValue + font.pointSize: Tokens.font.size.extraLarge + font.weight: Font.Medium + color: heroCard.accentColor + } + } - text: res.sublabel1 - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.smaller * res.primaryMult + Behavior on animatedUsage { + Anim { + type: Anim.StandardLarge } } - Column { - anchors.horizontalCenter: parent.right - anchors.top: parent.verticalCenter - anchors.horizontalCenterOffset: -res.thickness / 2 - anchors.topMargin: res.thickness / 2 + Appearance.spacing.small + Behavior on animatedTemp { + Anim { + type: Anim.StandardLarge + } + } + } - StyledText { - anchors.horizontalCenter: parent.horizontalCenter + component GaugeCard: StyledRect { + id: gaugeCard + + property string icon + property string title + property real percentage: 0 + property string subtitle + property color accentColor: Colours.palette.m3primary + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property real animatedPercentage: 0 + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.large + clip: true + Component.onCompleted: animatedPercentage = percentage + onPercentageChanged: animatedPercentage = percentage + + ColumnLayout { + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.smaller - text: res.label2 - font.pointSize: Appearance.font.size.smaller * res.primaryMult + CardHeader { + icon: gaugeCard.icon + title: gaugeCard.title + accentColor: gaugeCard.accentColor } - StyledText { - anchors.horizontalCenter: parent.horizontalCenter + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ArcGauge { + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: width + percentage: gaugeCard.animatedPercentage + accentColor: gaugeCard.accentColor + trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + startAngle: gaugeCard.arcStartAngle + sweepAngle: gaugeCard.arcSweep + } + + StyledText { + anchors.centerIn: parent + text: `${Math.round(gaugeCard.percentage * 100)}%` + font.pointSize: Tokens.font.size.extraLarge + font.weight: Font.Medium + color: gaugeCard.accentColor + } + } - text: res.sublabel2 + StyledText { + Layout.alignment: Qt.AlignHCenter + text: gaugeCard.subtitle + font.pointSize: Tokens.font.size.smaller color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small * res.primaryMult } } - Canvas { - id: canvas + Behavior on animatedPercentage { + Anim { + type: Anim.StandardLarge + } + } + } - readonly property real centerX: width / 2 - readonly property real centerY: height / 2 + component StorageGaugeCard: StyledRect { + id: storageGaugeCard + + property int currentDiskIndex: 0 + readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null + property int diskCount: 0 + readonly property real arcStartAngle: 0.75 * Math.PI + readonly property real arcSweep: 1.5 * Math.PI + property real animatedPercentage: 0 + property color accentColor: Colours.palette.m3secondary + + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.large + clip: true + Component.onCompleted: { + diskCount = SystemUsage.disks.length; + if (currentDisk) + animatedPercentage = currentDisk.perc; + } + onCurrentDiskChanged: { + if (currentDisk) + animatedPercentage = currentDisk.perc; + } + + // Update diskCount and animatedPercentage when disks data changes + Connections { + function onDisksChanged() { + if (SystemUsage.disks.length !== storageGaugeCard.diskCount) + storageGaugeCard.diskCount = SystemUsage.disks.length; - readonly property real arc1Start: degToRad(45) - readonly property real arc1End: degToRad(220) - readonly property real arc2Start: degToRad(230) - readonly property real arc2End: degToRad(360) + // Update animated percentage when disk data refreshes + if (storageGaugeCard.currentDisk) + storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; + } + + target: SystemUsage + } - function degToRad(deg: int): real { - return deg * Math.PI / 180; + MouseArea { + anchors.fill: parent + onWheel: wheel => { + if (wheel.angleDelta.y > 0) + storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; + else if (wheel.angleDelta.y < 0) + storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; } + } + ColumnLayout { anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.smaller + + CardHeader { + icon: "hard_disk" + title: { + const base = qsTr("Storage"); + if (!storageGaugeCard.currentDisk) + return base; + + return `${base} - ${storageGaugeCard.currentDisk.mount}`; + } + accentColor: storageGaugeCard.accentColor + + // Scroll hint icon + MaterialIcon { + text: "unfold_more" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.normal + visible: storageGaugeCard.diskCount > 1 + opacity: 0.7 + ToolTip.visible: hintHover.hovered + ToolTip.text: qsTr("Scroll to switch disks") + ToolTip.delay: 500 + + HoverHandler { + id: hintHover + } + } + } - onPaint: { - const ctx = getContext("2d"); - ctx.reset(); + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + ArcGauge { + anchors.centerIn: parent + width: Math.min(parent.width, parent.height) + height: width + percentage: storageGaugeCard.animatedPercentage + accentColor: storageGaugeCard.accentColor + trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) + startAngle: storageGaugeCard.arcStartAngle + sweepAngle: storageGaugeCard.arcSweep + } + + StyledText { + anchors.centerIn: parent + text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" + font.pointSize: Tokens.font.size.extraLarge + font.weight: Font.Medium + color: storageGaugeCard.accentColor + } + } - ctx.lineWidth = res.thickness; - ctx.lineCap = Appearance.rounding.scale === 0 ? "square" : "round"; + StyledText { + Layout.alignment: Qt.AlignHCenter + text: { + if (!storageGaugeCard.currentDisk) + return "—"; + + const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); + const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); + return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; + } + font.pointSize: Tokens.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + } - const radius = (Math.min(width, height) - ctx.lineWidth) / 2; - const cx = centerX; - const cy = centerY; - const a1s = arc1Start; - const a1e = arc1End; - const a2s = arc2Start; - const a2e = arc2End; + Behavior on animatedPercentage { + Anim { + type: Anim.StandardLarge + } + } + } - ctx.beginPath(); - ctx.arc(cx, cy, radius, a1s, a1e, false); - ctx.strokeStyle = res.bg1; - ctx.stroke(); + component NetworkCard: StyledRect { + id: networkCard - ctx.beginPath(); - ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false); - ctx.strokeStyle = res.fg1; - ctx.stroke(); + property color accentColor: Colours.palette.m3primary - ctx.beginPath(); - ctx.arc(cx, cy, radius, a2s, a2e, false); - ctx.strokeStyle = res.bg2; - ctx.stroke(); + color: Colours.tPalette.m3surfaceContainer + radius: Tokens.rounding.large + clip: true - ctx.beginPath(); - ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false); - ctx.strokeStyle = res.fg2; - ctx.stroke(); - } + Ref { + service: NetworkUsage } - Behavior on value1 { - Anim {} - } + ColumnLayout { + anchors.fill: parent + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small - Behavior on value2 { - Anim {} - } + CardHeader { + icon: "swap_vert" + title: qsTr("Network") + accentColor: networkCard.accentColor + } - Behavior on fg1 { - CAnim {} - } + // Sparkline graph + Item { + Layout.fillWidth: true + Layout.fillHeight: true + + SparklineItem { + id: sparkline + + property real targetMax: 1024 + property real smoothMax: targetMax + + anchors.fill: parent + line1: NetworkUsage.uploadBuffer // qmllint disable missing-type + line1Color: Colours.palette.m3secondary + line1FillAlpha: 0.15 + line2: NetworkUsage.downloadBuffer // qmllint disable missing-type + line2Color: Colours.palette.m3tertiary + line2FillAlpha: 0.2 + maxValue: smoothMax + historyLength: NetworkUsage.historyLength + + Connections { + function onValuesChanged(): void { + sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); + slideAnim.restart(); + } + + target: NetworkUsage.downloadBuffer + } + + NumberAnimation { + id: slideAnim + + target: sparkline + property: "slideProgress" + from: 0 + to: 1 + duration: GlobalConfig.dashboard.resourceUpdateInterval + } + + Behavior on smoothMax { + Anim { + type: Anim.StandardLarge + } + } + } + + // "No data" placeholder + StyledText { + anchors.centerIn: parent + text: qsTr("Collecting data...") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + visible: NetworkUsage.downloadBuffer.count < 2 + opacity: 0.6 + } + } - Behavior on fg2 { - CAnim {} - } + // Download row + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + + MaterialIcon { + text: "download" + color: Colours.palette.m3tertiary + font.pointSize: Tokens.font.size.normal + } + + StyledText { + text: qsTr("Download") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font.pointSize: Tokens.font.size.normal + font.weight: Font.Medium + color: Colours.palette.m3tertiary + } + } - Behavior on bg1 { - CAnim {} - } + // Upload row + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + + MaterialIcon { + text: "upload" + color: Colours.palette.m3secondary + font.pointSize: Tokens.font.size.normal + } + + StyledText { + text: qsTr("Upload") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); + return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; + } + font.pointSize: Tokens.font.size.normal + font.weight: Font.Medium + color: Colours.palette.m3secondary + } + } - Behavior on bg2 { - CAnim {} + // Session totals + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + + MaterialIcon { + text: "history" + color: Colours.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.normal + } + + StyledText { + text: qsTr("Total") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + + Item { + Layout.fillWidth: true + } + + StyledText { + text: { + const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); + const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); + return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; + } + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } } } } diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml index eef01be73..5bf99ca3e 100644 --- a/modules/dashboard/Tabs.qml +++ b/modules/dashboard/Tabs.qml @@ -1,19 +1,21 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Controls +import Quickshell +import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Controls Item { id: root required property real nonAnimWidth - required property PersistentProperties state + required property DashboardState dashState + required property var tabs + readonly property alias count: bar.count implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight @@ -25,55 +27,45 @@ Item { anchors.right: parent.right anchors.top: parent.top - currentIndex: root.state.currentTab + currentIndex: root.dashState.currentTab background: null - onCurrentIndexChanged: root.state.currentTab = currentIndex - - Tab { - iconName: "dashboard" - text: qsTr("Dashboard") - } - - Tab { - iconName: "queue_music" - text: qsTr("Media") - } + onCurrentIndexChanged: root.dashState.currentTab = currentIndex - Tab { - iconName: "speed" - text: qsTr("Performance") - } + Repeater { + model: ScriptModel { + values: root.tabs + } - Tab { - iconName: "cloud" - text: qsTr("Weather") - } + delegate: Tab { + required property var modelData - Tab { - iconName: "monitor" - text: qsTr("Monitors") + iconName: modelData.iconName + text: modelData.text + } } - - // Tab { - // iconName: "workspaces" - // text: qsTr("Workspaces") - // } } Item { id: indicator anchors.top: bar.bottom - anchors.topMargin: Config.dashboard.sizes.tabIndicatorSpacing + anchors.topMargin: 5 - implicitWidth: bar.currentItem.implicitWidth - implicitHeight: Config.dashboard.sizes.tabIndicatorHeight + implicitWidth: { + const tab = bar.currentItem; + if (tab) + return tab.implicitWidth; + const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; + return width; + } + implicitHeight: 3 x: { const tab = bar.currentItem; const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; - return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; + const tabWidth = tab?.implicitWidth ?? width; + return width * bar.currentIndex + (width - tabWidth) / 2; } clip: true @@ -85,7 +77,7 @@ Item { implicitHeight: parent.implicitHeight * 2 color: Colours.palette.m3primary - radius: Appearance.rounding.full + radius: Tokens.rounding.full } Behavior on x { @@ -119,13 +111,21 @@ Item { contentItem: CustomMouseArea { id: mouse + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y < 0) + root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, bar.count - 1); + else if (event.angleDelta.y > 0) + root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); + } + implicitWidth: Math.max(icon.width, label.width) implicitHeight: icon.height + label.height + hoverEnabled: true cursorShape: Qt.PointingHandCursor onPressed: event => { - root.state.currentTab = tab.TabBar.index; + root.dashState.currentTab = tab.TabBar.index; const stateY = stateWrapper.y; rippleAnim.x = event.x; @@ -137,13 +137,6 @@ Item { rippleAnim.restart(); } - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y < 0) - root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); - else if (event.angleDelta.y > 0) - root.state.currentTab = Math.max(root.state.currentTab - 1, 0); - } - SequentialAnimation { id: rippleAnim @@ -171,16 +164,13 @@ Item { properties: "implicitWidth,implicitHeight" from: 0 to: rippleAnim.radius * 2 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.normal + easing: Tokens.anim.standardDecel } Anim { target: ripple property: "opacity" to: 0 - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard } } @@ -190,10 +180,10 @@ Item { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2 + implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2 color: "transparent" - radius: Appearance.rounding.small + radius: Tokens.rounding.small StyledRect { id: stateLayer @@ -201,7 +191,7 @@ Item { anchors.fill: parent color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface - opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 + opacity: mouse.pressed ? 0.1 : mouse.containsMouse ? 0.08 : 0 Behavior on opacity { Anim {} @@ -211,7 +201,7 @@ Item { StyledRect { id: ripple - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface opacity: 0 @@ -231,7 +221,7 @@ Item { text: tab.iconName color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant fill: tab.current ? 1 : 0 - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large Behavior on fill { Anim {} diff --git a/modules/dashboard/WeatherTab.qml b/modules/dashboard/WeatherTab.qml new file mode 100644 index 000000000..3cf8d3b71 --- /dev/null +++ b/modules/dashboard/WeatherTab.qml @@ -0,0 +1,279 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services + +Item { + id: root + + readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null + + implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 + implicitHeight: layout.implicitHeight + Component.onCompleted: Weather.reload() + + ColumnLayout { + id: layout + + anchors.fill: parent + spacing: Tokens.spacing.smaller + + RowLayout { + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large + Layout.fillWidth: true + + Column { + spacing: Tokens.spacing.small / 2 + + StyledText { + text: Weather.city || qsTr("Loading...") + font.pointSize: Tokens.font.size.extraLarge + font.weight: 600 + color: Colours.palette.m3onSurface + } + + StyledText { + text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") + font.pointSize: Tokens.font.size.small + color: Colours.palette.m3onSurfaceVariant + } + } + + Item { + Layout.fillWidth: true + } + + Row { + spacing: Tokens.spacing.large + + WeatherStat { + icon: "wb_twilight" + label: "Sunrise" + value: Weather.sunrise + colour: Colours.palette.m3tertiary + } + + WeatherStat { + icon: "bedtime" + label: "Sunset" + value: Weather.sunset + colour: Colours.palette.m3tertiary + } + } + } + + StyledRect { + Layout.fillWidth: true + implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small * 2 + + radius: Tokens.rounding.large * 2 + color: Colours.tPalette.m3surfaceContainer + + RowLayout { + id: bigInfoRow + + anchors.centerIn: parent + spacing: Tokens.spacing.large + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: Weather.icon + font.pointSize: Tokens.font.size.extraLarge * 3 + color: Colours.palette.m3secondary + animate: true + } + + ColumnLayout { + Layout.alignment: Qt.AlignVCenter + spacing: -Tokens.spacing.small + + StyledText { + text: Weather.temp + font.pointSize: Tokens.font.size.extraLarge * 2 + font.weight: 500 + color: Colours.palette.m3primary + } + + StyledText { + Layout.leftMargin: Tokens.padding.small + text: Weather.description + font.pointSize: Tokens.font.size.normal + color: Colours.palette.m3onSurfaceVariant + } + } + } + } + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller + + DetailCard { + icon: "water_drop" + label: "Humidity" + value: Weather.humidity + "%" + colour: Colours.palette.m3secondary + } + DetailCard { + icon: "thermostat" + label: "Feels Like" + value: Weather.feelsLike + colour: Colours.palette.m3primary + } + DetailCard { + icon: "air" + label: "Wind" + value: Weather.windSpeed ? Weather.windSpeed + " km/h" : "--" + colour: Colours.palette.m3tertiary + } + } + + StyledText { + Layout.topMargin: Tokens.spacing.normal + Layout.leftMargin: Tokens.padding.normal + visible: forecastRepeater.count > 0 + text: qsTr("7-Day Forecast") + font.pointSize: Tokens.font.size.normal + font.weight: 600 + color: Colours.palette.m3onSurface + } + + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller + + Repeater { + id: forecastRepeater + + model: Weather.forecast + + StyledRect { + id: forecastItem + + required property int index + required property var modelData + + Layout.fillWidth: true + implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.normal * 2 + + radius: Tokens.rounding.normal + color: Colours.tPalette.m3surfaceContainer + + ColumnLayout { + id: forecastItemColumn + + anchors.centerIn: parent + spacing: Tokens.spacing.small + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") + font.pointSize: Tokens.font.size.normal + font.weight: 600 + color: Colours.palette.m3primary + } + + StyledText { + Layout.topMargin: -Tokens.spacing.small / 2 + Layout.alignment: Qt.AlignHCenter + text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") + font.pointSize: Tokens.font.size.small + opacity: 0.7 + color: Colours.palette.m3onSurfaceVariant + } + + MaterialIcon { + Layout.alignment: Qt.AlignHCenter + text: forecastItem.modelData.icon + font.pointSize: Tokens.font.size.extraLarge + color: Colours.palette.m3secondary + } + + StyledText { + Layout.alignment: Qt.AlignHCenter + text: GlobalConfig.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" + font.weight: 600 + color: Colours.palette.m3tertiary + } + } + } + } + } + } + + component DetailCard: StyledRect { + id: detailRoot + + property string icon + property string label + property string value + property color colour + + Layout.fillWidth: true + Layout.preferredHeight: 60 + radius: Tokens.rounding.small + color: Colours.tPalette.m3surfaceContainer + + Row { + anchors.centerIn: parent + spacing: Tokens.spacing.normal + + MaterialIcon { + text: detailRoot.icon + color: detailRoot.colour + font.pointSize: Tokens.font.size.large + anchors.verticalCenter: parent.verticalCenter + } + + Column { + anchors.verticalCenter: parent.verticalCenter + spacing: 0 + + StyledText { + text: detailRoot.label + font.pointSize: Tokens.font.size.smaller + opacity: 0.7 + horizontalAlignment: Text.AlignLeft + } + StyledText { + text: detailRoot.value + font.weight: 600 + horizontalAlignment: Text.AlignLeft + } + } + } + } + + component WeatherStat: Row { + id: weatherStat + + property string icon + property string label + property string value + property color colour + + spacing: Tokens.spacing.small + + MaterialIcon { + text: weatherStat.icon + font.pointSize: Tokens.font.size.extraLarge + color: weatherStat.colour + } + + Column { + StyledText { + text: weatherStat.label + font.pointSize: Tokens.font.size.smaller + color: Colours.palette.m3onSurfaceVariant + } + StyledText { + text: weatherStat.value + font.pointSize: Tokens.font.size.small + font.weight: 600 + color: Colours.palette.m3onSurface + } + } + } +} diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml index 0e37909e8..f7f037426 100644 --- a/modules/dashboard/Wrapper.qml +++ b/modules/dashboard/Wrapper.qml @@ -1,21 +1,19 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia +import Caelestia.Config import qs.components import qs.components.filedialog -import qs.config import qs.utils -import Caelestia -import Quickshell -import QtQuick Item { id: root - required property PersistentProperties visibilities - readonly property PersistentProperties dashState: PersistentProperties { - property int currentTab - property date currentDate: new Date() - + required property DrawerVisibilities visibilities + readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false + readonly property DashboardState dashState: DashboardState { reloadableId: "dashboardState" } readonly property FileDialog facePicker: FileDialog { @@ -30,60 +28,19 @@ Item { } } - readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 - - visible: height > 0 - implicitHeight: 0 - implicitWidth: content.implicitWidth - - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } - - states: State { - name: "visible" - when: root.visibilities.dashboard && Config.dashboard.enabled - - PropertyChanges { - root.implicitHeight: content.implicitHeight - } - } - - transitions: [ - Transition { - from: "" - to: "visible" - - Anim { - target: root - property: "implicitHeight" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitHeight" - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - ] + readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 + readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled + property real offsetScale: shouldBeActive ? 0 : 1 - Timer { - id: timer + visible: offsetScale < 1 + anchors.topMargin: (-implicitHeight - 5) * offsetScale + implicitHeight: content.implicitHeight + implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open + opacity: 1 - offsetScale - running: true - interval: Appearance.anim.durations.extraLarge - onTriggered: { - content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); - content.visible = true; + Behavior on offsetScale { + Anim { + type: Anim.DefaultSpatial } } @@ -93,12 +50,11 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - visible: false - active: true + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities - state: root.dashState + dashState: root.dashState facePicker: root.facePicker } } diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml index 56c04938c..e2af3c015 100644 --- a/modules/dashboard/dash/Calendar.qml +++ b/modules/dashboard/dash/Calendar.qml @@ -1,61 +1,58 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.components.effects -import qs.components.controls -import qs.services -import qs.config import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.components.controls +import qs.components.effects +import qs.services CustomMouseArea { id: root - required property var state + required property DashboardState dashState + + readonly property int currMonth: dashState.currentDate.getMonth() + readonly property int currYear: dashState.currentDate.getFullYear() - readonly property int currMonth: state.currentDate.getMonth() - readonly property int currYear: state.currentDate.getFullYear() + function onWheel(event: WheelEvent): void { + if (event.angleDelta.y > 0) + root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1); + else if (event.angleDelta.y < 0) + root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + } anchors.left: parent.left anchors.right: parent.right implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 acceptedButtons: Qt.MiddleButton - onClicked: root.state.currentDate = new Date() - - function onWheel(event: WheelEvent): void { - if (event.angleDelta.y > 0) - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - else if (event.angleDelta.y < 0) - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } + onClicked: root.dashState.currentDate = new Date() ColumnLayout { id: inner anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.small + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.small RowLayout { id: monthNavigationRow Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Item { implicitWidth: implicitHeight - implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 + implicitHeight: prevMonthText.implicitHeight + Tokens.padding.small * 2 StateLayer { id: prevMonthStateLayer - radius: Appearance.rounding.full - - function onClicked(): void { - root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); - } + radius: Tokens.rounding.full + onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) } MaterialIcon { @@ -64,7 +61,7 @@ CustomMouseArea { anchors.centerIn: parent text: "chevron_left" color: Colours.palette.m3tertiary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 700 } } @@ -72,24 +69,24 @@ CustomMouseArea { Item { Layout.fillWidth: true - implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 - implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 + implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.small * 2 + implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.small * 2 StateLayer { + onClicked: { + root.dashState.currentDate = new Date(); + } + anchors.fill: monthYearDisplay - anchors.margins: -Appearance.padding.small - anchors.leftMargin: -Appearance.padding.normal - anchors.rightMargin: -Appearance.padding.normal + anchors.margins: -Tokens.padding.small + anchors.leftMargin: -Tokens.padding.normal + anchors.rightMargin: -Tokens.padding.normal - radius: Appearance.rounding.full + radius: Tokens.rounding.full disabled: { const now = new Date(); return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); } - - function onClicked(): void { - root.state.currentDate = new Date(); - } } StyledText { @@ -98,7 +95,7 @@ CustomMouseArea { anchors.centerIn: parent text: grid.title color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 font.capitalization: Font.Capitalize } @@ -106,16 +103,16 @@ CustomMouseArea { Item { implicitWidth: implicitHeight - implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 + implicitHeight: nextMonthText.implicitHeight + Tokens.padding.small * 2 StateLayer { id: nextMonthStateLayer - radius: Appearance.rounding.full - - function onClicked(): void { - root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); + onClicked: { + root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); } + + radius: Tokens.rounding.full } MaterialIcon { @@ -124,7 +121,7 @@ CustomMouseArea { anchors.centerIn: parent text: "chevron_right" color: Colours.palette.m3tertiary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 700 } } @@ -167,7 +164,7 @@ CustomMouseArea { required property var model implicitWidth: implicitHeight - implicitHeight: text.implicitHeight + Appearance.padding.small * 2 + implicitHeight: text.implicitHeight + Tokens.padding.small * 2 StyledText { id: text @@ -184,7 +181,7 @@ CustomMouseArea { return Colours.palette.m3onSurfaceVariant; } opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal font.weight: 500 } } @@ -208,7 +205,7 @@ CustomMouseArea { implicitHeight: today?.implicitHeight ?? 0 clip: true - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary opacity: todayItem ? 1 : 0 @@ -236,15 +233,13 @@ CustomMouseArea { Behavior on x { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on y { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml index e74044883..82bee151c 100644 --- a/modules/dashboard/dash/DateTime.qml +++ b/modules/dashboard/dash/DateTime.qml @@ -1,17 +1,17 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services Item { id: root anchors.top: parent.top anchors.bottom: parent.bottom - implicitWidth: Config.dashboard.sizes.dateTimeWidth + implicitWidth: Tokens.sizes.dashboard.dateTimeWidth ColumnLayout { anchors.left: parent.left @@ -24,8 +24,8 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.hourStr color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.extraLarge + font.family: Tokens.font.family.clock font.weight: 600 } @@ -33,8 +33,8 @@ Item { Layout.alignment: Qt.AlignHCenter text: "•••" color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.extraLarge * 0.9 - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.extraLarge * 0.9 + font.family: Tokens.font.family.clock } StyledText { @@ -42,22 +42,23 @@ Item { Layout.alignment: Qt.AlignHCenter text: Time.minuteStr color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.extraLarge + font.family: Tokens.font.family.clock font.weight: 600 } Loader { + asynchronous: true Layout.alignment: Qt.AlignHCenter - active: Config.services.useTwelveHourClock + active: GlobalConfig.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.clock + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.clock font.weight: 600 } } diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml index 3a2b685ef..a14e5e389 100644 --- a/modules/dashboard/dash/Media.qml +++ b/modules/dashboard/dash/Media.qml @@ -1,32 +1,33 @@ +import QtQuick +import QtQuick.Shapes +import Quickshell +import Caelestia.Config +import Caelestia.Services import qs.components import qs.services -import qs.config import qs.utils -import Caelestia.Services -import QtQuick -import QtQuick.Shapes Item { id: root property real playerProgress: { const active = Players.active; - return active?.length ? active.position / active.length : 0; + return active?.length ? (active.position % active.length) / active.length : 0; } anchors.top: parent.top anchors.bottom: parent.bottom - implicitWidth: Config.dashboard.sizes.mediaWidth + implicitWidth: Tokens.sizes.dashboard.mediaWidth Behavior on playerProgress { Anim { - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } } Timer { running: Players.active?.isPlaying ?? false - interval: Config.dashboard.mediaUpdateInterval + interval: GlobalConfig.dashboard.mediaUpdateInterval triggeredOnStart: true repeat: true onTriggered: Players.active?.positionChanged() @@ -42,16 +43,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - strokeWidth: Config.dashboard.sizes.mediaProgressThickness - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 - sweepAngle: Config.dashboard.sizes.mediaProgressSweep + radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 + sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep } Behavior on strokeColor { @@ -62,16 +63,16 @@ Item { ShapePath { fillColor: "transparent" strokeColor: Colours.palette.m3primary - strokeWidth: Config.dashboard.sizes.mediaProgressThickness - capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap + strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness + capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap PathAngleArc { centerX: cover.x + cover.width / 2 centerY: cover.y + cover.height / 2 - radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small - startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 - sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress + radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small + startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 + sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress } Behavior on strokeColor { @@ -86,7 +87,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small + anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + Tokens.spacing.small implicitHeight: width color: Colours.tPalette.m3surfaceContainerHigh @@ -106,11 +107,13 @@ Item { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" + source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } } } @@ -119,15 +122,15 @@ Item { anchors.top: cover.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.normal + anchors.topMargin: Tokens.spacing.normal animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal - width: parent.implicitWidth - Appearance.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } @@ -136,15 +139,15 @@ Item { anchors.top: title.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small - width: parent.implicitWidth - Appearance.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } @@ -153,14 +156,14 @@ Item { anchors.top: album.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignHCenter text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") color: Colours.palette.m3secondary - width: parent.implicitWidth - Appearance.padding.large * 2 + width: parent.implicitWidth - Tokens.padding.large * 2 elide: Text.ElideRight } @@ -169,35 +172,26 @@ Item { anchors.top: artist.bottom anchors.horizontalCenter: parent.horizontalCenter - anchors.topMargin: Appearance.spacing.smaller + anchors.topMargin: Tokens.spacing.smaller - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small - Control { + PlayerControl { icon: "skip_previous" canUse: Players.active?.canGoPrevious ?? false - - function onClicked(): void { - Players.active?.previous(); - } + onClicked: Players.active?.previous() } - Control { + PlayerControl { icon: Players.active?.isPlaying ? "pause" : "play_arrow" canUse: Players.active?.canTogglePlaying ?? false - - function onClicked(): void { - Players.active?.togglePlaying(); - } + onClicked: Players.active?.togglePlaying() } - Control { + PlayerControl { icon: "skip_next" canUse: Players.active?.canGoNext ?? false - - function onClicked(): void { - Players.active?.next(); - } + onClicked: Players.active?.next() } } @@ -208,35 +202,32 @@ Item { anchors.bottom: parent.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Appearance.spacing.small - anchors.bottomMargin: Appearance.padding.large - anchors.margins: Appearance.padding.large * 2 + anchors.topMargin: Tokens.spacing.small + anchors.bottomMargin: Tokens.padding.large + anchors.margins: Tokens.padding.large * 2 playing: Players.active?.isPlaying ?? false - speed: Audio.beatTracker.bpm / 300 + speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type source: Paths.absolutePath(Config.paths.mediaGif) asynchronous: true fillMode: AnimatedImage.PreserveAspectFit } - component Control: StyledRect { + component PlayerControl: StyledRect { id: control required property string icon required property bool canUse - function onClicked(): void { - } - implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small + signal clicked + + implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small implicitHeight: implicitWidth StateLayer { disabled: !control.canUse - radius: Appearance.rounding.full - - function onClicked(): void { - control.onClicked(); - } + radius: Tokens.rounding.full + onClicked: control.clicked() } MaterialIcon { @@ -248,7 +239,7 @@ Item { animate: true text: control.icon color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml index 7f44a9d0b..2e0f085b7 100644 --- a/modules/dashboard/dash/Resources.qml +++ b/modules/dashboard/dash/Resources.qml @@ -1,8 +1,8 @@ +import QtQuick +import Caelestia.Config import qs.components import qs.components.misc import qs.services -import qs.config -import QtQuick Row { id: root @@ -10,8 +10,8 @@ Row { anchors.top: parent.top anchors.bottom: parent.bottom - padding: Appearance.padding.large - spacing: Appearance.spacing.normal + padding: Tokens.padding.large + spacing: Tokens.spacing.normal Ref { service: SystemUsage @@ -44,19 +44,19 @@ Row { anchors.top: parent.top anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large implicitWidth: icon.implicitWidth StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: icon.top - anchors.bottomMargin: Appearance.spacing.small + anchors.bottomMargin: Tokens.spacing.small - implicitWidth: Config.dashboard.sizes.resourceProgessThickness + implicitWidth: Tokens.sizes.dashboard.resourceProgressThickness color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StyledRect { anchors.left: parent.left @@ -65,7 +65,7 @@ Row { implicitHeight: res.value * parent.height color: res.colour - radius: Appearance.rounding.full + radius: Tokens.rounding.full } } @@ -80,7 +80,7 @@ Row { Behavior on value { Anim { - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/modules/dashboard/dash/SmallWeather.qml b/modules/dashboard/dash/SmallWeather.qml new file mode 100644 index 000000000..998c7125a --- /dev/null +++ b/modules/dashboard/dash/SmallWeather.qml @@ -0,0 +1,56 @@ +import QtQuick +import Caelestia.Config +import qs.components +import qs.services + +Item { + id: root + + anchors.centerIn: parent + + implicitWidth: icon.implicitWidth + info.implicitWidth + info.anchors.leftMargin + + Component.onCompleted: Weather.reload() + + MaterialIcon { + id: icon + + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + + animate: true + text: Weather.icon + color: Colours.palette.m3secondary + font.pointSize: Tokens.font.size.extraLarge * 2 + } + + Column { + id: info + + anchors.verticalCenter: parent.verticalCenter + anchors.left: icon.right + anchors.leftMargin: Tokens.spacing.large + + spacing: Tokens.spacing.small + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + animate: true + text: Weather.temp + color: Colours.palette.m3primary + font.pointSize: Tokens.font.size.extraLarge + font.weight: 500 + } + + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + + animate: true + text: Weather.description + + elide: Text.ElideRight + width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.large * 2) + } + } +} diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml index b66b1f9a4..79787de51 100644 --- a/modules/dashboard/dash/User.qml +++ b/modules/dashboard/dash/User.qml @@ -1,28 +1,26 @@ +import QtQuick +import Caelestia.Config import qs.components import qs.components.effects -import qs.components.images import qs.components.filedialog +import qs.components.images import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick Row { id: root - required property PersistentProperties visibilities - required property PersistentProperties state + required property DrawerVisibilities visibilities required property FileDialog facePicker - padding: Appearance.padding.large - spacing: Appearance.spacing.normal + padding: Tokens.padding.large + spacing: Tokens.spacing.normal StyledClippingRect { implicitWidth: info.implicitHeight implicitHeight: info.implicitHeight - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) MaterialIcon { @@ -32,6 +30,7 @@ Row { fill: 1 grade: 200 font.pointSize: Math.floor(info.implicitHeight / 2) || 1 + visible: pfp.status !== Image.Ready } CachingImage { @@ -53,7 +52,7 @@ Row { Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } } @@ -61,18 +60,17 @@ Row { StyledRect { anchors.centerIn: parent - implicitWidth: selectIcon.implicitHeight + Appearance.padding.small * 2 - implicitHeight: selectIcon.implicitHeight + Appearance.padding.small * 2 + implicitWidth: selectIcon.implicitHeight + Tokens.padding.small * 2 + implicitHeight: selectIcon.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.palette.m3primary scale: parent.containsMouse ? 1 : 0.5 opacity: parent.containsMouse ? 1 : 0 StateLayer { color: Colours.palette.m3onPrimary - - function onClicked(): void { + onClicked: { root.visibilities.launcher = false; root.facePicker.open(); } @@ -86,19 +84,18 @@ Row { text: "frame_person" color: Colours.palette.m3onPrimary - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge } Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } } @@ -109,7 +106,7 @@ Row { id: info anchors.verticalCenter: parent.verticalCenter - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Item { id: line @@ -121,10 +118,10 @@ Row { id: icon anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 source: SysInfo.osLogo - implicitSize: Math.floor(Appearance.font.size.normal * 1.34) + implicitSize: Math.floor(Tokens.font.size.normal * 1.34) colour: Colours.palette.m3primary } @@ -135,9 +132,9 @@ Row { anchors.left: icon.right anchors.leftMargin: icon.anchors.leftMargin text: `: ${SysInfo.osPrettyName || SysInfo.osName}` - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal - width: Config.dashboard.sizes.infoWidth + width: Tokens.sizes.dashboard.infoWidth elide: Text.ElideRight } } @@ -171,12 +168,12 @@ Row { id: icon anchors.left: parent.left - anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 + anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 fill: 1 text: line.icon color: line.colour - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { @@ -186,9 +183,9 @@ Row { anchors.left: icon.right anchors.leftMargin: icon.anchors.leftMargin text: `: ${line.text}` - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal - width: Config.dashboard.sizes.infoWidth + width: Tokens.sizes.dashboard.infoWidth elide: Text.ElideRight } } diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml new file mode 100644 index 000000000..677c84a09 --- /dev/null +++ b/modules/drawers/ContentWindow.qml @@ -0,0 +1,318 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Caelestia.Blobs +import Caelestia.Config +import qs.components +import qs.components.containers +import qs.services +import qs.modules.bar + +StyledWindow { + id: root + + readonly property alias bar: bar + readonly property alias interactionWrapper: interactions + + readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) + readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0 + readonly property bool hasFullscreen: { + if (hasSpecialWorkspace) { + const specialName = monitor?.lastIpcObject.specialWorkspace?.name; + if (!specialName) + return false; + const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); + return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + + property real fsTransitionProg: hasFullscreen ? 1 : 0 + readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows + readonly property real borderThickness: contentItem.Config.border.thickness * (1 - fsTransitionProg) + readonly property real borderRounding: contentItem.Config.border.rounding * (1 - fsTransitionProg) + readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) + readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness + + readonly property int dragMaskPadding: { + if (focusGrab.active || panels.popouts.isDetached) + return 0; + + if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0) + return 0; + + const thresholds = []; + for (const panel of ["dashboard", "launcher", "session", "sidebar"]) + if (contentItem.Config[panel].enabled) + thresholds.push(contentItem.Config[panel].dragThreshold); + return Math.max(...thresholds); + } + + onHasFullscreenChanged: { + visibilities.launcher = false; + visibilities.session = false; + visibilities.dashboard = false; + panels.popouts.close(); + } + + name: "drawers" + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: fsTransitionProg > 0 && contentItem.Config.general.showOverFullscreen ? WlrLayer.Overlay : WlrLayer.Top + WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None + + mask: hasFullscreen ? emptyRegion : regions + + anchors.top: true + anchors.bottom: true + anchors.left: true + anchors.right: true + + Behavior on fsTransitionProg { + Anim {} + } + + Region { + id: emptyRegion + + x: panels.notifications.x + bar.implicitWidth + y: panels.notifications.y + root.borderThickness + width: panels.notifications.width + height: panels.notifications.height + + Region { + x: root.width - width + y: panels.osdWrapper.y + root.borderThickness + width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness + height: panels.osd.height + } + } + + Regions { + id: regions + + bar: bar + panels: panels + win: root + } + + HyprlandFocusGrab { + id: focusGrab + + active: (visibilities.launcher && root.contentItem.Config.launcher.enabled) || (visibilities.session && root.contentItem.Config.session.enabled) || (visibilities.sidebar && root.contentItem.Config.sidebar.enabled) || (!root.contentItem.Config.dashboard.showOnHover && visibilities.dashboard && root.contentItem.Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) + windows: [root] + onCleared: { + visibilities.launcher = false; + visibilities.session = false; + visibilities.sidebar = false; + visibilities.dashboard = false; + panels.popouts.hasCurrent = false; + bar.closeTray(); + } + } + + StyledRect { + anchors.fill: parent + opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 + color: Colours.palette.m3scrim + + Behavior on opacity { + Anim {} + } + } + + Item { + anchors.fill: parent + opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 + layer.enabled: true + layer.effect: MultiEffect { + shadowEnabled: true + blurMax: 15 + shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity)) + } + + BlobGroup { + id: blobGroup + + color: Colours.palette.m3surface + smoothing: root.contentItem.Config.border.smoothing + + Behavior on color { + CAnim {} + } + } + + BlobInvertedRect { + anchors.fill: parent + anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers + group: blobGroup + radius: root.borderRounding + borderLeft: bar.implicitWidth - anchors.margins - root.sdfBorderOffset + borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset + borderTop: root.borderThickness - anchors.margins - root.sdfBorderOffset + borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset + } + + PanelBg { + id: dashBg + + panel: panels.dashboard + deformAmount: 0.1 + } + + PanelBg { + id: launcherBg + + panel: panels.launcher + deformAmount: 0.1 + } + + PanelBg { + id: sessionBg + + panel: panels.sessionWrapper + deformAmount: 0.2 + x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth + implicitWidth: panels.session.width + } + + PanelBg { + id: sidebarBg + + panel: panels.sidebar + deformAmount: 0.03 + implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] + bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + } + + PanelBg { + id: osdBg + + panel: panels.osdWrapper + deformAmount: 0.25 + x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth + implicitWidth: panels.osd.width + } + + PanelBg { + id: notifsBg + + panel: panels.notifications + } + + PanelBg { + id: utilsBg + + panel: panels.utilities + deformAmount: panels.sidebar.visible ? 0.1 : 0.15 + exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] + topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius + } + + PanelBg { + id: popoutBg + + // Extra width to prevent vertical movement deformation partially detaching panel from bar + property real extraWidth: panels.popouts.isDetached ? 0 : 0.2 + + panel: panels.popoutsWrapper + deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 + x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth + implicitWidth: panels.popouts.width * (1 + extraWidth) + + Behavior on extraWidth { + Anim { + type: Anim.DefaultSpatial + } + } + } + } + + DrawerVisibilities { + id: visibilities + + Component.onCompleted: Visibilities.load(root.screen, this) + } + + Interactions { + id: interactions + + screen: root.screen + popouts: panels.popouts + visibilities: visibilities + panels: panels + bar: bar + borderThickness: root.borderLayoutThickness + fullscreen: root.hasFullscreen + + Panels { + id: panels + + screen: root.screen + visibilities: visibilities + bar: bar + borderThickness: root.borderThickness + + utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 + utilities.deformMatrix: utilsBg.rawDeformMatrix + + dashboard.transform: Matrix4x4 { + matrix: dashBg.deformMatrix + } + launcher.transform: Matrix4x4 { + matrix: launcherBg.deformMatrix + } + session.transform: Matrix4x4 { + matrix: sessionBg.deformMatrix + } + sidebar.transform: Matrix4x4 { + matrix: sidebarBg.deformMatrix + } + osd.transform: Matrix4x4 { + matrix: osdBg.deformMatrix + } + notifications.transform: Matrix4x4 { + matrix: notifsBg.deformMatrix + } + utilities.transform: Matrix4x4 { + matrix: utilsBg.deformMatrix + } + popouts.transform: Matrix4x4 { + matrix: popoutBg.deformMatrix + } + } + + BarWrapper { + id: bar + + anchors.top: parent.top + anchors.bottom: parent.bottom + + screen: root.screen + visibilities: visibilities + popouts: panels.popouts + + fullscreen: root.hasFullscreen + + Component.onCompleted: Visibilities.bars.set(root.screen, this) + } + } + + component PanelBg: BlobRect { + required property Item panel + property real deformAmount: 0.15 + + group: blobGroup + x: panel.x + bar.implicitWidth + y: panel.y + root.borderThickness + implicitWidth: panel.width + implicitHeight: panel.height + radius: Tokens.rounding.large + deformScale: (deformAmount * Config.appearance.deformScale) / 10000 + } +} diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 00f9596a4..c642692c3 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -1,193 +1,24 @@ -pragma ComponentBehavior: Bound - -import qs.components -import qs.components.containers -import qs.services -import qs.config -import qs.modules.bar import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland -import QtQuick -import QtQuick.Effects +import qs.services Variants { - model: Quickshell.screens + model: Screens.screens Scope { id: scope required property ShellScreen modelData - readonly property bool barDisabled: { - const regexChecker = /^\^.*\$$/; - for (const filter of Config.bar.excludedScreens) { - // If filter is a regex - if (regexChecker.test(filter)) { - if ((new RegExp(filter)).test(modelData.name)) - return true; - } else { - if (filter === modelData.name) - return true; - } - } - return false; - } Exclusions { screen: scope.modelData - bar: bar + bar: drawerWindow.bar } - StyledWindow { - id: win - - readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false - readonly property int dragMaskPadding: { - if (focusGrab.active || panels.popouts.isDetached) - return 0; - - const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject?.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject?.windows > 0) - return 0; - - const thresholds = []; - for (const panel of ["dashboard", "launcher", "session", "sidebar"]) - if (Config[panel].enabled) - thresholds.push(Config[panel].dragThreshold); - return Math.max(...thresholds); - } - - onHasFullscreenChanged: { - visibilities.launcher = false; - visibilities.session = false; - visibilities.dashboard = false; - } + ContentWindow { + id: drawerWindow screen: scope.modelData name: "drawers" - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None - - mask: Region { - x: bar.implicitWidth + win.dragMaskPadding - y: Config.border.thickness + win.dragMaskPadding - width: win.width - bar.implicitWidth - Config.border.thickness - win.dragMaskPadding * 2 - height: win.height - Config.border.thickness * 2 - win.dragMaskPadding * 2 - intersection: Intersection.Xor - - regions: regions.instances - } - - anchors.top: true - anchors.bottom: true - anchors.left: true - anchors.right: true - - Variants { - id: regions - - model: panels.children - - Region { - required property Item modelData - - x: modelData.x + bar.implicitWidth - y: modelData.y + Config.border.thickness - width: modelData.width - height: modelData.height - intersection: Intersection.Subtract - } - } - - HyprlandFocusGrab { - id: focusGrab - - active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && panels.popouts.current?.depth > 1) - windows: [win] - onCleared: { - visibilities.launcher = false; - visibilities.session = false; - visibilities.sidebar = false; - visibilities.dashboard = false; - panels.popouts.hasCurrent = false; - bar.closeTray(); - } - } - - StyledRect { - anchors.fill: parent - opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 - color: Colours.palette.m3scrim - - Behavior on opacity { - Anim {} - } - } - - Item { - anchors.fill: parent - opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 - layer.enabled: true - layer.effect: MultiEffect { - shadowEnabled: true - blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) - } - - Border { - bar: bar - } - - Backgrounds { - panels: panels - bar: bar - } - } - - PersistentProperties { - id: visibilities - - property bool bar - property bool osd - property bool session - property bool launcher - property bool dashboard - property bool utilities - property bool sidebar - - Component.onCompleted: Visibilities.load(scope.modelData, this) - } - - Interactions { - screen: scope.modelData - popouts: panels.popouts - visibilities: visibilities - panels: panels - bar: bar - - Panels { - id: panels - - screen: scope.modelData - visibilities: visibilities - bar: bar - } - - BarWrapper { - id: bar - - anchors.top: parent.top - anchors.bottom: parent.bottom - - screen: scope.modelData - visibilities: visibilities - popouts: panels.popouts - - disabled: scope.barDisabled - - Component.onCompleted: Visibilities.bars.set(scope.modelData, this) - } - } } } } diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index e4015c89a..fe72730c3 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -1,15 +1,16 @@ pragma ComponentBehavior: Bound -import qs.components.containers -import qs.config -import Quickshell import QtQuick +import Quickshell +import Caelestia.Config +import qs.components.containers +import qs.modules.bar as Bar Scope { id: root required property ShellScreen screen - required property Item bar + required property Bar.BarWrapper bar ExclusionZone { anchors.left: true @@ -31,7 +32,7 @@ Scope { component ExclusionZone: StyledWindow { screen: root.screen name: "border-exclusion" - exclusiveZone: Config.border.thickness + exclusiveZone: contentItem.Config.border.thickness mask: Region {} implicitWidth: 1 implicitHeight: 1 diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index 9579b15ae..b89cb0077 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -1,17 +1,22 @@ +import QtQuick +import QtQuick.Controls +import Quickshell +import Caelestia.Config +import qs.components import qs.components.controls -import qs.config +import qs.modules.bar as Bar import qs.modules.bar.popouts as BarPopouts -import Quickshell -import QtQuick CustomMouseArea { id: root required property ShellScreen screen required property BarPopouts.Wrapper popouts - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property Panels panels - required property Item bar + required property Bar.BarWrapper bar + required property real borderThickness + required property bool fullscreen property point dragStart property bool dashboardShortcutActive @@ -19,7 +24,7 @@ CustomMouseArea { property bool utilitiesShortcutActive function withinPanelHeight(panel: Item, x: real, y: real): bool { - const panelY = Config.border.thickness + panel.y; + const panelY = root.borderThickness + panel.y; return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; } @@ -33,24 +38,29 @@ CustomMouseArea { } function inRightPanel(panel: Item, x: real, y: real): bool { - return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y); + return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); } function inTopPanel(panel: Item, x: real, y: real): bool { - return y < Config.border.thickness + panel.y + panel.height && withinPanelWidth(panel, x, y); + const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property + return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y); } - function inBottomPanel(panel: Item, x: real, y: real): bool { - return y > root.height - Config.border.thickness - panel.height - Config.border.rounding && withinPanelWidth(panel, x, y); + function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { + const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property + return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); } function onWheel(event: WheelEvent): void { + if (fullscreen) + return; if (event.x < bar.implicitWidth) { bar.handleWheel(event.y, event.angleDelta); } } anchors.fill: parent + acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons hoverEnabled: true onPressed: event => dragStart = Qt.point(event.x, event.y) @@ -68,7 +78,7 @@ CustomMouseArea { if (!utilitiesShortcutActive) visibilities.utilities = false; - if (!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) { + if (!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) { popouts.hasCurrent = false; bar.closeTray(); } @@ -87,21 +97,26 @@ CustomMouseArea { const dragX = x - dragStart.x; const dragY = y - dragStart.y; + if (fullscreen) { + root.panels.osd.hovered = inRightPanel(panels.osdWrapper, x, y); + return; + } + // Show bar in non-exclusive mode on hover - if (!visibilities.bar && Config.bar.showOnHover && x < bar.implicitWidth) + if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) bar.isHovered = true; // Show/hide bar on drag - if (pressed && dragStart.x < bar.implicitWidth) { + if (pressed && dragStart.x < bar.clampedWidth) { if (dragX > Config.bar.dragThreshold) visibilities.bar = true; else if (dragX < -Config.bar.dragThreshold) visibilities.bar = false; } - if (panels.sidebar.width === 0) { + if (panels.sidebar.offsetScale === 1) { // Show osd on hover - const showOsd = inRightPanel(panels.osd, x, y); + const showOsd = inRightPanel(panels.osdWrapper, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { @@ -113,26 +128,26 @@ CustomMouseArea { root.panels.osd.hovered = true; } - const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; + const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); // Show/hide session on drag - if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) visibilities.session = false; // Show sidebar on drag if in session area and session is nearly fully visible - if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) + if (showSidebar && panels.session.offsetScale <= 0 && dragX < -Config.sidebar.dragThreshold) visibilities.sidebar = true; } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { // Show sidebar on drag if not in session area visibilities.sidebar = true; } } else { - const outOfSidebar = x < width - panels.sidebar.width; + const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale); // Show osd on hover - const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); + const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y); // Always update visibility based on hover if not in shortcut mode if (!osdShortcutActive) { @@ -145,7 +160,7 @@ CustomMouseArea { } // Show/hide session on drag - if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { + if (pressed && outOfSidebar && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { if (dragX < -Config.session.dragThreshold) visibilities.session = true; else if (dragX > Config.session.dragThreshold) @@ -188,7 +203,7 @@ CustomMouseArea { } // Show utilities on hover - const showUtilities = inBottomPanel(panels.utilities, x, y); + const showUtilities = inBottomPanel(panels.utilities, x, y, true); // Always update visibility based on hover if not in shortcut mode if (!utilitiesShortcutActive) { @@ -201,7 +216,7 @@ CustomMouseArea { // Show popouts on hover if (x < bar.implicitWidth) { bar.checkPopout(y); - } else if ((!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { + } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) { popouts.hasCurrent = false; bar.closeTray(); } @@ -209,8 +224,6 @@ CustomMouseArea { // Monitor individual visibility changes Connections { - target: root.visibilities - function onLauncherChanged() { // If launcher is hidden, clear shortcut flags for dashboard and OSD if (!root.visibilities.launcher) { @@ -220,7 +233,7 @@ CustomMouseArea { // Also hide dashboard and OSD if they're not being hovered const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); if (!inDashboardArea) { root.visibilities.dashboard = false; @@ -248,7 +261,7 @@ CustomMouseArea { function onOsdChanged() { if (root.visibilities.osd) { // OSD became visible, immediately check if this should be shortcut mode - const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); + const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); if (!inOsdArea) { root.osdShortcutActive = true; } @@ -270,5 +283,7 @@ CustomMouseArea { root.utilitiesShortcutActive = false; } } + + target: root.visibilities } } diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index 7705732a7..4ca420e3e 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -1,69 +1,106 @@ -import qs.config -import qs.modules.osd as Osd +import QtQuick +import Quickshell +import Caelestia.Config +import qs.components +import qs.modules.bar as Bar +import qs.modules.dashboard as Dashboard +import qs.modules.launcher as Launcher import qs.modules.notifications as Notifications +import qs.modules.osd as Osd import qs.modules.session as Session -import qs.modules.launcher as Launcher -import qs.modules.dashboard as Dashboard -import qs.modules.bar.popouts as BarPopouts +import qs.modules.sidebar as Sidebar import qs.modules.utilities as Utilities +import qs.modules.bar.popouts as BarPopouts import qs.modules.utilities.toasts as Toasts -import qs.modules.sidebar as Sidebar -import Quickshell -import QtQuick Item { id: root required property ShellScreen screen - required property PersistentProperties visibilities - required property Item bar + required property DrawerVisibilities visibilities + required property Bar.BarWrapper bar + required property real borderThickness readonly property alias osd: osd + readonly property alias osdWrapper: osdWrapper readonly property alias notifications: notifications readonly property alias session: session + readonly property alias sessionWrapper: sessionWrapper readonly property alias launcher: launcher readonly property alias dashboard: dashboard - readonly property alias popouts: popouts + readonly property alias popouts: popoutsWrapper.content + readonly property alias popoutsWrapper: popoutsWrapper readonly property alias utilities: utilities readonly property alias toasts: toasts readonly property alias sidebar: sidebar anchors.fill: parent - anchors.margins: Config.border.thickness + anchors.margins: borderThickness anchors.leftMargin: bar.implicitWidth - Osd.Wrapper { - id: osd + Behavior on anchors.margins { + Anim {} + } - clip: session.width > 0 || sidebar.width > 0 - screen: root.screen - visibilities: root.visibilities + Behavior on anchors.leftMargin { + Anim {} + } + + Item { + id: osdWrapper anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: session.width + sidebar.width + anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale) + clip: sidebar.visible || session.visible + + implicitWidth: osd.implicitWidth * (1 - osd.offsetScale) + implicitHeight: osd.implicitHeight + + Osd.Wrapper { + id: osd + + screen: root.screen + visibilities: root.visibilities + sidebarOrSessionVisible: sidebar.visible || session.visible + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } } Notifications.Wrapper { id: notifications visibilities: root.visibilities - panels: root + sidebarPanel: sidebar + osdPanel: osdWrapper + sessionPanel: sessionWrapper anchors.top: parent.top anchors.right: parent.right } - Session.Wrapper { - id: session - - clip: sidebar.width > 0 - visibilities: root.visibilities - panels: root + Item { + id: sessionWrapper anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: sidebar.width + anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale) + clip: sidebar.visible + + implicitWidth: session.implicitWidth * (1 - session.offsetScale) + implicitHeight: session.implicitHeight + + Session.Wrapper { + id: session + + visibilities: root.visibilities + sidebarVisible: sidebar.visible + + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + } } Launcher.Wrapper { @@ -86,22 +123,11 @@ Item { anchors.top: parent.top } - BarPopouts.Wrapper { - id: popouts + BarPopouts.ClipWrapper { + id: popoutsWrapper screen: root.screen - - x: isDetached ? (root.width - nonAnimWidth) / 2 : 0 - y: { - if (isDetached) - return (root.height - nonAnimHeight) / 2; - - const off = currentCenter - Config.border.thickness - nonAnimHeight / 2; - const diff = root.height - Math.floor(off + nonAnimHeight); - if (diff < 0) - return off + diff; - return Math.max(off, 0); - } + borderThickness: root.borderThickness } Utilities.Wrapper { @@ -109,7 +135,7 @@ Item { visibilities: root.visibilities sidebar: sidebar - popouts: popouts + popouts: popoutsWrapper.content anchors.bottom: parent.bottom anchors.right: parent.right @@ -120,14 +146,13 @@ Item { anchors.bottom: sidebar.visible ? parent.bottom : utilities.top anchors.right: sidebar.left - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal } Sidebar.Wrapper { id: sidebar visibilities: root.visibilities - panels: root anchors.top: notifications.bottom anchors.bottom: utilities.top diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml new file mode 100644 index 000000000..47ad06804 --- /dev/null +++ b/modules/drawers/Regions.qml @@ -0,0 +1,84 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Caelestia.Config +import qs.modules.bar as Bar + +Region { + id: root + + required property Bar.BarWrapper bar + required property Panels panels + required property var win + + readonly property real borderThickness: win.contentItem.Config.border.thickness + readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness + + x: bar.clampedWidth + win.dragMaskPadding + y: clampedThickness + win.dragMaskPadding + width: win.width - bar.clampedWidth - clampedThickness - win.dragMaskPadding * 2 + height: win.height - clampedThickness * 2 - win.dragMaskPadding * 2 + intersection: Intersection.Xor + + R { + panel: root.panels.dashboard + y: 0 + height: panel.height * (1 - root.panels.dashboard.offsetScale) + root.borderThickness + } + + R { + panel: root.panels.launcher + y: root.win.height - height + height: panel.height * (1 - root.panels.launcher.offsetScale) + root.borderThickness + } + + R { + id: sessionRegion + + panel: root.panels.sessionWrapper + x: root.win.width - width + width: panel.width * (1 - root.panels.session.offsetScale) + root.borderThickness + sidebarRegion.width + } + + R { + id: sidebarRegion + + panel: root.panels.sidebar + x: root.win.width - width + width: panel.width * (1 - root.panels.sidebar.offsetScale) + root.borderThickness + } + + R { + panel: root.panels.osdWrapper + x: root.win.width - width + width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness + sessionRegion.width + } + + R { + panel: root.panels.notifications + y: 0 + height: panel.height + root.borderThickness + } + + R { + panel: root.panels.utilities + y: root.win.height - height + height: panel.height * (1 - root.panels.utilities.offsetScale) + root.borderThickness + } + + R { + panel: root.panels.popoutsWrapper + width: panel.width * (1 - root.panels.popoutsWrapper.offsetScale) + } + + component R: Region { + required property Item panel + + x: panel.x + root.bar.implicitWidth + y: panel.y + root.borderThickness + width: panel.width + height: panel.height + intersection: Intersection.Subtract + } +} diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml index 7f7b843a9..a2109c040 100644 --- a/modules/launcher/AppList.qml +++ b/modules/launcher/AppList.qml @@ -1,20 +1,20 @@ pragma ComponentBehavior: Bound -import "items" -import "services" +import QtQuick +import Quickshell +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config -import Quickshell -import QtQuick +import qs.modules.launcher.items +import qs.modules.launcher.services StyledListView { id: root required property StyledTextField search - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities model: ScriptModel { id: model @@ -22,9 +22,9 @@ StyledListView { onValuesChanged: root.currentIndex = 0 } - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small orientation: Qt.Vertical - implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing + implicitHeight: (Tokens.sizes.launcher.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing preferredHighlightBegin: 0 preferredHighlightEnd: height @@ -32,7 +32,7 @@ StyledListView { highlightFollowsCurrentItem: false highlight: StyledRect { - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.palette.m3onSurface opacity: 0.08 @@ -42,15 +42,14 @@ StyledListView { Behavior on y { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } state: { const text = search.text; - const prefix = Config.launcher.actionPrefix; + const prefix = GlobalConfig.launcher.actionPrefix; if (text.startsWith(prefix)) { for (const action of ["calc", "scheme", "variant"]) if (text.startsWith(`${prefix}${action} `)) @@ -118,16 +117,16 @@ StyledListView { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.small + easing: Tokens.anim.standardAccel } Anim { target: root property: "scale" from: 1 to: 0.9 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.small + easing: Tokens.anim.standardAccel } } PropertyAction { @@ -140,16 +139,16 @@ StyledListView { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.small + easing: Tokens.anim.standardDecel } Anim { target: root property: "scale" from: 0.9 to: 1 - duration: Appearance.anim.durations.small - easing.bezierCurve: Appearance.anim.curves.standardDecel + duration: Tokens.anim.durations.small + easing: Tokens.anim.standardDecel } } PropertyAction { @@ -197,7 +196,7 @@ StyledListView { addDisplaced: Transition { Anim { property: "y" - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } Anim { properties: "opacity,scale" diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml index c08597698..69be0c3ae 100644 --- a/modules/launcher/Content.qml +++ b/modules/launcher/Content.qml @@ -1,22 +1,21 @@ pragma ComponentBehavior: Bound -import "services" +import QtQuick +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config -import Quickshell -import QtQuick +import qs.modules.launcher.services Item { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property var panels required property real maxHeight - readonly property int padding: Appearance.padding.large - readonly property int rounding: Appearance.rounding.large + readonly property int padding: Tokens.padding.large + readonly property int rounding: Tokens.rounding.large implicitWidth: listWrapper.width + padding * 2 implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 @@ -48,7 +47,7 @@ Item { id: searchWrapper color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full anchors.left: parent.left anchors.right: parent.right @@ -73,13 +72,13 @@ Item { anchors.left: searchIcon.right anchors.right: clearIcon.left - anchors.leftMargin: Appearance.spacing.small - anchors.rightMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small + anchors.rightMargin: Tokens.spacing.small - topPadding: Appearance.padding.larger - bottomPadding: Appearance.padding.larger + topPadding: Tokens.padding.larger + bottomPadding: Tokens.padding.larger - placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) + placeholderText: qsTr("Type \"%1\" for commands").arg(GlobalConfig.launcher.actionPrefix) onAccepted: { const currentItem = list.currentList?.currentItem; @@ -89,8 +88,8 @@ Item { Wallpapers.previewColourLock = true; Wallpapers.setWallpaper(currentItem.modelData.path); root.visibilities.launcher = false; - } else if (text.startsWith(Config.launcher.actionPrefix)) { - if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) + } else if (text.startsWith(GlobalConfig.launcher.actionPrefix)) { + if (text.startsWith(`${GlobalConfig.launcher.actionPrefix}calc `)) currentItem.onClicked(); else currentItem.modelData.onClicked(list.currentList); @@ -107,14 +106,14 @@ Item { Keys.onEscapePressed: root.visibilities.launcher = false Keys.onPressed: event => { - if (!Config.launcher.vimKeybinds) + if (!GlobalConfig.launcher.vimKeybinds) return; if (event.modifiers & Qt.ControlModifier) { - if (event.key === Qt.Key_J) { + if (event.key === Qt.Key_J || event.key === Qt.Key_N) { list.currentList?.incrementCurrentIndex(); event.accepted = true; - } else if (event.key === Qt.Key_K) { + } else if (event.key === Qt.Key_K || event.key === Qt.Key_P) { list.currentList?.decrementCurrentIndex(); event.accepted = true; } @@ -130,8 +129,6 @@ Item { Component.onCompleted: forceActiveFocus() Connections { - target: root.visibilities - function onLauncherChanged(): void { if (!root.visibilities.launcher) search.text = ""; @@ -141,6 +138,8 @@ Item { if (!root.visibilities.session) search.forceActiveFocus(); } + + target: root.visibilities } } @@ -177,13 +176,13 @@ Item { Behavior on width { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml index b2a9c7708..db7abdf92 100644 --- a/modules/launcher/ContentList.qml +++ b/modules/launcher/ContentList.qml @@ -1,26 +1,25 @@ pragma ComponentBehavior: Bound +import QtQuick +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick Item { id: root required property var content - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property var panels required property real maxHeight required property StyledTextField search required property int padding required property int rounding - readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) - readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item + readonly property bool showWallpapers: search.text.startsWith(`${GlobalConfig.launcher.actionPrefix}wallpaper `) + readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom @@ -33,7 +32,7 @@ Item { name: "apps" PropertyChanges { - root.implicitWidth: Config.launcher.sizes.itemWidth + root.implicitWidth: root.Tokens.sizes.launcher.itemWidth root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) appList.active: true } @@ -47,8 +46,8 @@ Item { name: "wallpapers" PropertyChanges { - root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) - root.implicitHeight: Config.launcher.sizes.wallpaperHeight + root.implicitWidth: Math.max(root.Tokens.sizes.launcher.itemWidth * 1.2, wallpaperList.implicitWidth) + root.implicitHeight: root.Tokens.sizes.launcher.wallpaperHeight wallpaperList.active: true } } @@ -61,7 +60,7 @@ Item { property: "opacity" from: 1 to: 0 - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } PropertyAction {} Anim { @@ -69,7 +68,7 @@ Item { property: "opacity" from: 0 to: 1 - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } } @@ -90,6 +89,7 @@ Item { Loader { id: wallpaperList + asynchronous: true active: false anchors.top: parent.top @@ -110,8 +110,8 @@ Item { opacity: root.currentList?.count === 0 ? 1 : 0 scale: root.currentList?.count === 0 ? 1 : 0.5 - spacing: Appearance.spacing.normal - padding: Appearance.padding.large + spacing: Tokens.spacing.normal + padding: Tokens.padding.large anchors.horizontalCenter: parent.horizontalCenter anchors.verticalCenter: parent.verticalCenter @@ -119,7 +119,7 @@ Item { MaterialIcon { text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } @@ -130,14 +130,14 @@ Item { StyledText { text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger font.weight: 500 } StyledText { text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } @@ -154,8 +154,8 @@ Item { enabled: root.visibilities.launcher Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + duration: Tokens.anim.durations.large + easing: Tokens.anim.emphasizedDecel } } @@ -163,8 +163,8 @@ Item { enabled: root.visibilities.launcher Anim { - duration: Appearance.anim.durations.large - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + duration: Tokens.anim.durations.large + easing: Tokens.anim.emphasizedDecel } } } diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml index 4aba4365b..3663f4bc4 100644 --- a/modules/launcher/WallpaperList.qml +++ b/modules/launcher/WallpaperList.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound import "items" +import QtQuick +import Quickshell +import Caelestia.Config import qs.components.controls import qs.services -import qs.config -import Quickshell -import QtQuick PathView { id: root @@ -15,10 +15,10 @@ PathView { required property var panels required property var content - readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 + readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 readonly property int numItems: { - const screen = QsWindow.window?.screen; + const screen = (QsWindow.window as QsWindow)?.screen; if (!screen) return 0; @@ -58,7 +58,7 @@ PathView { onCurrentItemChanged: { if (currentItem) - Wallpapers.preview(currentItem.modelData.path); + Wallpapers.preview((currentItem as WallpaperItem).modelData.path); } implicitWidth: Math.min(numItems, count) * itemWidth diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml index d62d726ab..d5630b233 100644 --- a/modules/launcher/Wrapper.qml +++ b/modules/launcher/Wrapper.qml @@ -1,111 +1,47 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config -import Quickshell import QtQuick +import Quickshell +import Caelestia.Config +import qs.components +import qs.modules.launcher.services Item { id: root required property ShellScreen screen - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities required property var panels readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled - property int contentHeight readonly property real maxHeight: { - let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; + let max = screen.height - Config.border.thickness * 2 - Tokens.spacing.large; if (visibilities.dashboard) max -= panels.dashboard.nonAnimHeight; return max; } - onMaxHeightChanged: timer.start() - - visible: height > 0 - implicitHeight: 0 - implicitWidth: content.implicitWidth + property real offsetScale: shouldBeActive ? 0 : 1 onShouldBeActiveChanged: { - if (shouldBeActive) { - timer.stop(); - hideAnim.stop(); - showAnim.start(); - } else { - showAnim.stop(); - hideAnim.start(); - } + if (shouldBeActive) + implicitHeight = Qt.binding(() => content.implicitHeight); + else + implicitHeight = implicitHeight; // Break binding during close anim } - SequentialAnimation { - id: showAnim - - Anim { - target: root - property: "implicitHeight" - to: root.contentHeight - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - ScriptAction { - script: root.implicitHeight = Qt.binding(() => content.implicitHeight) - } - } + visible: offsetScale < 1 + anchors.bottomMargin: (-implicitHeight - 5) * offsetScale + implicitHeight: content.implicitHeight + implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open + opacity: 1 - offsetScale - SequentialAnimation { - id: hideAnim + Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init - ScriptAction { - script: root.implicitHeight = root.implicitHeight - } + Behavior on offsetScale { Anim { - target: root - property: "implicitHeight" - to: 0 - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - - Connections { - target: Config.launcher - - function onEnabledChanged(): void { - timer.start(); - } - - function onMaxShownChanged(): void { - timer.start(); - } - } - - Connections { - target: DesktopEntries.applications - - function onValuesChanged(): void { - if (DesktopEntries.applications.values.length < Config.launcher.maxShown) - timer.start(); - } - } - - Timer { - id: timer - - interval: Appearance.anim.durations.extraLarge - onRunningChanged: { - if (running && !root.shouldBeActive) { - content.visible = false; - content.active = true; - } else { - root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; - if (showAnim.running) { - showAnim.stop(); - showAnim.start(); - } - } + type: Anim.DefaultSpatial } } @@ -115,16 +51,12 @@ Item { anchors.top: parent.top anchors.horizontalCenter: parent.horizontalCenter - visible: false - active: false - Component.onCompleted: timer.start() + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities panels: root.panels maxHeight: root.maxHeight - - Component.onCompleted: root.contentHeight = implicitHeight } } } diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml index e15802907..0f9fb5dd6 100644 --- a/modules/launcher/items/ActionItem.qml +++ b/modules/launcher/items/ActionItem.qml @@ -1,8 +1,7 @@ -import "../services" +import QtQuick +import Caelestia.Config import qs.components import qs.services -import qs.config -import QtQuick Item { id: root @@ -10,37 +9,34 @@ Item { required property var modelData required property var list - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { - root.modelData?.onClicked(root.list); - } + radius: Tokens.rounding.normal + onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller MaterialIcon { id: icon text: root.modelData?.icon ?? "" - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } Item { anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal anchors.verticalCenter: icon.verticalCenter implicitWidth: parent.width - icon.width @@ -50,18 +46,18 @@ Item { id: name text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { id: desc text: root.modelData?.desc ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - Appearance.rounding.normal * 2 + width: root.width - icon.width - Tokens.rounding.normal * 2 anchors.top: name.bottom } diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml index 48aace76d..80739c8f1 100644 --- a/modules/launcher/items/AppItem.qml +++ b/modules/launcher/items/AppItem.qml @@ -1,26 +1,26 @@ -import "../services" -import qs.components -import qs.services -import qs.config +import QtQuick import Quickshell import Quickshell.Widgets -import QtQuick +import Caelestia.Config +import qs.components +import qs.services +import qs.utils +import qs.modules.launcher.services Item { id: root required property DesktopEntry modelData - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { + radius: Tokens.rounding.normal + onClicked: { Apps.launch(root.modelData); root.visibilities.launcher = false; } @@ -28,13 +28,14 @@ Item { Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller IconImage { id: icon + asynchronous: true source: Quickshell.iconPath(root.modelData?.icon, "image-missing") implicitSize: parent.height * 0.8 @@ -43,31 +44,46 @@ Item { Item { anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal anchors.verticalCenter: icon.verticalCenter - implicitWidth: parent.width - icon.width + implicitWidth: parent.width - icon.width - favouriteIcon.width implicitHeight: name.implicitHeight + comment.implicitHeight StyledText { id: name text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { id: comment text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight - width: root.width - icon.width - Appearance.rounding.normal * 2 + width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.normal * 2 anchors.top: name.bottom } } + + Loader { + id: favouriteIcon + + asynchronous: true + anchors.verticalCenter: parent.verticalCenter + anchors.right: parent.right + active: root.modelData && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, root.modelData.id) + + sourceComponent: MaterialIcon { + text: "favorite" + fill: 1 + color: Colours.palette.m3primary + } + } } } diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml index 65489d9bc..f746f96c6 100644 --- a/modules/launcher/items/CalcItem.qml +++ b/modules/launcher/items/CalcItem.qml @@ -1,46 +1,48 @@ -import qs.components -import qs.services -import qs.config -import Caelestia -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import Caelestia +import Caelestia.Config +import qs.components +import qs.services Item { id: root required property var list - readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) + readonly property string math: list.search.text.slice(`${GlobalConfig.launcher.actionPrefix}calc `.length) function onClicked(): void { - Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); + Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); root.list.visibilities.launcher = false; } - implicitHeight: Config.launcher.sizes.itemHeight + onMathChanged: { + if (math.length > 0) + Qalculator.evalAsync(math); + } + + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { - root.onClicked(); - } + radius: Tokens.rounding.normal + onClicked: root.onClicked() } RowLayout { anchors.left: parent.left anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter - anchors.margins: Appearance.padding.larger + anchors.margins: Tokens.padding.larger - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal MaterialIcon { text: "function" - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge Layout.alignment: Qt.AlignVCenter } @@ -55,7 +57,7 @@ Item { return Colours.palette.m3onSurface; } - text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") + text: root.math.length > 0 ? (Qalculator.result || qsTr("Calculating...")) : qsTr("Type an expression to calculate") elide: Text.ElideLeft Layout.fillWidth: true @@ -64,23 +66,23 @@ Item { StyledRect { color: Colours.palette.m3tertiary - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal clip: true - implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 + implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small * 2 Layout.alignment: Qt.AlignVCenter StateLayer { id: stateLayer - color: Colours.palette.m3onTertiary - - function onClicked(): void { - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); + onClicked: { + Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); root.list.visibilities.launcher = false; } + + color: Colours.palette.m3onTertiary } StyledText { @@ -88,11 +90,11 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: icon.left - anchors.rightMargin: Appearance.spacing.small + anchors.rightMargin: Tokens.spacing.small text: qsTr("Open in calculator") color: Colours.palette.m3onTertiary - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal opacity: stateLayer.containsMouse ? 1 : 0 @@ -106,16 +108,16 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.right: parent.right - anchors.rightMargin: Appearance.padding.normal + anchors.rightMargin: Tokens.padding.normal text: "open_in_new" color: Colours.palette.m3onTertiary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } Behavior on implicitWidth { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } } diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml index 3ff184681..ab0784bd6 100644 --- a/modules/launcher/items/SchemeItem.qml +++ b/modules/launcher/items/SchemeItem.qml @@ -1,8 +1,8 @@ -import "../services" +import QtQuick +import Caelestia.Config import qs.components import qs.services -import qs.config -import QtQuick +import qs.modules.launcher.services Item { id: root @@ -10,24 +10,21 @@ Item { required property Schemes.Scheme modelData required property var list - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { - root.modelData?.onClicked(root.list); - } + radius: Tokens.rounding.normal + onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller StyledRect { id: preview @@ -38,7 +35,7 @@ Item { border.color: Qt.alpha(`#${root.modelData?.colours?.outline}`, 0.5) color: `#${root.modelData?.colours?.surface}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full implicitWidth: parent.height * 0.8 implicitHeight: parent.height * 0.8 @@ -57,27 +54,27 @@ Item { implicitWidth: preview.implicitWidth color: `#${root.modelData?.colours?.primary}` - radius: Appearance.rounding.full + radius: Tokens.rounding.full } } } Column { anchors.left: preview.right - anchors.leftMargin: Appearance.spacing.normal + anchors.leftMargin: Tokens.spacing.normal anchors.verticalCenter: parent.verticalCenter - width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) + width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) spacing: 0 StyledText { text: root.modelData?.flavour ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -89,6 +86,7 @@ Item { Loader { id: current + asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -97,7 +95,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml index 5c34fa89f..b13d5d8e4 100644 --- a/modules/launcher/items/VariantItem.qml +++ b/modules/launcher/items/VariantItem.qml @@ -1,8 +1,8 @@ -import "../services" +import QtQuick +import Caelestia.Config import qs.components import qs.services -import qs.config -import QtQuick +import qs.modules.launcher.services Item { id: root @@ -10,50 +10,47 @@ Item { required property M3Variants.Variant modelData required property var list - implicitHeight: Config.launcher.sizes.itemHeight + implicitHeight: Tokens.sizes.launcher.itemHeight anchors.left: parent?.left anchors.right: parent?.right StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { - root.modelData?.onClicked(root.list); - } + radius: Tokens.rounding.normal + onClicked: root.modelData?.onClicked(root.list) } Item { anchors.fill: parent - anchors.leftMargin: Appearance.padding.larger - anchors.rightMargin: Appearance.padding.larger - anchors.margins: Appearance.padding.smaller + anchors.leftMargin: Tokens.padding.larger + anchors.rightMargin: Tokens.padding.larger + anchors.margins: Tokens.padding.smaller MaterialIcon { id: icon text: root.modelData?.icon ?? "" - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge anchors.verticalCenter: parent.verticalCenter } Column { anchors.left: icon.right - anchors.leftMargin: Appearance.spacing.larger + anchors.leftMargin: Tokens.spacing.larger anchors.verticalCenter: icon.verticalCenter - width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) + width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) spacing: 0 StyledText { text: root.modelData?.name ?? "" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } StyledText { text: root.modelData?.description ?? "" - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small color: Colours.palette.m3outline elide: Text.ElideRight @@ -65,6 +62,7 @@ Item { Loader { id: current + asynchronous: true anchors.right: parent.right anchors.verticalCenter: parent.verticalCenter @@ -73,7 +71,7 @@ Item { sourceComponent: MaterialIcon { text: "check" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml index 9fdac3f38..d09a894c6 100644 --- a/modules/launcher/items/WallpaperItem.qml +++ b/modules/launcher/items/WallpaperItem.qml @@ -1,34 +1,33 @@ +import QtQuick +import Quickshell +import Caelestia.Config +import Caelestia.Models import qs.components import qs.components.effects import qs.components.images import qs.services -import qs.config -import Caelestia.Models -import Quickshell -import QtQuick Item { id: root required property FileSystemEntry modelData - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities scale: 0.5 opacity: 0 - z: PathView.z ?? 0 + z: PathView.z ?? 0 // qmllint disable missing-property Component.onCompleted: { scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); opacity = Qt.binding(() => PathView.onPath ? 1 : 0); } - implicitWidth: image.width + Appearance.padding.larger * 2 - implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal + implicitWidth: image.width + Tokens.padding.larger * 2 + implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal StateLayer { - radius: Appearance.rounding.normal - - function onClicked(): void { + radius: Tokens.rounding.normal + onClicked: { Wallpapers.setWallpaper(root.modelData.path); root.visibilities.launcher = false; } @@ -49,27 +48,29 @@ Item { id: image anchors.horizontalCenter: parent.horizontalCenter - y: Appearance.padding.large + y: Tokens.padding.large color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal - implicitWidth: Config.launcher.sizes.wallpaperWidth + implicitWidth: Tokens.sizes.launcher.wallpaperWidth implicitHeight: implicitWidth / 16 * 9 MaterialIcon { anchors.centerIn: parent text: "image" color: Colours.tPalette.m3outline - font.pointSize: Appearance.font.size.extraLarge * 2 + font.pointSize: Tokens.font.size.extraLarge * 2 font.weight: 600 } CachingImage { + anchors.fill: parent path: root.modelData.path smooth: !root.PathView.view.moving - cache: true - - anchors.fill: parent + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr); + } } } @@ -77,15 +78,15 @@ Item { id: label anchors.top: image.bottom - anchors.topMargin: Appearance.spacing.small / 2 + anchors.topMargin: Tokens.spacing.small / 2 anchors.horizontalCenter: parent.horizontalCenter - width: image.width - Appearance.padding.normal * 2 + width: image.width - Tokens.padding.normal * 2 horizontalAlignment: Text.AlignHCenter elide: Text.ElideRight renderType: Text.QtRendering text: root.modelData.relativePath - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } Behavior on scale { diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml index 5c1cb6bb8..634b3e6ad 100644 --- a/modules/launcher/services/Actions.qml +++ b/modules/launcher/services/Actions.qml @@ -1,26 +1,26 @@ pragma Singleton import ".." +import QtQuick +import Quickshell +import Caelestia.Config import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick Searcher { id: root function transformSearch(search: string): string { - return search.slice(Config.launcher.actionPrefix.length); + return search.slice(GlobalConfig.launcher.actionPrefix.length); } list: variants.instances - useFuzzy: Config.launcher.useFuzzy.actions + useFuzzy: GlobalConfig.launcher.useFuzzy.actions Variants { id: variants - model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false))) + model: GlobalConfig.launcher.actions.filter(a => (a.enabled ?? true) && (GlobalConfig.launcher.enableDangerousActions || !(a.dangerous ?? false))) Action {} } @@ -39,7 +39,7 @@ Searcher { return; if (command[0] === "autocomplete" && command.length > 1) { - list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; + list.search.text = `${GlobalConfig.launcher.actionPrefix}${command[1]} `; } else if (command[0] === "setMode" && command.length > 1) { list.visibilities.launcher = false; Colours.setMode(command[1]); diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml index c409a7bb0..168d9209d 100644 --- a/modules/launcher/services/Apps.qml +++ b/modules/launcher/services/Apps.qml @@ -1,9 +1,9 @@ pragma Singleton -import qs.config -import qs.utils -import Caelestia import Quickshell +import Caelestia +import Caelestia.Config +import qs.utils Searcher { id: root @@ -13,7 +13,7 @@ Searcher { if (entry.runInTerminal) Quickshell.execDetached({ - command: ["app2unit", "--", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], + command: ["app2unit", "--", ...GlobalConfig.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], workingDirectory: entry.workingDirectory }); else @@ -24,7 +24,7 @@ Searcher { } function search(search: string): list { - const prefix = Config.launcher.specialPrefix; + const prefix = GlobalConfig.launcher.specialPrefix; if (search.startsWith(`${prefix}i `)) { keys = ["id", "name"]; @@ -66,12 +66,13 @@ Searcher { } list: appDb.apps - useFuzzy: Config.launcher.useFuzzy.apps + useFuzzy: GlobalConfig.launcher.useFuzzy.apps AppDb { id: appDb path: `${Paths.state}/apps.sqlite` - entries: DesktopEntries.applications.values.filter(a => !Config.launcher.hiddenApps.includes(a.id)) + favouriteApps: GlobalConfig.launcher.favouriteApps + entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(GlobalConfig.launcher.hiddenApps, a.id)) } } diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml index 963a4d435..4517d02cb 100644 --- a/modules/launcher/services/M3Variants.qml +++ b/modules/launcher/services/M3Variants.qml @@ -1,16 +1,16 @@ pragma Singleton import ".." -import qs.config -import qs.utils -import Quickshell import QtQuick +import Quickshell +import Caelestia.Config +import qs.utils Searcher { id: root function transformSearch(search: string): string { - return search.slice(`${Config.launcher.actionPrefix}variant `.length); + return search.slice(`${GlobalConfig.launcher.actionPrefix}variant `.length); } list: [ @@ -69,7 +69,7 @@ Searcher { description: qsTr("All colours are grayscale, no chroma.") } ] - useFuzzy: Config.launcher.useFuzzy.variants + useFuzzy: GlobalConfig.launcher.useFuzzy.variants component Variant: QtObject { required property string variant diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml index dbb2dac0a..cc555f294 100644 --- a/modules/launcher/services/Schemes.qml +++ b/modules/launcher/services/Schemes.qml @@ -1,11 +1,11 @@ pragma Singleton import ".." -import qs.config -import qs.utils +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia.Config +import qs.utils Searcher { id: root @@ -14,7 +14,7 @@ Searcher { property string currentVariant function transformSearch(search: string): string { - return search.slice(`${Config.launcher.actionPrefix}scheme `.length); + return search.slice(`${GlobalConfig.launcher.actionPrefix}scheme `.length); } function selector(item: var): string { @@ -26,7 +26,7 @@ Searcher { } list: schemes.instances - useFuzzy: Config.launcher.useFuzzy.schemes + useFuzzy: GlobalConfig.launcher.useFuzzy.schemes keys: ["name", "flavour"] weights: [0.9, 0.1] @@ -55,7 +55,7 @@ Searcher { for (const f of s) flat.push(f); - schemes.model = flat.sort((a, b) => (a.name + a.flavour).localeCompare((b.name + b.flavour))); + schemes.model = flat.sort((a, b) => String(a.name + a.flavour).localeCompare((b.name + b.flavour))); } } } diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml index 19cf9d290..a987d4eb9 100644 --- a/modules/lock/Center.qml +++ b/modules/lock/Center.qml @@ -1,37 +1,37 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.images import qs.services -import qs.config import qs.utils -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root required property var lock readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) - readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale + readonly property int centerWidth: Tokens.sizes.lock.centerWidth * centerScale Layout.preferredWidth: centerWidth Layout.fillWidth: false Layout.fillHeight: true - spacing: Appearance.spacing.large * 2 + spacing: Tokens.spacing.large * 2 RowLayout { Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.alignment: Qt.AlignVCenter text: Time.hourStr color: Colours.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } @@ -39,8 +39,8 @@ ColumnLayout { Layout.alignment: Qt.AlignVCenter text: ":" color: Colours.palette.m3primary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } @@ -48,23 +48,24 @@ ColumnLayout { Layout.alignment: Qt.AlignVCenter text: Time.minuteStr color: Colours.palette.m3secondary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } Loader { - Layout.leftMargin: Appearance.spacing.small + asynchronous: true + Layout.leftMargin: Tokens.spacing.small Layout.alignment: Qt.AlignVCenter - active: Config.services.useTwelveHourClock + active: GlobalConfig.services.useTwelveHourClock visible: active sourceComponent: StyledText { text: Time.amPmStr color: Colours.palette.m3primary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * 2 * root.centerScale) - font.family: Appearance.font.family.clock + font.pointSize: Math.floor(Tokens.font.size.extraLarge * 2 * root.centerScale) + font.family: Tokens.font.family.clock font.bold: true } } @@ -72,24 +73,24 @@ ColumnLayout { StyledText { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: -Appearance.padding.large * 2 + Layout.topMargin: -Tokens.padding.large * 2 text: Time.format("dddd, d MMMM yyyy") color: Colours.palette.m3tertiary - font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) - font.family: Appearance.font.family.mono + font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale) + font.family: Tokens.font.family.mono font.bold: true } StyledClippingRect { - Layout.topMargin: Appearance.spacing.large * 2 + Layout.topMargin: Tokens.spacing.large * 2 Layout.alignment: Qt.AlignHCenter implicitWidth: root.centerWidth / 2 implicitHeight: root.centerWidth / 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full MaterialIcon { anchors.centerIn: parent @@ -97,6 +98,7 @@ ColumnLayout { text: "person" color: Colours.palette.m3onSurfaceVariant font.pointSize: Math.floor(root.centerWidth / 4) + visible: pfp.status !== Image.Ready } CachingImage { @@ -111,10 +113,10 @@ ColumnLayout { Layout.alignment: Qt.AlignHCenter implicitWidth: root.centerWidth * 0.8 - implicitHeight: input.implicitHeight + Appearance.padding.small * 2 + implicitHeight: input.implicitHeight + Tokens.padding.small * 2 color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full focus: true onActiveFocusChanged: { @@ -133,24 +135,24 @@ ColumnLayout { } StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor - - function onClicked(): void { + onClicked: { parent.forceActiveFocus(); } + + hoverEnabled: false + cursorShape: Qt.IBeamCursor } RowLayout { id: input anchors.fill: parent - anchors.margins: Appearance.padding.small - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.small + spacing: Tokens.spacing.normal Item { implicitWidth: implicitHeight - implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small * 2 MaterialIcon { id: fprintIcon @@ -158,13 +160,13 @@ ColumnLayout { anchors.centerIn: parent animate: true text: { - if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) + if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) return "fingerprint_off"; if (root.lock.pam.fprint.active) return "fingerprint"; return "lock"; } - color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface + color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface opacity: root.lock.pam.passwd.active ? 0 : 1 Behavior on opacity { @@ -186,17 +188,14 @@ ColumnLayout { StyledRect { implicitWidth: implicitHeight - implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 + implicitHeight: enterIcon.implicitHeight + Tokens.padding.small * 2 color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StateLayer { color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - - function onClicked(): void { - root.lock.pam.passwd.start(); - } + onClicked: root.lock.pam.passwd.start() } MaterialIcon { @@ -213,7 +212,7 @@ ColumnLayout { Item { Layout.fillWidth: true - Layout.topMargin: -Appearance.spacing.large + Layout.topMargin: -Tokens.spacing.large implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) @@ -270,7 +269,7 @@ ColumnLayout { color: Colours.palette.m3onSurfaceVariant animateProp: "opacity" - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono horizontalAlignment: Qt.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere lineHeight: 1.2 @@ -325,8 +324,8 @@ ColumnLayout { opacity: 0 color: Colours.palette.m3error - font.pointSize: Appearance.font.size.small - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.small + font.family: Tokens.font.family.mono horizontalAlignment: Qt.AlignHCenter wrapMode: Text.WrapAtWordBoundaryOrAnywhere @@ -355,8 +354,6 @@ ColumnLayout { } Connections { - target: root.lock.pam - function onFlashMsg(): void { exitAnim.stop(); if (message.scale < 1) @@ -364,6 +361,8 @@ ColumnLayout { else flashAnim.restart(); } + + target: root.lock.pam } Anim { @@ -395,13 +394,13 @@ ColumnLayout { target: message property: "scale" to: 0.7 - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } Anim { target: message property: "opacity" to: 0 - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } } } @@ -410,7 +409,7 @@ ColumnLayout { component FlashAnim: NumberAnimation { target: message property: "opacity" - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small easing.type: Easing.Linear } } diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml index a024ddc23..0ff7daad4 100644 --- a/modules/lock/Content.qml +++ b/modules/lock/Content.qml @@ -1,26 +1,26 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services RowLayout { id: root required property var lock - spacing: Appearance.spacing.large * 2 + spacing: Tokens.spacing.large * 2 ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { Layout.fillWidth: true implicitHeight: weather.implicitHeight - topLeftRadius: Appearance.rounding.large - radius: Appearance.rounding.small + topLeftRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer WeatherInfo { @@ -34,7 +34,7 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer Fetch {} @@ -44,8 +44,8 @@ RowLayout { Layout.fillWidth: true implicitHeight: media.implicitHeight - bottomLeftRadius: Appearance.rounding.large - radius: Appearance.rounding.small + bottomLeftRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer Media { @@ -62,14 +62,14 @@ RowLayout { ColumnLayout { Layout.fillWidth: true - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { Layout.fillWidth: true implicitHeight: resources.implicitHeight - topRightRadius: Appearance.rounding.large - radius: Appearance.rounding.small + topRightRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer Resources { @@ -81,8 +81,8 @@ RowLayout { Layout.fillWidth: true Layout.fillHeight: true - bottomRightRadius: Appearance.rounding.large - radius: Appearance.rounding.small + bottomRightRadius: Tokens.rounding.large + radius: Tokens.rounding.small color: Colours.tPalette.m3surfaceContainer NotifDock { diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml index ded56084b..0afbe4557 100644 --- a/modules/lock/Fetch.qml +++ b/modules/lock/Fetch.qml @@ -1,41 +1,41 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell.Services.UPower +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import qs.config import qs.utils -import Quickshell.Services.UPower -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root anchors.fill: parent - anchors.margins: Appearance.padding.large * 2 - anchors.topMargin: Appearance.padding.large + anchors.margins: Tokens.padding.large * 2 + anchors.topMargin: Tokens.padding.large - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { Layout.fillWidth: true Layout.fillHeight: false - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { - implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2 + implicitWidth: prompt.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: prompt.implicitHeight + Tokens.padding.normal * 2 color: Colours.palette.m3primary - radius: Appearance.rounding.small + radius: Tokens.rounding.small MonoText { id: prompt anchors.centerIn: parent text: ">" - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal color: Colours.palette.m3onPrimary } } @@ -43,7 +43,7 @@ ColumnLayout { MonoText { Layout.fillWidth: true text: "caelestiafetch.sh" - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal elide: Text.ElideRight } @@ -51,7 +51,7 @@ ColumnLayout { Layout.fillHeight: true active: !iconLoader.active - sourceComponent: OsLogo {} + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } } @@ -66,15 +66,15 @@ ColumnLayout { Layout.fillHeight: true active: root.width > 320 - sourceComponent: OsLogo {} + sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon } ColumnLayout { Layout.fillWidth: true - Layout.topMargin: Appearance.padding.normal - Layout.bottomMargin: Appearance.padding.normal + Layout.topMargin: Tokens.padding.normal + Layout.bottomMargin: Tokens.padding.normal Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal WrappedLoader { Layout.fillWidth: true @@ -125,41 +125,54 @@ ColumnLayout { active: root.height > 180 sourceComponent: RowLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Repeater { - model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large))) + model: Math.max(0, Math.min(8, root.width / (Tokens.font.size.larger * 2 + Tokens.spacing.large))) StyledRect { required property int index implicitWidth: implicitHeight - implicitHeight: Appearance.font.size.larger * 2 + implicitHeight: Tokens.font.size.larger * 2 color: Colours.palette[`term${index}`] - radius: Appearance.rounding.small + radius: Tokens.rounding.small } } } } - component WrappedLoader: Loader { - visible: active + Component { + id: caelestiaLogo + + Logo { + width: height + } + } + + Component { + id: distroIcon + + ColouredIcon { + source: SysInfo.osLogo + implicitSize: height + colour: Colours.palette.m3primary + layer.enabled: Config.lock.recolourLogo + } } - component OsLogo: ColouredIcon { - source: SysInfo.osLogo - implicitSize: height - colour: Colours.palette.m3primary - layer.enabled: Config.lock.recolourLogo || SysInfo.isDefaultLogo + component WrappedLoader: Loader { + asynchronous: true + visible: active } component FetchText: MonoText { Layout.fillWidth: true - font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal + font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal elide: Text.ElideRight } component MonoText: StyledText { - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono } } diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml index 358093f39..1b6c34a73 100644 --- a/modules/lock/InputField.qml +++ b/modules/lock/InputField.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.services Item { id: root @@ -20,8 +20,6 @@ Item { clip: true Connections { - target: root.pam - function onBufferChanged(): void { if (root.pam.buffer.length > root.buffer.length) { charList.bindImWidth(); @@ -32,6 +30,8 @@ Item { root.buffer = root.pam.buffer; } + + target: root.pam } StyledText { @@ -49,8 +49,8 @@ Item { animate: true color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono opacity: root.buffer ? 0 : 1 @@ -74,10 +74,10 @@ Item { anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 implicitWidth: fullWidth - implicitHeight: Appearance.font.size.normal + implicitHeight: Tokens.font.size.normal orientation: Qt.Horizontal - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 interactive: false model: ScriptModel { @@ -91,7 +91,7 @@ Item { implicitHeight: charList.implicitHeight color: Colours.palette.m3onSurface - radius: Appearance.rounding.small / 2 + radius: Tokens.rounding.small / 2 opacity: 0 scale: 0 @@ -134,8 +134,7 @@ Item { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml index 6fd5277fe..f852cb7ff 100644 --- a/modules/lock/Lock.qml +++ b/modules/lock/Lock.qml @@ -1,9 +1,10 @@ pragma ComponentBehavior: Bound -import qs.components.misc +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland +import qs.components.misc Scope { property alias lock: lock @@ -25,21 +26,37 @@ Scope { lock: lock } + Loader { + asynchronous: true + active: true + onLoaded: active = false + + // Force a load of a screencopy so the one in the lock works + // My guess is the ICC backend loads async on first request, which if the lock is + // the first request it fails to capture (because it's async and the compositor + // refuses capture when locked) + sourceComponent: ScreencopyView { + captureSource: Quickshell.screens[0] + } + } + + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "lock" description: "Lock the current session" onPressed: lock.locked = true } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "unlock" description: "Unlock the current session" onPressed: lock.unlock() } IpcHandler { - target: "lock" - function lock(): void { lock.locked = true; } @@ -51,5 +68,7 @@ Scope { function isLocked(): bool { return lock.locked; } + + target: "lock" } } diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml index 279c55138..322773fd8 100644 --- a/modules/lock/LockSurface.qml +++ b/modules/lock/LockSurface.qml @@ -1,11 +1,11 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Quickshell.Wayland import QtQuick import QtQuick.Effects +import Quickshell.Wayland +import Caelestia.Config +import qs.components +import qs.services WlSessionLockSurface { id: root @@ -15,14 +15,17 @@ WlSessionLockSurface { readonly property alias unlocking: unlockAnim.running + contentItem.Config.screen: screen.name + contentItem.Tokens.screen: screen.name + color: "transparent" Connections { - target: root.lock - function onUnlock(): void { unlockAnim.start(); } + + target: root.lock } SequentialAnimation { @@ -33,8 +36,7 @@ WlSessionLockSurface { target: lockContent properties: "implicitWidth,implicitHeight" to: lockContent.size - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } Anim { target: lockBg @@ -45,30 +47,29 @@ WlSessionLockSurface { target: content property: "scale" to: 0 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } Anim { target: content property: "opacity" to: 0 - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } Anim { target: lockIcon property: "opacity" to: 1 - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } Anim { target: background property: "opacity" to: 0 - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } SequentialAnimation { PauseAnimation { - duration: Appearance.anim.durations.small + duration: Tokens.anim.durations.small } Anim { target: lockContent @@ -93,7 +94,7 @@ WlSessionLockSurface { target: background property: "opacity" to: 1 - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } SequentialAnimation { ParallelAnimation { @@ -101,15 +102,14 @@ WlSessionLockSurface { target: lockContent property: "scale" to: 1 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } Anim { target: lockContent property: "rotation" to: 360 - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.standardAccel + duration: Tokens.anim.durations.expressiveFastSpatial + easing: Tokens.anim.standardAccel } } ParallelAnimation { @@ -117,7 +117,7 @@ WlSessionLockSurface { target: lockIcon property: "rotation" to: 360 - easing.bezierCurve: Appearance.anim.curves.standardDecel + easing: Tokens.anim.standardDecel } Anim { target: lockIcon @@ -133,27 +133,24 @@ WlSessionLockSurface { target: content property: "scale" to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } Anim { target: lockBg property: "radius" - to: Appearance.rounding.large * 1.5 + to: lockContent.Tokens.rounding.large * 1.5 } Anim { target: lockContent property: "implicitWidth" - to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio + type: Anim.DefaultSpatial } Anim { target: lockContent property: "implicitHeight" - to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult + type: Anim.DefaultSpatial } } } @@ -179,8 +176,8 @@ WlSessionLockSurface { Item { id: lockContent - readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 - readonly property int radius: size / 4 * Appearance.rounding.scale + readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 + readonly property int radius: size / 4 * Tokens.rounding.scale anchors.centerIn: parent implicitWidth: size @@ -210,7 +207,7 @@ WlSessionLockSurface { anchors.centerIn: parent text: "lock" - font.pointSize: Appearance.font.size.extraLarge * 4 + font.pointSize: Tokens.font.size.extraLarge * 4 font.bold: true rotation: 180 } @@ -219,8 +216,8 @@ WlSessionLockSurface { id: content anchors.centerIn: parent - width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 - height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 + width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 + height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 lock: root opacity: 0 diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml index b7e58bbcb..c55333c7f 100644 --- a/modules/lock/Media.qml +++ b/modules/lock/Media.qml @@ -1,11 +1,12 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts Item { id: root @@ -18,12 +19,14 @@ Item { Image { anchors.fill: parent - source: Players.active?.trackArtUrl ?? "" + source: Players.getArtUrl(Players.active) asynchronous: true fillMode: Image.PreserveAspectCrop - sourceSize.width: width - sourceSize.height: height + sourceSize: { + const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; + return Qt.size(width * dpr, height * dpr); + } layer.enabled: true layer.effect: OpacityMask { @@ -34,7 +37,7 @@ Item { Behavior on opacity { Anim { - duration: Appearance.anim.durations.extraLarge + type: Anim.StandardExtraLarge } } } @@ -69,14 +72,14 @@ Item { anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large StyledText { - Layout.topMargin: Appearance.padding.large - Layout.bottomMargin: Appearance.spacing.larger + Layout.topMargin: Tokens.padding.large + Layout.bottomMargin: Tokens.spacing.larger text: qsTr("Now playing") color: Colours.palette.m3onSurfaceVariant - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono font.weight: 500 } @@ -86,8 +89,8 @@ Item { text: Players.active?.trackArtist ?? qsTr("No media") color: Colours.palette.m3primary horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.mono font.weight: 600 elide: Text.ElideRight } @@ -97,22 +100,21 @@ Item { animate: true text: Players.active?.trackTitle ?? qsTr("No media") horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.larger - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.larger + font.family: Tokens.font.family.mono elide: Text.ElideRight } RowLayout { Layout.alignment: Qt.AlignHCenter - Layout.topMargin: Appearance.spacing.large * 1.2 - Layout.bottomMargin: Appearance.padding.large + Layout.topMargin: Tokens.spacing.large * 1.2 + Layout.bottomMargin: Tokens.padding.large - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large PlayerControl { icon: "skip_previous" - - function onClicked(): void { + onClicked: { if (Players.active?.canGoPrevious) Players.active.previous(); } @@ -124,8 +126,7 @@ Item { colour: "Primary" level: active ? 2 : 1 active: Players.active?.isPlaying ?? false - - function onClicked(): void { + onClicked: { if (Players.active?.canTogglePlaying) Players.active.togglePlaying(); } @@ -133,8 +134,7 @@ Item { PlayerControl { icon: "skip_next" - - function onClicked(): void { + onClicked: { if (Players.active?.canGoNext) Players.active.next(); } @@ -151,15 +151,14 @@ Item { property string colour: "Secondary" property int level: 1 - function onClicked(): void { - } + signal clicked - Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) - implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 - implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2 + Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : active ? Tokens.padding.small * 2 : 0) + implicitWidth: controlIcon.implicitWidth + Tokens.padding.large * 2 + implicitHeight: controlIcon.implicitHeight + Tokens.padding.normal * 2 color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`] - radius: active || controlState.pressed ? Appearance.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Appearance.rounding.scale) + radius: active || controlState.pressed ? Tokens.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale) Elevation { anchors.fill: parent @@ -172,10 +171,7 @@ Item { id: controlState color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - - function onClicked(): void { - control.onClicked(); - } + onClicked: control.clicked() } MaterialIcon { @@ -183,7 +179,7 @@ Item { anchors.centerIn: parent color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large fill: control.active ? 1 : 0 Behavior on fill { @@ -193,15 +189,13 @@ Item { Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on radius { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml index 01f7e4b47..2254dfe0b 100644 --- a/modules/lock/NotifDock.qml +++ b/modules/lock/NotifDock.qml @@ -1,14 +1,15 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.effects import qs.services -import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts +import qs.utils ColumnLayout { id: root @@ -16,15 +17,15 @@ ColumnLayout { required property var lock anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.family: Appearance.font.family.mono + font.family: Tokens.font.family.mono font.weight: 500 elide: Text.ElideRight } @@ -35,22 +36,23 @@ ColumnLayout { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: "transparent" Loader { + asynchronous: true anchors.centerIn: parent active: opacity > 0 - opacity: Notifs.list.length > 0 ? 0 : 1 + opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Image { asynchronous: true - source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) + source: Paths.absolutePath(Config.paths.lockNoNotifsPic) fillMode: Image.PreserveAspectFit - sourceSize.width: clipRect.width * 0.8 + sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) layer.enabled: true layer.effect: Colouriser { @@ -61,25 +63,25 @@ ColumnLayout { StyledText { Layout.alignment: Qt.AlignHCenter - text: qsTr("No Notifications") + text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") color: Colours.palette.m3outlineVariant - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.mono font.weight: 500 } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.extraLarge + type: Anim.StandardExtraLarge } } } StyledListView { anchors.fill: parent - - spacing: Appearance.spacing.small + visible: !Config.lock.hideNotifs + spacing: Tokens.spacing.small clip: true model: ScriptModel { @@ -101,8 +103,7 @@ ColumnLayout { property: "scale" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -124,8 +125,7 @@ ColumnLayout { } Anim { property: "y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } @@ -136,8 +136,7 @@ ColumnLayout { } Anim { property: "y" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml index 779609065..971b921a0 100644 --- a/modules/lock/NotifGroup.qml +++ b/modules/lock/NotifGroup.qml @@ -1,15 +1,15 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Services.Notifications +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts StyledRect { id: root @@ -17,18 +17,39 @@ StyledRect { required property string modelData readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) - readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" - readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" - readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" + readonly property var props: { + let img = ""; + let icon = ""; + let hasCritical = false; + let hasNormal = false; + for (const n of notifs) { + if (!img && n.image.length > 0) + img = n.image; + if (!icon && n.appIcon.length > 0) + icon = n.appIcon; + if (n.urgency === NotificationUrgency.Critical) + hasCritical = true; + else if (n.urgency === NotificationUrgency.Normal) + hasNormal = true; + } + return { + img, + icon, + urgency: hasCritical ? "critical" : hasNormal ? "normal" : "low" + }; + } + readonly property string image: props.img + readonly property string appIcon: props.icon + readonly property string urgency: props.urgency property bool expanded anchors.left: parent?.left anchors.right: parent?.right - implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 clip: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) RowLayout { @@ -37,14 +58,14 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image Component { id: imageComp @@ -52,10 +73,14 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image + width: TokenConfig.sizes.notifs.image + height: TokenConfig.sizes.notifs.image } } @@ -63,7 +88,7 @@ StyledRect { id: appIconComp ColouredIcon { - implicitSize: Math.round(Config.notifs.sizes.image * 0.6) + implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") @@ -76,36 +101,38 @@ StyledRect { MaterialIcon { text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } ClippingRectangle { anchors.fill: parent color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full Loader { + asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { + asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image sourceComponent: StyledRect { - implicitWidth: Config.notifs.sizes.badge - implicitHeight: Config.notifs.sizes.badge + implicitWidth: Tokens.sizes.notifs.badge + implicitHeight: Tokens.sizes.notifs.badge color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full ColouredIcon { anchors.centerIn: parent - implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) + implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") @@ -115,21 +142,21 @@ StyledRect { } ColumnLayout { - Layout.topMargin: -Appearance.padding.small - Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing) + Layout.topMargin: -Tokens.padding.small + Layout.bottomMargin: -Tokens.padding.small / 2 - (root.expanded ? 0 : spacing) Layout.fillWidth: true - spacing: Math.round(Appearance.spacing.small / 2) + spacing: Math.round(Tokens.spacing.small / 2) RowLayout { Layout.bottomMargin: -parent.spacing Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } @@ -137,45 +164,42 @@ StyledRect { animate: true text: root.notifs[0]?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Appearance.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 + implicitHeight: groupCount.implicitHeight + Tokens.padding.small color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - radius: Appearance.rounding.full + radius: Tokens.rounding.full opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0 Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 StateLayer { color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface - - function onClicked(): void { - root.expanded = !root.expanded; - } + onClicked: root.expanded = !root.expanded } RowLayout { id: expandBtn anchors.centerIn: parent - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { id: groupCount - Layout.leftMargin: Appearance.padding.small / 2 + Layout.leftMargin: Tokens.padding.small / 2 animate: true text: root.notifs.length color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } MaterialIcon { - Layout.rightMargin: -Appearance.padding.small / 2 + Layout.rightMargin: -Tokens.padding.small / 2 animate: true text: root.expanded ? "expand_less" : "expand_more" color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface @@ -194,7 +218,7 @@ StyledRect { Repeater { model: ScriptModel { - values: root.notifs.slice(0, Config.notifs.groupPreviewNum) + values: root.notifs.slice(0, root.Config.notifs.groupPreviewNum) } NotifLine { @@ -247,6 +271,7 @@ StyledRect { } Loader { + asynchronous: true Layout.fillWidth: true opacity: root.expanded ? 1 : 0 @@ -256,7 +281,7 @@ StyledRect { sourceComponent: ColumnLayout { Repeater { model: ScriptModel { - values: root.notifs.slice(Config.notifs.groupPreviewNum) + values: root.notifs.slice(root.Config.notifs.groupPreviewNum) } NotifLine {} @@ -272,15 +297,14 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } component NotifLine: StyledText { id: notifLine - required property Notifs.Notif modelData + required property NotifData modelData Layout.fillWidth: true textFormat: Text.MarkdownText diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml index 0186c2f84..f4c617473 100644 --- a/modules/lock/Pam.qml +++ b/modules/lock/Pam.qml @@ -1,9 +1,9 @@ -import qs.config +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Wayland import Quickshell.Services.Pam -import QtQuick +import Caelestia.Config Scope { id: root @@ -31,8 +31,8 @@ Scope { } else { buffer = buffer.slice(0, -1); } - } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { - // No illegal characters (you are insane if you use unicode in your password) + } else if (/^[^\x00-\x1F\x7F-\x9F]+$/.test(event.text)) { + // Allow anything except control characters buffer += event.text; } } @@ -82,7 +82,7 @@ Scope { property int errorTries function checkAvail(): void { - if (!available || !Config.lock.enableFprint || !root.lock.secure) { + if (!available || !GlobalConfig.lock.enableFprint || !root.lock.secure) { abort(); return; } @@ -113,7 +113,7 @@ Scope { // Isn't actually the real max tries as pam only reports completed // when max tries is reached. tries++; - if (tries < Config.lock.maxFprintTries) { + if (tries < GlobalConfig.lock.maxFprintTries) { // Restart if not actually real max tries root.fprintState = "fail"; start(); @@ -132,7 +132,7 @@ Scope { id: availProc command: ["sh", "-c", "fprintd-list $USER"] - onExited: code => { + onExited: code => { // qmllint disable signal-handler-parameters fprint.available = code === 0; fprint.checkAvail(); } @@ -166,8 +166,6 @@ Scope { } Connections { - target: root.lock - function onSecureChanged(): void { if (root.lock.secure) { availProc.running = true; @@ -181,13 +179,15 @@ Scope { function onUnlock(): void { fprint.abort(); } + + target: root.lock } Connections { - target: Config.lock - function onEnableFprintChanged(): void { fprint.checkAvail(); } + + target: GlobalConfig.lock } } diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml index 82c004c22..1f38aa00d 100644 --- a/modules/lock/Resources.qml +++ b/modules/lock/Resources.qml @@ -1,20 +1,20 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.components.misc import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts GridLayout { id: root anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - rowSpacing: Appearance.spacing.large - columnSpacing: Appearance.spacing.large + rowSpacing: Tokens.spacing.large + columnSpacing: Tokens.spacing.large rows: 2 columns: 2 @@ -23,28 +23,28 @@ GridLayout { } Resource { - Layout.topMargin: Appearance.padding.large + Layout.topMargin: Tokens.padding.large icon: "memory" value: SystemUsage.cpuPerc colour: Colours.palette.m3primary } Resource { - Layout.topMargin: Appearance.padding.large + Layout.topMargin: Tokens.padding.large icon: "thermostat" value: Math.min(1, SystemUsage.cpuTemp / 90) colour: Colours.palette.m3secondary } Resource { - Layout.bottomMargin: Appearance.padding.large + Layout.bottomMargin: Tokens.padding.large icon: "memory_alt" value: SystemUsage.memPerc colour: Colours.palette.m3secondary } Resource { - Layout.bottomMargin: Appearance.padding.large + Layout.bottomMargin: Tokens.padding.large icon: "hard_disk" value: SystemUsage.storagePerc colour: Colours.palette.m3tertiary @@ -61,17 +61,17 @@ GridLayout { implicitHeight: width color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - radius: Appearance.rounding.large + radius: Tokens.rounding.large CircularProgress { id: circ anchors.fill: parent value: res.value - padding: Appearance.padding.large * 3 + padding: Tokens.padding.large * 3 fgColour: res.colour bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) - strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal + strokeWidth: width < 200 ? Tokens.padding.smaller : Tokens.padding.normal } MaterialIcon { @@ -86,7 +86,7 @@ GridLayout { Behavior on value { Anim { - duration: Appearance.anim.durations.large + type: Anim.StandardLarge } } } diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml index d6c25af29..d96a54dfb 100644 --- a/modules/lock/WeatherInfo.qml +++ b/modules/lock/WeatherInfo.qml @@ -1,11 +1,10 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import qs.utils import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root @@ -14,13 +13,14 @@ ColumnLayout { anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large * 2 + anchors.margins: Tokens.padding.large * 2 - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Loader { - Layout.topMargin: Appearance.padding.large * 2 - Layout.bottomMargin: -Appearance.padding.large + asynchronous: true + Layout.topMargin: Tokens.padding.large * 2 + Layout.bottomMargin: -Tokens.padding.large Layout.alignment: Qt.AlignHCenter active: root.rootHeight > 610 @@ -29,24 +29,24 @@ ColumnLayout { sourceComponent: StyledText { text: qsTr("Weather") color: Colours.palette.m3primary - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 } } RowLayout { Layout.fillWidth: true - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large MaterialIcon { animate: true text: Weather.icon color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.extraLarge * 2.5 + font.pointSize: Tokens.font.size.extraLarge * 2.5 } ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true @@ -54,7 +54,7 @@ ColumnLayout { animate: true text: Weather.description color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 elide: Text.ElideRight } @@ -65,18 +65,19 @@ ColumnLayout { animate: true text: qsTr("Humidity: %1%").arg(Weather.humidity) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } } Loader { - Layout.rightMargin: Appearance.padding.smaller + asynchronous: true + Layout.rightMargin: Tokens.padding.smaller active: root.width > 400 visible: active sourceComponent: ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true @@ -85,7 +86,7 @@ ColumnLayout { text: Weather.temp color: Colours.palette.m3primary horizontalAlignment: Text.AlignRight - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 elide: Text.ElideLeft } @@ -97,7 +98,7 @@ ColumnLayout { text: qsTr("Feels like: %1").arg(Weather.feelsLike) color: Colours.palette.m3outline horizontalAlignment: Text.AlignRight - font.pointSize: Appearance.font.size.smaller + font.pointSize: Tokens.font.size.smaller elide: Text.ElideLeft } } @@ -107,15 +108,16 @@ ColumnLayout { Loader { id: forecastLoader - Layout.topMargin: Appearance.spacing.smaller - Layout.bottomMargin: Appearance.padding.large * 2 + asynchronous: true + Layout.topMargin: Tokens.spacing.smaller + Layout.bottomMargin: Tokens.padding.large * 2 Layout.fillWidth: true active: root.rootHeight > 820 visible: active sourceComponent: RowLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Repeater { model: { @@ -135,7 +137,7 @@ ColumnLayout { required property var modelData Layout.fillWidth: true - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small StyledText { Layout.fillWidth: true @@ -145,21 +147,21 @@ ColumnLayout { } color: Colours.palette.m3outline horizontalAlignment: Text.AlignHCenter - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger } MaterialIcon { Layout.alignment: Qt.AlignHCenter text: forecastHour.modelData?.icon ?? "cloud_alert" - font.pointSize: Appearance.font.size.extraLarge * 1.5 + font.pointSize: Tokens.font.size.extraLarge * 1.5 font.weight: 500 } StyledText { Layout.alignment: Qt.AlignHCenter - text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` + text: GlobalConfig.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` color: Colours.palette.m3secondary - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger } } } diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml index 2d4590e0e..aeaa7deaf 100644 --- a/modules/notifications/Content.qml +++ b/modules/notifications/Content.qml @@ -1,47 +1,47 @@ +import QtQuick +import Quickshell +import Quickshell.Widgets +import Caelestia.Config +import qs.components import qs.components.containers import qs.components.widgets import qs.services -import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick Item { id: root - required property PersistentProperties visibilities - required property Item panels - readonly property int padding: Appearance.padding.large + required property DrawerVisibilities visibilities + required property Item osdPanel + required property Item sessionPanel + readonly property int padding: Tokens.padding.large anchors.top: parent.top anchors.bottom: parent.bottom anchors.right: parent.right - implicitWidth: Config.notifs.sizes.width + padding * 2 + implicitWidth: Tokens.sizes.notifs.width + padding * 2 implicitHeight: { const count = list.count; if (count === 0) return 0; - let height = (count - 1) * Appearance.spacing.smaller; + let height = (count - 1) * Tokens.spacing.smaller; for (let i = 0; i < count; i++) - height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; + height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; - if (visibilities && panels) { - if (visibilities.osd) { - const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; - } + if (visibilities.osd) { + const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; + } - if (visibilities.session) { - const h = panels.session.y - Config.border.rounding * 2 - padding * 2; - if (height > h) - height = h; - } + if (visibilities.session) { + const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; + if (height > h) + height = h; } - return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); + return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); } ClippingWrapperRectangle { @@ -49,7 +49,7 @@ Item { anchors.margins: root.padding color: "transparent" - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal StyledListView { id: list @@ -62,79 +62,9 @@ Item { orientation: Qt.Vertical spacing: 0 - cacheBuffer: QsWindow.window?.screen.height ?? 0 - - delegate: Item { - id: wrapper - - required property Notifs.Notif modelData - required property int index - readonly property alias nonAnimHeight: notif.nonAnimHeight - property int idx - - onIndexChanged: { - if (index !== -1) - idx = index; - } - - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) - - ListView.onRemove: removeAnim.start() - - SequentialAnimation { - id: removeAnim - - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: true - } - PropertyAction { - target: wrapper - property: "enabled" - value: false - } - PropertyAction { - target: wrapper - property: "implicitHeight" - value: 0 - } - PropertyAction { - target: wrapper - property: "z" - value: 1 - } - Anim { - target: notif - property: "x" - to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 - duration: Appearance.anim.durations.normal - easing.bezierCurve: Appearance.anim.curves.emphasized - } - PropertyAction { - target: wrapper - property: "ListView.delayRemove" - value: false - } - } - - ClippingRectangle { - anchors.top: parent.top - anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller - - color: "transparent" - radius: notif.radius - implicitWidth: notif.implicitWidth - implicitHeight: notif.implicitHeight + cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0 - Notification { - id: notif - - modelData: wrapper.modelData - } - } - } + delegate: NotifWrapper {} move: Transition { Anim { @@ -159,9 +89,9 @@ Item { let height = 0; for (let i = 0; i < count; i++) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; - if (height - Appearance.spacing.smaller >= scrollY) + if (height - Tokens.spacing.smaller >= scrollY) return i; } @@ -180,9 +110,9 @@ Item { let height = 0; for (let i = count - 1; i >= 0; i--) { - height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; + height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; - if (height - Appearance.spacing.smaller >= scrollY) + if (height - Tokens.spacing.smaller >= scrollY) return count - i - 1; } @@ -196,9 +126,80 @@ Item { Anim {} } + component NotifWrapper: Item { + id: wrapper + + required property NotifData modelData + required property int index + readonly property alias nonAnimHeight: notif.nonAnimHeight + property int idx + + onIndexChanged: { + if (index !== -1) + idx = index; + } + + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.smaller) + + ListView.onRemove: removeAnim.start() + + SequentialAnimation { + id: removeAnim + + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: true + } + PropertyAction { + target: wrapper + property: "enabled" + value: false + } + PropertyAction { + target: wrapper + property: "implicitHeight" + value: 0 + } + PropertyAction { + target: wrapper + property: "z" + value: 1 + } + Anim { + target: notif + property: "x" + to: (notif.x >= 0 ? wrapper.Tokens.sizes.notifs.width : -wrapper.Tokens.sizes.notifs.width) * 2 + duration: Tokens.anim.durations.normal + easing: Tokens.anim.emphasized + } + PropertyAction { + target: wrapper + property: "ListView.delayRemove" + value: false + } + } + + ClippingRectangle { + anchors.top: parent.top + anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.smaller + + color: "transparent" + radius: notif.radius + implicitWidth: notif.implicitWidth + implicitHeight: notif.implicitHeight + + Notification { + id: notif + + modelData: wrapper.modelData + } + } + } + component Anim: NumberAnimation { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + duration: Tokens.anim.durations.expressiveDefaultSpatial + easing: Tokens.anim.expressiveDefaultSpatial } } diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml index 8c2d3ec22..fe6fd0567 100644 --- a/modules/notifications/Notification.qml +++ b/modules/notifications/Notification.qml @@ -1,31 +1,32 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import Quickshell +import Quickshell.Services.Notifications +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import qs.config import qs.utils -import Quickshell -import Quickshell.Widgets -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts StyledRect { id: root - required property Notifs.Notif modelData + required property NotifData modelData readonly property bool hasImage: modelData.image.length > 0 readonly property bool hasAppIcon: modelData.appIcon.length > 0 + readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 property bool expanded: Config.notifs.openExpanded color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal - implicitWidth: Config.notifs.sizes.width + radius: Tokens.rounding.normal + implicitWidth: Tokens.sizes.notifs.width implicitHeight: inner.implicitHeight - x: Config.notifs.sizes.width + x: Tokens.sizes.notifs.width Component.onCompleted: { x = 0; modelData.lock(this); @@ -34,7 +35,7 @@ StyledRect { Behavior on x { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasizedDecel + easing: Tokens.anim.emphasizedDecel } } @@ -66,7 +67,7 @@ StyledRect { if (!containsMouse) root.modelData.timer.start(); - if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) + if (Math.abs(root.x) < Tokens.sizes.notifs.width * Config.notifs.clearThreshold) root.x = 0; else root.modelData.popup = false; @@ -79,11 +80,11 @@ StyledRect { } } onClicked: event => { - if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) + if (!GlobalConfig.notifs.actionOnClick || event.button !== Qt.LeftButton) return; const actions = root.modelData.actions; - if (actions?.length === 1) + if (actions.length === 1) actions[0].invoke(); } @@ -93,37 +94,42 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal implicitHeight: root.nonAnimHeight Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Loader { id: image + asynchronous: true active: root.hasImage anchors.left: parent.left anchors.top: parent.top - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image + width: TokenConfig.sizes.notifs.image + height: TokenConfig.sizes.notifs.image visible: root.hasImage || root.hasAppIcon - sourceComponent: ClippingRectangle { - radius: Appearance.rounding.full - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + sourceComponent: StyledClippingRect { + radius: Tokens.rounding.full + color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image Image { anchors.fill: parent source: Qt.resolvedUrl(root.modelData.image) fillMode: Image.PreserveAspectCrop + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true } @@ -133,6 +139,7 @@ StyledRect { Loader { id: appIcon + asynchronous: true active: root.hasAppIcon || !root.hasImage anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter @@ -141,14 +148,15 @@ StyledRect { anchors.bottom: root.hasImage ? image.bottom : undefined sourceComponent: StyledRect { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer - implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image - implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image + implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image + implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image Loader { id: icon + asynchronous: true active: root.hasAppIcon anchors.centerIn: parent @@ -165,16 +173,53 @@ StyledRect { } Loader { + asynchronous: true active: !root.hasAppIcon anchors.centerIn: parent - anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 - anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 + anchors.horizontalCenterOffset: -Tokens.font.size.large * 0.02 + anchors.verticalCenterOffset: Tokens.font.size.large * 0.02 sourceComponent: MaterialIcon { text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large + } + } + } + } + + Shape { + id: progressIndicator + + anchors.centerIn: appIcon + width: appIcon.implicitWidth + progressShape.strokeWidth * 2 + height: appIcon.implicitHeight + progressShape.strokeWidth * 2 + preferredRendererType: Shape.CurveRenderer + + ShapePath { + id: progressShape + + capStyle: ShapePath.RoundCap + fillColor: "transparent" + strokeWidth: 2 + strokeColor: Colours.palette.m3primary + + PathAngleArc { + id: progressArc + + radiusX: progressIndicator.width / 2 - root.Tokens.padding.small / 2 + centerX: progressIndicator.width / 2 + radiusY: progressIndicator.height / 2 - root.Tokens.padding.small / 2 + centerY: progressIndicator.height / 2 + + startAngle: -90 + sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360 + + Behavior on sweepAngle { + Anim { + easing: Tokens.anim.emphasizedDecel + } } } } @@ -185,13 +230,13 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Appearance.spacing.smaller + anchors.leftMargin: Tokens.spacing.smaller animate: true text: appNameMetrics.elidedText maximumLineCount: 1 color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small opacity: root.expanded ? 1 : 0 @@ -207,7 +252,7 @@ StyledRect { font.family: appName.font.family font.pointSize: appName.font.pointSize elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } StyledText { @@ -215,7 +260,7 @@ StyledRect { anchors.top: parent.top anchors.left: image.right - anchors.leftMargin: Appearance.spacing.smaller + anchors.leftMargin: Tokens.spacing.smaller animate: true text: summaryMetrics.elidedText @@ -241,10 +286,8 @@ StyledRect { target: summary property: "maximumLineCount" } - AnchorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + AnchorAnim { + type: AnchorAnim.Standard } } @@ -260,7 +303,7 @@ StyledRect { font.family: summary.font.family font.pointSize: summary.font.pointSize elide: Text.ElideRight - elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 + elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 } StyledText { @@ -268,11 +311,11 @@ StyledRect { anchors.top: parent.top anchors.left: summary.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small text: "•" color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small states: State { name: "expanded" @@ -285,10 +328,8 @@ StyledRect { } transitions: Transition { - AnchorAnimation { - duration: Appearance.anim.durations.normal - easing.type: Easing.BezierSpline - easing.bezierCurve: Appearance.anim.curves.standard + AnchorAnim { + type: AnchorAnim.Standard } } } @@ -298,13 +339,13 @@ StyledRect { anchors.top: parent.top anchors.left: timeSep.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small animate: true horizontalAlignment: Text.AlignLeft text: root.modelData.timeStr color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } Item { @@ -317,12 +358,9 @@ StyledRect { implicitHeight: expandIcon.height StateLayer { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - - function onClicked() { - root.expanded = !root.expanded; - } + onClicked: root.expanded = !root.expanded } MaterialIcon { @@ -332,7 +370,7 @@ StyledRect { animate: true text: root.expanded ? "expand_less" : "expand_more" - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } @@ -342,13 +380,13 @@ StyledRect { anchors.left: summary.left anchors.right: expandBtn.left anchors.top: summary.bottom - anchors.rightMargin: Appearance.spacing.small + anchors.rightMargin: Tokens.spacing.small animate: true - textFormat: Text.MarkdownText + textFormat: root.bodyTextFormat text: bodyPreviewMetrics.elidedText color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small opacity: root.expanded ? 0 : 1 @@ -373,13 +411,13 @@ StyledRect { anchors.left: summary.left anchors.right: expandBtn.left anchors.top: summary.bottom - anchors.rightMargin: Appearance.spacing.small + anchors.rightMargin: Tokens.spacing.small animate: true - textFormat: Text.MarkdownText + textFormat: root.bodyTextFormat text: root.modelData.body color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere height: text ? implicitHeight : 0 @@ -403,9 +441,9 @@ StyledRect { anchors.horizontalCenter: parent.horizontalCenter anchors.top: body.bottom - anchors.topMargin: Appearance.spacing.small + anchors.topMargin: Tokens.spacing.small - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller opacity: root.expanded ? 1 : 0 @@ -416,6 +454,7 @@ StyledRect { Action { modelData: QtObject { readonly property string text: qsTr("Close") + function invoke(): void { root.modelData.close(); } @@ -438,21 +477,18 @@ StyledRect { required property var modelData - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 - Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 - implicitWidth: actionText.width + Appearance.padding.normal * 2 - implicitHeight: actionText.height + Appearance.padding.small * 2 + Layout.preferredWidth: actionText.width + Tokens.padding.normal * 2 + Layout.preferredHeight: actionText.height + Tokens.padding.small * 2 + implicitWidth: actionText.width + Tokens.padding.normal * 2 + implicitHeight: actionText.height + Tokens.padding.small * 2 StateLayer { - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface - - function onClicked(): void { - action.modelData.invoke(); - } + onClicked: action.modelData.invoke() } StyledText { @@ -461,7 +497,7 @@ StyledRect { anchors.centerIn: parent text: actionTextMetrics.elidedText color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } TextMetrics { @@ -473,7 +509,7 @@ StyledRect { elide: Text.ElideRight elideWidth: { const numActions = root.modelData.actions.length + 1; - return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2; + return (inner.width - actions.spacing * (numActions - 1)) / numActions - root.Tokens.padding.normal * 2; } } } diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml index 61acc56e1..97417e9cf 100644 --- a/modules/notifications/Wrapper.qml +++ b/modules/notifications/Wrapper.qml @@ -1,39 +1,22 @@ -import qs.components -import qs.config import QtQuick +import qs.components Item { id: root - required property var visibilities - required property Item panels + required property DrawerVisibilities visibilities + required property Item sidebarPanel + property alias osdPanel: content.osdPanel + property alias sessionPanel: content.sessionPanel visible: height > 0 - implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) + anchors.topMargin: -5 + implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) implicitHeight: content.implicitHeight - states: State { - name: "hidden" - when: root.visibilities.sidebar && Config.sidebar.enabled - - PropertyChanges { - root.implicitHeight: 0 - } - } - - transitions: Transition { - Anim { - target: root - property: "implicitHeight" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - Content { id: content visibilities: root.visibilities - panels: root.panels } } diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml index 770fb6968..3ad3ef886 100644 --- a/modules/osd/Content.qml +++ b/modules/osd/Content.qml @@ -1,18 +1,18 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config import qs.utils -import QtQuick -import QtQuick.Layouts Item { id: root required property Brightness.Monitor monitor - required property var visibilities + required property DrawerVisibilities visibilities required property real volume required property bool muted @@ -20,20 +20,17 @@ Item { required property bool sourceMuted required property real brightness - implicitWidth: layout.implicitWidth + Appearance.padding.large * 2 - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + implicitWidth: layout.implicitWidth + Tokens.padding.large * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 ColumnLayout { id: layout anchors.centerIn: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal // Speaker volume CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementVolume(); @@ -41,12 +38,15 @@ Item { Audio.decrementVolume(); } + implicitWidth: Tokens.sizes.osd.sliderWidth + implicitHeight: Tokens.sizes.osd.sliderHeight + FilledSlider { anchors.fill: parent icon: Icons.getVolumeIcon(value, root.muted) value: root.volume - to: Config.services.maxVolume + to: GlobalConfig.services.maxVolume onMoved: Audio.setVolume(value) } } @@ -56,9 +56,6 @@ Item { shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) sourceComponent: CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - function onWheel(event: WheelEvent) { if (event.angleDelta.y > 0) Audio.incrementSourceVolume(); @@ -66,12 +63,15 @@ Item { Audio.decrementSourceVolume(); } + implicitWidth: Tokens.sizes.osd.sliderWidth + implicitHeight: Tokens.sizes.osd.sliderHeight + FilledSlider { anchors.fill: parent icon: Icons.getMicVolumeIcon(value, root.sourceMuted) value: root.sourceVolume - to: Config.services.maxVolume + to: GlobalConfig.services.maxVolume onMoved: Audio.setSourceVolume(value) } } @@ -82,19 +82,19 @@ Item { shouldBeActive: Config.osd.enableBrightness sourceComponent: CustomMouseArea { - implicitWidth: Config.osd.sizes.sliderWidth - implicitHeight: Config.osd.sizes.sliderHeight - function onWheel(event: WheelEvent) { const monitor = root.monitor; if (!monitor) return; if (event.angleDelta.y > 0) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); else if (event.angleDelta.y < 0) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); } + implicitWidth: Tokens.sizes.osd.sliderWidth + implicitHeight: Tokens.sizes.osd.sliderHeight + FilledSlider { anchors.fill: parent @@ -109,14 +109,15 @@ Item { component WrappedLoader: Loader { required property bool shouldBeActive - Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 + asynchronous: true + Layout.preferredHeight: shouldBeActive ? Tokens.sizes.osd.sliderHeight : 0 opacity: shouldBeActive ? 1 : 0 active: opacity > 0 visible: active Behavior on Layout.preferredHeight { Anim { - easing.bezierCurve: Appearance.anim.curves.emphasized + type: Anim.Emphasized } } diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml index 2519609dd..3ea0e1a36 100644 --- a/modules/osd/Wrapper.qml +++ b/modules/osd/Wrapper.qml @@ -1,19 +1,23 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell -import QtQuick Item { id: root required property ShellScreen screen - required property var visibilities + required property DrawerVisibilities visibilities + required property bool sidebarOrSessionVisible + property bool hovered readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) + property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0 property real volume property bool muted @@ -34,45 +38,19 @@ Item { brightness = root.monitor?.brightness ?? 0; } - visible: width > 0 - implicitWidth: 0 + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale + implicitWidth: content.implicitWidth implicitHeight: content.implicitHeight + opacity: 1 - offsetScale - states: State { - name: "visible" - when: root.shouldBeActive - - PropertyChanges { - root.implicitWidth: content.implicitWidth + Behavior on offsetScale { + Anim { + type: Anim.DefaultSpatial } } - transitions: [ - Transition { - from: "" - to: "visible" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: Appearance.anim.curves.emphasized - } - } - ] - Connections { - target: Audio - function onMutedChanged(): void { root.show(); root.muted = Audio.muted; @@ -92,21 +70,23 @@ Item { root.show(); root.sourceVolume = Audio.sourceVolume; } + + target: Audio } Connections { - target: root.monitor - function onBrightnessChanged(): void { root.show(); root.brightness = root.monitor?.brightness ?? 0; } + + target: root.monitor } Timer { id: timer - interval: Config.osd.hideDelay + interval: root.Config.osd.hideDelay onTriggered: { if (!root.hovered) root.visibilities.osd = false; @@ -119,7 +99,8 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) + asynchronous: true + active: root.shouldBeActive || root.visible sourceComponent: Content { monitor: root.monitor diff --git a/modules/session/Content.qml b/modules/session/Content.qml index 6c56d4429..e1ba28098 100644 --- a/modules/session/Content.qml +++ b/modules/session/Content.qml @@ -1,24 +1,24 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia.Config import qs.components import qs.services -import qs.config import qs.utils -import Quickshell -import QtQuick Column { id: root - required property PersistentProperties visibilities + required property DrawerVisibilities visibilities - padding: Appearance.padding.large - spacing: Appearance.spacing.large + padding: Tokens.padding.large + spacing: Tokens.spacing.large SessionButton { id: logout - icon: "logout" + icon: Config.session.icons.logout command: Config.session.commands.logout KeyNavigation.down: shutdown @@ -26,19 +26,19 @@ Column { Component.onCompleted: forceActiveFocus() Connections { - target: root.visibilities - function onLauncherChanged(): void { if (!root.visibilities.launcher) logout.forceActiveFocus(); } + + target: root.visibilities } } SessionButton { id: shutdown - icon: "power_settings_new" + icon: Config.session.icons.shutdown command: Config.session.commands.shutdown KeyNavigation.up: logout @@ -46,21 +46,21 @@ Column { } AnimatedImage { - width: Config.session.sizes.button - height: Config.session.sizes.button - sourceSize.width: width - sourceSize.height: height + width: Tokens.sizes.session.button + height: Tokens.sizes.session.button + sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) playing: visible asynchronous: true - speed: 0.7 + speed: Config.general.sessionGifSpeed source: Paths.absolutePath(Config.paths.sessionGif) + fillMode: AnimatedImage.PreserveAspectFit } SessionButton { id: hibernate - icon: "downloading" + icon: Config.session.icons.hibernate command: Config.session.commands.hibernate KeyNavigation.up: shutdown @@ -70,7 +70,7 @@ Column { SessionButton { id: reboot - icon: "cached" + icon: Config.session.icons.reboot command: Config.session.commands.reboot KeyNavigation.up: hibernate @@ -82,10 +82,10 @@ Column { required property string icon required property list command - implicitWidth: Config.session.sizes.button - implicitHeight: Config.session.sizes.button + implicitWidth: Tokens.sizes.session.button + implicitHeight: Tokens.sizes.session.button - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer Keys.onEnterPressed: Quickshell.execDetached(button.command) @@ -96,10 +96,10 @@ Column { return; if (event.modifiers & Qt.ControlModifier) { - if (event.key === Qt.Key_J && KeyNavigation.down) { + if ((event.key === Qt.Key_J || event.key === Qt.Key_N) && KeyNavigation.down) { KeyNavigation.down.focus = true; event.accepted = true; - } else if (event.key === Qt.Key_K && KeyNavigation.up) { + } else if ((event.key === Qt.Key_K || event.key === Qt.Key_P) && KeyNavigation.up) { KeyNavigation.up.focus = true; event.accepted = true; } @@ -117,10 +117,7 @@ Column { StateLayer { radius: parent.radius color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - - function onClicked(): void { - Quickshell.execDetached(button.command); - } + onClicked: Quickshell.execDetached(button.command) } MaterialIcon { @@ -128,7 +125,7 @@ Column { text: button.icon color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 } } diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml index 14b03a809..41d9ba1ae 100644 --- a/modules/session/Wrapper.qml +++ b/modules/session/Wrapper.qml @@ -1,60 +1,39 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config -import Quickshell import QtQuick +import Caelestia.Config +import qs.components Item { id: root - required property PersistentProperties visibilities - required property var panels + required property DrawerVisibilities visibilities + required property bool sidebarVisible readonly property real nonAnimWidth: content.implicitWidth - visible: width > 0 - implicitWidth: 0 - implicitHeight: content.implicitHeight + readonly property bool shouldBeActive: visibilities.session && Config.session.enabled + property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarOffset: sidebarVisible ? 14 : 0 - states: State { - name: "visible" - when: root.visibilities.session && Config.session.enabled + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale + implicitWidth: content.implicitWidth + implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open + opacity: 1 - offsetScale - PropertyChanges { - root.implicitWidth: root.nonAnimWidth + Behavior on offsetScale { + Anim { + type: Anim.DefaultSpatial } } - transitions: [ - Transition { - from: "" - to: "visible" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - } - } - ] - Loader { id: content anchors.verticalCenter: parent.verticalCenter anchors.left: parent.left - Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { visibilities: root.visibilities diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml index 1b7feed66..f6b8eb051 100644 --- a/modules/sidebar/Content.qml +++ b/modules/sidebar/Content.qml @@ -1,26 +1,26 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.services Item { id: root required property Props props - required property var visibilities + required property DrawerVisibilities visibilities ColumnLayout { id: layout anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledRect { Layout.fillWidth: true Layout.fillHeight: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainerLow NotifDock { @@ -30,7 +30,7 @@ Item { } StyledRect { - Layout.topMargin: Appearance.padding.large - layout.spacing + Layout.topMargin: Tokens.padding.large - layout.spacing Layout.fillWidth: true implicitHeight: 1 diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml index 5a317640f..2a390926d 100644 --- a/modules/sidebar/Notif.qml +++ b/modules/sidebar/Notif.qml @@ -1,42 +1,43 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Quickshell import QtQuick import QtQuick.Layouts +import Quickshell +import Caelestia.Config +import qs.components +import qs.services StyledRect { id: root - required property Notifs.Notif modelData + required property NotifData modelData required property Props props required property bool expanded - required property var visibilities + required property DrawerVisibilities visibilities - readonly property StyledText body: expandedContent.item?.body ?? null - readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height + readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null + readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.normal * 2 : summaryHeightMetrics.height implicitHeight: nonAnimHeight - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: { - const c = root.modelData.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); + const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); return expanded ? c : Qt.alpha(c, 0); } + state: expanded ? "expanded" : "" + states: State { name: "expanded" - when: root.expanded PropertyChanges { - summary.anchors.margins: Appearance.padding.normal - dummySummary.anchors.margins: Appearance.padding.normal - compactBody.anchors.margins: Appearance.padding.normal - timeStr.anchors.margins: Appearance.padding.normal - expandedContent.anchors.margins: Appearance.padding.normal - summary.width: root.width - Appearance.padding.normal * 2 - timeStr.implicitWidth - Appearance.spacing.small + summary.anchors.margins: root.Tokens.padding.normal + dummySummary.anchors.margins: root.Tokens.padding.normal + compactBody.anchors.margins: root.Tokens.padding.normal + timeStr.anchors.margins: root.Tokens.padding.normal + expandedContent.anchors.margins: root.Tokens.padding.normal + summary.width: root.width - root.Tokens.padding.normal * 2 - timeStr.implicitWidth - root.Tokens.spacing.small summary.maximumLineCount: Number.MAX_SAFE_INTEGER } } @@ -61,8 +62,8 @@ StyledRect { anchors.left: parent.left width: parent.width - text: root.modelData.summary - color: root.modelData.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface + text: root.modelData?.summary ?? "" + color: root.modelData?.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface elide: Text.ElideRight wrapMode: Text.WordWrap maximumLineCount: 1 @@ -75,7 +76,7 @@ StyledRect { anchors.left: parent.left visible: false - text: root.modelData.summary + text: root.modelData?.summary ?? "" } WrappedLoader { @@ -85,11 +86,11 @@ StyledRect { anchors.top: parent.top anchors.left: dummySummary.right anchors.right: parent.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small sourceComponent: StyledText { - text: root.modelData.body.replace(/\n/g, " ") - color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + text: String(root.modelData?.body ?? "").replace(/\n/g, " ") + color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline elide: Text.ElideRight } } @@ -103,9 +104,9 @@ StyledRect { sourceComponent: StyledText { animate: true - text: root.modelData.timeStr + text: root.modelData?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } } @@ -116,49 +117,88 @@ StyledRect { anchors.top: summary.bottom anchors.left: parent.left anchors.right: parent.right - anchors.topMargin: Appearance.spacing.small / 2 + anchors.topMargin: Tokens.spacing.small / 2 - sourceComponent: ColumnLayout { - readonly property alias body: body + sourceComponent: ExpandedBody {} + } - spacing: Appearance.spacing.smaller + Behavior on implicitHeight { + Anim { + type: Anim.DefaultSpatial + } + } - StyledText { - id: body + component ExpandedBody: ColumnLayout { + readonly property alias body: bodyText - Layout.fillWidth: true - textFormat: Text.MarkdownText - text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") - color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline - wrapMode: Text.WordWrap + spacing: Tokens.spacing.smaller - onLinkActivated: link => { - Quickshell.execDetached(["app2unit", "-O", "--", link]); - root.visibilities.sidebar = false; - } - } + StyledText { + id: bodyText + + Layout.fillWidth: true + textFormat: Text.MarkdownText + text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") + color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline + wrapMode: Text.WordWrap - NotifActionList { - notif: root.modelData + onLinkActivated: link => { + Quickshell.execDetached(["app2unit", "-O", "--", link]); + root.visibilities.sidebar = false; } } - } - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + NotifActionList { + notif: root.modelData } } component WrappedLoader: Loader { + id: comp + required property bool shouldBeActive - opacity: shouldBeActive ? 1 : 0 - active: opacity > 0 + active: false + opacity: 0 - Behavior on opacity { - Anim {} + // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set + states: State { + name: "active" + when: comp.shouldBeActive + + PropertyChanges { + comp.opacity: 1 + comp.active: true + } } + + transitions: [ + Transition { + from: "" + to: "active" + + SequentialAnimation { + PropertyAction { + property: "active" + } + Anim { + property: "opacity" + } + } + }, + Transition { + from: "active" + to: "" + + SequentialAnimation { + Anim { + property: "opacity" + } + PropertyAction { + property: "active" + } + } + } + ] } } diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml index d1f1e1f51..084fe6781 100644 --- a/modules/sidebar/NotifActionList.qml +++ b/modules/sidebar/NotifActionList.qml @@ -1,19 +1,19 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config import qs.components import qs.components.containers import qs.components.effects import qs.services -import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts Item { id: root - required property Notifs.Notif notif + required property NotifData notif Layout.fillWidth: true implicitHeight: flickable.contentHeight @@ -94,14 +94,14 @@ Item { id: actionList anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Repeater { model: [ { isClose: true }, - ...root.notif.actions, + ...(root.notif?.actions ?? []), { isCopy: true } @@ -114,11 +114,11 @@ Item { Layout.fillWidth: true Layout.fillHeight: true - implicitWidth: actionInner.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: actionInner.implicitHeight + Appearance.padding.small * 2 + implicitWidth: actionInner.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: actionInner.implicitHeight + Tokens.padding.small * 2 - Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Appearance.padding.large : 0) - radius: actionStateLayer.pressed ? Appearance.rounding.small / 2 : Appearance.rounding.small + Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0) + radius: actionStateLayer.pressed ? Tokens.rounding.small / 2 : Tokens.rounding.small color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4) Timer { @@ -131,7 +131,7 @@ Item { StateLayer { id: actionStateLayer - function onClicked(): void { + onClicked: { if (action.modelData.isClose) { root.notif.close(); } else if (action.modelData.isCopy) { @@ -150,7 +150,7 @@ Item { id: actionInner anchors.centerIn: parent - sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp + sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp } Component { @@ -167,6 +167,7 @@ Item { id: iconComp IconImage { + asynchronous: true source: Quickshell.iconPath(action.modelData.identifier) } } @@ -182,15 +183,13 @@ Item { Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on radius { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml index d039d15d6..4509ce26b 100644 --- a/modules/sidebar/NotifDock.qml +++ b/modules/sidebar/NotifDock.qml @@ -1,25 +1,26 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.components.effects import qs.services -import qs.config -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts +import qs.utils Item { id: root required property Props props - required property var visibilities + required property DrawerVisibilities visibilities readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) anchors.fill: parent - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal Component.onCompleted: Notifs.list.forEach(n => n.popup = false) @@ -29,7 +30,7 @@ Item { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.small + anchors.margins: Tokens.padding.small implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) @@ -43,8 +44,8 @@ Item { text: root.notifCount color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono font.weight: 500 Behavior on anchors.leftMargin { @@ -62,12 +63,12 @@ Item { anchors.verticalCenter: parent.verticalCenter anchors.left: count.right anchors.right: parent.right - anchors.leftMargin: Appearance.spacing.small + anchors.leftMargin: Tokens.spacing.small text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.normal - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.normal + font.family: Tokens.font.family.mono font.weight: 500 elide: Text.ElideRight } @@ -80,24 +81,25 @@ Item { anchors.right: parent.right anchors.top: title.bottom anchors.bottom: parent.bottom - anchors.topMargin: Appearance.spacing.smaller + anchors.topMargin: Tokens.spacing.smaller - radius: Appearance.rounding.small + radius: Tokens.rounding.small color: "transparent" Loader { + asynchronous: true anchors.centerIn: parent active: opacity > 0 opacity: root.notifCount > 0 ? 0 : 1 sourceComponent: ColumnLayout { - spacing: Appearance.spacing.large + spacing: Tokens.spacing.large Image { asynchronous: true - source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) + source: Paths.absolutePath(Config.paths.noNotifsPic) fillMode: Image.PreserveAspectFit - sourceSize.width: clipRect.width * 0.8 + sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) layer.enabled: true layer.effect: Colouriser { @@ -110,15 +112,15 @@ Item { Layout.alignment: Qt.AlignHCenter text: qsTr("No Notifications") color: Colours.palette.m3outlineVariant - font.pointSize: Appearance.font.size.large - font.family: Appearance.font.family.mono + font.pointSize: Tokens.font.size.large + font.family: Tokens.font.family.mono font.weight: 500 } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.extraLarge + type: Anim.StandardExtraLarge } } } @@ -150,25 +152,33 @@ Item { id: clearTimer repeat: true - interval: 50 + triggeredOnStart: true + interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(Notifs.notClosed.length))) onTriggered: { - let next = null; - for (let i = 0; i < notifList.repeater.count; i++) { - next = notifList.repeater.itemAt(i); - if (!next?.closed) - break; - } - if (next) - next.closeAll(); - else + const first = Notifs.notClosed[0]; + if (!first) { stop(); + return; + } + + const appName = first.appName; + let cleared = 0; + for (const n of Notifs.notClosed.filter(n => n.appName === appName)) { + n.close(); + cleared++; + if (cleared > 30) { + interval = 5; + return; + } + } } } Loader { + asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal scale: root.notifCount > 0 ? 1 : 0.5 opacity: root.notifCount > 0 ? 1 : 0 @@ -178,9 +188,9 @@ Item { id: clearBtn icon: "clear_all" - radius: Appearance.rounding.normal - padding: Appearance.padding.normal - font.pointSize: Math.round(Appearance.font.size.large * 1.2) + radius: Tokens.rounding.normal + padding: Tokens.padding.normal + font.pointSize: Math.round(Tokens.font.size.large * 1.2) onClicked: clearTimer.start() Elevation { @@ -193,14 +203,13 @@ Item { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial + duration: Tokens.anim.durations.expressiveFastSpatial } } } diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml index b927e91a7..2677698b8 100644 --- a/modules/sidebar/NotifDockList.qml +++ b/modules/sidebar/NotifDockList.qml @@ -1,44 +1,52 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia.Components +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell -import QtQuick -Item { +LazyListView { id: root required property Props props required property Flickable container - required property var visibilities + required property DrawerVisibilities visibilities + + anchors.left: parent?.left + anchors.right: parent?.right + implicitHeight: contentHeight + + spacing: Tokens.spacing.small + readyDelay: 1 + cacheBuffer: 400 + asynchronous: true + + onViewportAdjustNeeded: d => { + if (contentYAnim.running) + contentYAnim.complete(); + contentYAnim.to = Math.max(0, container.contentY + d); + contentYAnim.start(); + } - readonly property alias repeater: repeater - readonly property int spacing: Appearance.spacing.small - property bool flag + useCustomViewport: true + viewport: Qt.rect(0, container.contentY, width, container.height) - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: { - const item = repeater.itemAt(repeater.count - 1); - return item ? item.y + item.implicitHeight : 0; - } + removeDuration: Tokens.anim.durations.normal - Repeater { - id: repeater - - model: ScriptModel { - values: { - const map = new Map(); - for (const n of Notifs.notClosed) - map.set(n.appName, null); - for (const n of Notifs.list) - map.set(n.appName, null); - return [...map.keys()]; - } - onValuesChanged: root.flagChanged() + model: ScriptModel { + values: { + const map = new Map(); + for (const n of Notifs.notClosed) + map.set(n.appName, null); + for (const n of Notifs.list) + map.set(n.appName, null); + return [...map.keys()]; } + } + delegate: Component { MouseArea { id: notif @@ -46,36 +54,20 @@ Item { required property string modelData readonly property bool closed: notifInner.notifCount === 0 - readonly property alias nonAnimHeight: notifInner.nonAnimHeight property int startY function closeAll(): void { - for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) - n.close(); - } - - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (!item.closed) - y += item.nonAnimHeight + root.spacing; - } - return y; + clearTimer.start(); } - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } - } - - implicitWidth: root.width + LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight + LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight + LazyListView.visibleHeight: notifInner.implicitHeight implicitHeight: notifInner.implicitHeight + opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1 + scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1 + hoverEnabled: true cursorShape: pressed ? Qt.ClosedHandCursor : undefined acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton @@ -106,37 +98,21 @@ Item { closeAll(); } - ParallelAnimation { - running: true - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: notif - property: "scale" - from: 0 - to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - ParallelAnimation { - running: notif.closed - - Anim { - target: notif - property: "opacity" - to: 0 - } - Anim { - target: notif - property: "scale" - to: 0.6 + Timer { + id: clearTimer + + interval: 15 + repeat: true + triggeredOnStart: true + onTriggered: { + const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData); + if (notifs.length === 0) { + stop(); + return; + } + + for (const n of notifs.slice(0, 30)) + n.close(); } } @@ -149,19 +125,37 @@ Item { visibilities: root.visibilities } - Behavior on x { + Behavior on y { + enabled: notif.LazyListView.ready + Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } - Behavior on y { + Behavior on opacity { + Anim {} + } + + Behavior on scale { + Anim { + type: Anim.DefaultSpatial + } + } + + Behavior on x { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } } + + Anim { + id: contentYAnim + + target: root.container + property: "contentY" + type: Anim.DefaultSpatial + } } diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml index 16aac33ba..2920af743 100644 --- a/modules/sidebar/NotifGroup.qml +++ b/modules/sidebar/NotifGroup.qml @@ -1,14 +1,14 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Services.Notifications +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import qs.config import qs.utils -import Quickshell -import Quickshell.Services.Notifications -import QtQuick -import QtQuick.Layouts StyledRect { id: root @@ -16,18 +16,25 @@ StyledRect { required property string modelData required property Props props required property Flickable container - required property var visibilities + required property DrawerVisibilities visibilities readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) - readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) - readonly property string image: notifs.find(n => !n.closed && n.image.length > 0)?.image ?? "" - readonly property string appIcon: notifs.find(n => !n.closed && n.appIcon.length > 0)?.appIcon ?? "" - readonly property int urgency: notifs.some(n => !n.closed && n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low + readonly property list activeNotifs: notifs.filter(n => !n.closed) + readonly property int notifCount: activeNotifs.length + readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? "" + readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" + readonly property int urgency: { + if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical)) + return NotificationUrgency.Critical; + if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal)) + return NotificationUrgency.Normal; + return NotificationUrgency.Low; + } readonly property int nonAnimHeight: { - const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0); - const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; - return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2); + const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); + const columnHeight = headerHeight + notifList.layoutHeight; + return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); } readonly property bool expanded: props.expandedNotifs.includes(modelData) @@ -47,26 +54,32 @@ StyledRect { anchors.left: parent?.left anchors.right: parent?.right - implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 + implicitHeight: nonAnimHeight clip: true - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.layer(Colours.palette.m3surfaceContainer, 2) + Behavior on implicitHeight { + Anim { + type: Anim.DefaultSpatial + } + } + RowLayout { id: content anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - anchors.margins: Appearance.padding.normal + anchors.margins: Tokens.padding.normal - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Item { Layout.alignment: Qt.AlignLeft | Qt.AlignTop - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image Component { id: imageComp @@ -74,10 +87,14 @@ StyledRect { Image { source: Qt.resolvedUrl(root.image) fillMode: Image.PreserveAspectCrop + sourceSize: { + const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); + return Qt.size(size, size); + } cache: false asynchronous: true - width: Config.notifs.sizes.image - height: Config.notifs.sizes.image + width: TokenConfig.sizes.notifs.image + height: TokenConfig.sizes.notifs.image } } @@ -85,7 +102,7 @@ StyledRect { id: appIconComp ColouredIcon { - implicitSize: Math.round(Config.notifs.sizes.image * 0.6) + implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") @@ -96,38 +113,40 @@ StyledRect { id: materialIconComp MaterialIcon { - text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) + text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency) color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } StyledClippingRect { anchors.fill: parent color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full Loader { + asynchronous: true anchors.centerIn: parent sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp } } Loader { + asynchronous: true anchors.right: parent.right anchors.bottom: parent.bottom active: root.appIcon && root.image sourceComponent: StyledRect { - implicitWidth: Config.notifs.sizes.badge - implicitHeight: Config.notifs.sizes.badge + implicitWidth: Tokens.sizes.notifs.badge + implicitHeight: Tokens.sizes.notifs.badge color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer - radius: Appearance.rounding.full + radius: Tokens.rounding.full ColouredIcon { anchors.centerIn: parent - implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) + implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) source: Quickshell.iconPath(root.appIcon) colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer layer.enabled: root.appIcon.endsWith("symbolic") @@ -136,94 +155,87 @@ StyledRect { } } - ColumnLayout { + Column { id: column - Layout.topMargin: -Appearance.padding.small - Layout.bottomMargin: -Appearance.padding.small / 2 Layout.fillWidth: true - spacing: 0 + spacing: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 + + Behavior on spacing { + Anim {} + } RowLayout { id: header - Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0 - Layout.fillWidth: true - spacing: Appearance.spacing.smaller + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.smaller StyledText { Layout.fillWidth: true text: root.modelData color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } StyledText { animate: true - text: root.notifs.find(n => !n.closed)?.timeStr ?? "" + text: root.activeNotifs[0]?.timeStr ?? "" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } StyledRect { - implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 - implicitHeight: groupCount.implicitHeight + Appearance.padding.small + implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 + implicitHeight: groupCount.implicitHeight + Tokens.padding.small color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) - radius: Appearance.rounding.full + radius: Tokens.rounding.full StateLayer { color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface - - function onClicked(): void { - root.toggleExpand(!root.expanded); - } + onClicked: root.toggleExpand(!root.expanded) } RowLayout { id: expandBtn anchors.centerIn: parent - spacing: Appearance.spacing.small / 2 + spacing: Tokens.spacing.small / 2 StyledText { id: groupCount - Layout.leftMargin: Appearance.padding.small / 2 + Layout.leftMargin: Tokens.padding.small / 2 animate: true text: root.notifCount color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small } MaterialIcon { - Layout.rightMargin: -Appearance.padding.small / 2 + Layout.rightMargin: -Tokens.padding.small / 2 text: "expand_more" color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface rotation: root.expanded ? 180 : 0 - Layout.topMargin: root.expanded ? -Math.floor(Appearance.padding.smaller / 2) : 0 + Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.smaller / 2) : 0 Behavior on rotation { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on Layout.topMargin { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } } } - - Behavior on Layout.bottomMargin { - Anim {} - } } NotifGroupList { diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml index e586b5f7a..72b8b3a8e 100644 --- a/modules/sidebar/NotifGroupList.qml +++ b/modules/sidebar/NotifGroupList.qml @@ -1,114 +1,82 @@ pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import Caelestia.Components +import Caelestia.Config import qs.components import qs.services -import qs.config -import Quickshell -import QtQuick -import QtQuick.Layouts -Item { +LazyListView { id: root required property Props props required property list notifs required property bool expanded required property Flickable container - required property var visibilities - - readonly property real nonAnimHeight: { - let h = -root.spacing; - for (let i = 0; i < repeater.count; i++) { - const item = repeater.itemAt(i); - if (!item.modelData.closed && !item.previewHidden) - h += item.nonAnimHeight + root.spacing; - } - return h; - } - - readonly property int spacing: Math.round(Appearance.spacing.small / 2) - property bool showAllNotifs - property bool flag + required property DrawerVisibilities visibilities signal requestToggleExpand(expand: bool) - onExpandedChanged: { - if (expanded) { - clearTimer.stop(); - showAllNotifs = true; - } else { - clearTimer.start(); - } - } + anchors.left: parent.left + anchors.right: parent.right + implicitHeight: contentHeight - Layout.fillWidth: true - implicitHeight: nonAnimHeight + spacing: Math.round(Tokens.spacing.small / 2) + asynchronous: true - Timer { - id: clearTimer + readyDelay: 1 + cacheBuffer: 400 + removeDuration: Tokens.anim.durations.normal - interval: Appearance.anim.durations.normal - onTriggered: root.showAllNotifs = false + useCustomViewport: true + viewport: { + tWatcher.transform; // mapToItem is not reactive so use this to trigger updates + return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height); } - Repeater { - id: repeater + model: ScriptModel { + values: { + if (root.expanded) + return root.notifs; + + let count = 0; + let i = 0; + const previewNum = root.Config.notifs.groupPreviewNum; + while (i < root.notifs.length && count < previewNum) { + if (!(root.notifs[i]?.closed ?? true)) + count++; + i++; + } - model: ScriptModel { - values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) - onValuesChanged: root.flagChanged() + return root.notifs.slice(0, i); } + } + delegate: Component { MouseArea { id: notif required property int index - required property Notifs.Notif modelData - - readonly property alias nonAnimHeight: notifInner.nonAnimHeight - readonly property bool previewHidden: { - if (root.expanded) - return false; + required property NotifData modelData - let extraHidden = 0; - for (let i = 0; i < index; i++) - if (root.notifs[i].closed) - extraHidden++; - - return index >= Config.notifs.groupPreviewNum + extraHidden; - } property int startY - y: { - root.flag; // Force update - let y = 0; - for (let i = 0; i < index; i++) { - const item = repeater.itemAt(i); - if (!item.modelData.closed && !item.previewHidden) - y += item.nonAnimHeight + root.spacing; - } - return y; - } + Component.onCompleted: modelData?.lock(this) + Component.onDestruction: modelData?.unlock(this) - containmentMask: QtObject { - function contains(p: point): bool { - if (!root.container.contains(notif.mapToItem(root.container, p))) - return false; - return notifInner.contains(p); - } - } - - opacity: previewHidden ? 0 : 1 - scale: previewHidden ? 0.7 : 1 - - implicitWidth: root.width + LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight + LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight implicitHeight: notifInner.implicitHeight + opacity: LazyListView.removing || LazyListView.adding ? 0 : 1 + scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1 + hoverEnabled: true cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton preventStealing: !root.expanded - enabled: !modelData.closed + enabled: !(modelData?.closed ?? true) drag.target: this drag.axis: Drag.XAxis @@ -118,7 +86,7 @@ Item { if (event.button === Qt.RightButton) root.requestToggleExpand(!root.expanded); else if (event.button === Qt.MiddleButton) - modelData.close(); + modelData?.close(); } onPositionChanged: event => { if (pressed && !root.expanded) { @@ -131,32 +99,12 @@ Item { if (Math.abs(x) < width * Config.notifs.clearThreshold) x = 0; else - modelData.close(); - } - - Component.onCompleted: modelData.lock(this) - Component.onDestruction: modelData.unlock(this) - - ParallelAnimation { - Component.onCompleted: running = !notif.previewHidden - - Anim { - target: notif - property: "opacity" - from: 0 - to: 1 - } - Anim { - target: notif - property: "scale" - from: 0.7 - to: 1 - } + modelData?.close(); } ParallelAnimation { - running: notif.modelData.closed - onFinished: notif.modelData.unlock(notif) + running: notif.modelData?.closed ?? false + onFinished: notif.modelData?.unlock(notif) Anim { target: notif @@ -180,6 +128,14 @@ Item { visibilities: root.visibilities } + Behavior on y { + enabled: notif.LazyListView.ready + + Anim { + type: Anim.DefaultSpatial + } + } + Behavior on opacity { Anim {} } @@ -190,24 +146,16 @@ Item { Behavior on x { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - } - - Behavior on y { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } } - Behavior on implicitHeight { - Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } + TransformWatcher { + id: tWatcher + + a: root.container.contentItem + b: root } } diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml index 9303c6b94..108c51a26 100644 --- a/modules/sidebar/Wrapper.qml +++ b/modules/sidebar/Wrapper.qml @@ -1,66 +1,42 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config import QtQuick +import Caelestia.Config +import qs.components Item { id: root - required property var visibilities - required property var panels + required property DrawerVisibilities visibilities readonly property Props props: Props {} - visible: width > 0 - implicitWidth: 0 + readonly property bool shouldBeActive: visibilities.sidebar && Config.sidebar.enabled + property real offsetScale: shouldBeActive ? 0 : 1 - states: State { - name: "visible" - when: root.visibilities.sidebar && Config.sidebar.enabled + visible: offsetScale < 1 + anchors.rightMargin: (-implicitWidth - 5) * offsetScale + implicitWidth: Tokens.sizes.sidebar.width + opacity: 1 - offsetScale - PropertyChanges { - root.implicitWidth: Config.sidebar.sizes.width + Behavior on offsetScale { + Anim { + type: Anim.DefaultSpatial } } - transitions: [ - Transition { - from: "" - to: "visible" - - Anim { - target: root - property: "implicitWidth" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial - } - }, - Transition { - from: "visible" - to: "" - - Anim { - target: root - property: "implicitWidth" - easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized - } - } - ] - Loader { id: content anchors.top: parent.top anchors.bottom: parent.bottom anchors.left: parent.left - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large anchors.bottomMargin: 0 - active: true - Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) + active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2 + implicitWidth: Tokens.sizes.sidebar.width - Tokens.padding.large * 2 props: root.props visibilities: root.visibilities } diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml index fbce89616..5b58b41e0 100644 --- a/modules/utilities/Background.qml +++ b/modules/utilities/Background.qml @@ -1,15 +1,14 @@ -import qs.components -import qs.services -import qs.config import QtQuick import QtQuick.Shapes +import qs.components +import qs.services ShapePath { id: root required property Wrapper wrapper required property var sidebar - readonly property real rounding: Config.border.rounding + required property real rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml index 902656de5..e03c546fd 100644 --- a/modules/utilities/Content.qml +++ b/modules/utilities/Content.qml @@ -1,14 +1,17 @@ import "cards" -import qs.config import QtQuick import QtQuick.Layouts +import Caelestia.Config +import qs.components +import qs.modules.bar.popouts as BarPopouts Item { id: root required property var props - required property var visibilities - required property Item popouts + required property DrawerVisibilities visibilities + required property BarPopouts.Wrapper popouts + required property matrix4x4 deformMatrix implicitWidth: layout.implicitWidth implicitHeight: layout.implicitHeight @@ -17,7 +20,7 @@ Item { id: layout anchors.fill: parent - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal IdleInhibit {} @@ -35,5 +38,6 @@ Item { RecordingDeleteModal { props: root.props + deformMatrix: root.deformMatrix } } diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml index 127afe93b..13125f2c3 100644 --- a/modules/utilities/RecordingDeleteModal.qml +++ b/modules/utilities/RecordingDeleteModal.qml @@ -1,20 +1,22 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import QtQuick.Shapes +import Caelestia +import Caelestia.Config import qs.components import qs.components.controls import qs.components.effects import qs.services -import qs.config -import Caelestia -import QtQuick -import QtQuick.Layouts -import QtQuick.Shapes Loader { id: root required property var props + required property matrix4x4 deformMatrix + asynchronous: true anchors.fill: parent opacity: root.props.recordingConfirmDelete ? 1 : 0 @@ -32,14 +34,16 @@ Loader { Item { anchors.fill: parent - anchors.margins: -Appearance.padding.large - anchors.rightMargin: -Appearance.padding.large - Config.border.thickness - anchors.bottomMargin: -Appearance.padding.large - Config.border.thickness + anchors.margins: -Tokens.padding.large + anchors.rightMargin: -Tokens.padding.large - Config.border.thickness + anchors.bottomMargin: -Tokens.padding.large - Config.border.thickness opacity: 0.5 StyledRect { anchors.fill: parent - topLeftRadius: Config.border.rounding + anchors.rightMargin: -parent.width * (1 - root.deformMatrix.m11) / 2 // Additional bit to account for deform + anchors.bottomMargin: -parent.height * 0.1 // Additional bit to account for overshoot + topLeftRadius: Tokens.rounding.large color: Colours.palette.m3scrim } @@ -50,13 +54,14 @@ Loader { preferredRendererType: Shape.CurveRenderer asynchronous: true + // Bottom left ShapePath { - startX: -Config.border.rounding * 2 - startY: shape.height - Config.border.thickness + startX: -root.Config.border.smoothing * 2 + startY: shape.height - root.Config.border.thickness strokeWidth: 0 fillGradient: LinearGradient { orientation: LinearGradient.Horizontal - x1: -Config.border.rounding * 2 + x1: -root.Config.border.smoothing * 2 GradientStop { position: 0 @@ -69,31 +74,34 @@ Loader { } PathLine { - relativeX: Config.border.rounding + relativeX: root.Config.border.smoothing relativeY: 0 } - PathArc { - relativeY: -Config.border.rounding - radiusX: Config.border.rounding - radiusY: Config.border.rounding - direction: PathArc.Counterclockwise + PathCubic { + relativeX: root.Config.border.smoothing + relativeY: -root.Config.border.smoothing + relativeControl1X: root.Config.border.smoothing * 0.93 + relativeControl1Y: -root.Config.border.smoothing * 0.07 + relativeControl2X: root.Config.border.smoothing * 0.93 + relativeControl2Y: -root.Config.border.smoothing * 0.07 } PathLine { relativeX: 0 - relativeY: Config.border.rounding + Config.border.thickness + relativeY: root.Config.border.smoothing + root.Config.border.thickness } PathLine { - relativeX: -Config.border.rounding * 2 + relativeX: -root.Config.border.smoothing * 2 relativeY: 0 } } + // Top right curve ShapePath { - startX: shape.width - Config.border.rounding - Config.border.thickness + startX: shape.width - root.Config.border.smoothing - root.Config.border.thickness + (1 - root.deformMatrix.m11) * shape.width / 2 strokeWidth: 0 fillGradient: LinearGradient { orientation: LinearGradient.Vertical - y1: -Config.border.rounding * 2 + y1: -root.Config.border.smoothing * 2 GradientStop { position: 0 @@ -105,19 +113,20 @@ Loader { } } - PathArc { - relativeX: Config.border.rounding - relativeY: -Config.border.rounding - radiusX: Config.border.rounding - radiusY: Config.border.rounding - direction: PathArc.Counterclockwise + PathCubic { + relativeX: root.Config.border.smoothing + relativeY: -root.Config.border.smoothing + relativeControl1X: root.Config.border.smoothing * 0.93 + relativeControl1Y: -root.Config.border.smoothing * 0.07 + relativeControl2X: root.Config.border.smoothing * 0.93 + relativeControl2Y: -root.Config.border.smoothing * 0.07 } PathLine { relativeX: 0 - relativeY: -Config.border.rounding + relativeY: -root.Config.border.smoothing } PathLine { - relativeX: Config.border.thickness + relativeX: root.Config.border.thickness relativeY: 0 } PathLine { @@ -129,15 +138,15 @@ Loader { StyledRect { anchors.centerIn: parent - radius: Appearance.rounding.large + radius: Tokens.rounding.large color: Colours.palette.m3surfaceContainerHigh scale: 0 Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0) - width: Math.min(parent.width - Appearance.padding.large * 2, implicitWidth) - implicitWidth: deleteConfirmationLayout.implicitWidth + Appearance.padding.large * 3 - implicitHeight: deleteConfirmationLayout.implicitHeight + Appearance.padding.large * 3 + width: Math.min(parent.width - Tokens.padding.large * 2, implicitWidth) + implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.large * 3 + implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.large * 3 MouseArea { anchors.fill: parent @@ -154,26 +163,26 @@ Loader { id: deleteConfirmationLayout anchors.fill: parent - anchors.margins: Appearance.padding.large * 1.5 - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large * 1.5 + spacing: Tokens.spacing.normal StyledText { text: qsTr("Delete recording?") - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } StyledText { Layout.fillWidth: true text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path) color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small wrapMode: Text.WrapAtWordBoundaryOrAnywhere } RowLayout { - Layout.topMargin: Appearance.spacing.normal + Layout.topMargin: Tokens.spacing.normal Layout.alignment: Qt.AlignRight - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal TextButton { text: qsTr("Cancel") @@ -194,8 +203,7 @@ Loader { Behavior on scale { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml index 842a550f9..cace56852 100644 --- a/modules/utilities/Wrapper.qml +++ b/modules/utilities/Wrapper.qml @@ -1,16 +1,20 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config -import Quickshell import QtQuick +import Quickshell +import Caelestia.Config +import qs.components +import qs.modules.sidebar as Sidebar +import qs.modules.bar.popouts as BarPopouts Item { id: root - required property var visibilities - required property Item sidebar - required property Item popouts + required property DrawerVisibilities visibilities + required property Sidebar.Wrapper sidebar + required property BarPopouts.Wrapper popouts + property real horizontalStretch + property matrix4x4 deformMatrix readonly property PersistentProperties props: PersistentProperties { property bool recordingListExpanded: false @@ -21,59 +25,48 @@ Item { reloadableId: "utilities" } readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) + property real offsetScale: shouldBeActive ? 0 : 1 + property real sidebarLerp - visible: height > 0 - implicitHeight: 0 - implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width - - onStateChanged: { - if (state === "visible" && timer.running) { - timer.triggered(); - timer.stop(); - } - } + visible: offsetScale < 1 + anchors.bottomMargin: (-implicitHeight - 5) * offsetScale + implicitHeight: content.implicitHeight + content.anchors.margins * 2 + implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Tokens.sizes.utilities.width * (1 - sidebarLerp) + opacity: 1 - offsetScale states: State { - name: "visible" - when: root.shouldBeActive + name: "attachedToSidebar" + when: root.visibilities.sidebar PropertyChanges { - root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2 + root.sidebarLerp: 1 } } transitions: [ Transition { from: "" - to: "visible" Anim { - target: root - property: "implicitHeight" - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + property: "sidebarLerp" + duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 + easing: Tokens.anim.standardAccel } }, Transition { - from: "visible" to: "" Anim { - target: root - property: "implicitHeight" - easing.bezierCurve: Appearance.anim.curves.emphasized + property: "sidebarLerp" + duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 + easing: Tokens.anim.standardDecel } } ] - Timer { - id: timer - - running: true - interval: Appearance.anim.durations.extraLarge - onTriggered: { - content.active = Qt.binding(() => root.shouldBeActive || root.visible); - content.visible = true; + Behavior on offsetScale { + Anim { + type: Anim.DefaultSpatial } } @@ -82,16 +75,17 @@ Item { anchors.top: parent.top anchors.left: parent.left - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - visible: false - active: true + asynchronous: true + active: root.shouldBeActive || root.visible sourceComponent: Content { - implicitWidth: root.implicitWidth - Appearance.padding.large * 2 + implicitWidth: root.implicitWidth - content.anchors.margins * 2 props: root.props visibilities: root.visibilities popouts: root.popouts + deformMatrix: root.deformMatrix } } } diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml index 0344e3ad2..c37ef3c52 100644 --- a/modules/utilities/cards/IdleInhibit.qml +++ b/modules/utilities/cards/IdleInhibit.qml @@ -1,17 +1,17 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config -import QtQuick -import QtQuick.Layouts StyledRect { id: root Layout.fillWidth: true - implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2 + implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer clip: true @@ -21,14 +21,14 @@ StyledRect { anchors.top: parent.top anchors.left: parent.left anchors.right: parent.right - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledRect { implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { @@ -37,7 +37,7 @@ StyledRect { anchors.centerIn: parent text: "coffee" color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } @@ -48,7 +48,7 @@ StyledRect { StyledText { Layout.fillWidth: true text: qsTr("Keep Awake") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } @@ -56,7 +56,7 @@ StyledRect { Layout.fillWidth: true text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") color: Colours.palette.m3onSurfaceVariant - font.pointSize: Appearance.font.size.small + font.pointSize: Tokens.font.size.small elide: Text.ElideRight } } @@ -70,11 +70,12 @@ StyledRect { Loader { id: activeChip + asynchronous: true anchors.bottom: parent.bottom anchors.left: parent.left - anchors.topMargin: Appearance.spacing.larger - anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight - anchors.leftMargin: Appearance.padding.large + anchors.topMargin: Tokens.spacing.larger + anchors.bottomMargin: IdleInhibitor.enabled ? Tokens.padding.large : -implicitHeight + anchors.leftMargin: Tokens.padding.large opacity: IdleInhibitor.enabled ? 1 : 0 scale: IdleInhibitor.enabled ? 1 : 0.5 @@ -82,32 +83,31 @@ StyledRect { Component.onCompleted: active = Qt.binding(() => opacity > 0) sourceComponent: StyledRect { - implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2 + implicitWidth: activeText.implicitWidth + Tokens.padding.normal * 2 + implicitHeight: activeText.implicitHeight + Tokens.padding.small * 2 - radius: Appearance.rounding.full + radius: Tokens.rounding.full color: Colours.palette.m3primary StyledText { id: activeText anchors.centerIn: parent - text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) + text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, GlobalConfig.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) color: Colours.palette.m3onPrimary - font.pointSize: Math.round(Appearance.font.size.small * 0.9) + font.pointSize: Math.round(Tokens.font.size.small * 0.9) } } Behavior on anchors.bottomMargin { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } Behavior on opacity { Anim { - duration: Appearance.anim.durations.small + type: Anim.StandardSmall } } @@ -118,8 +118,7 @@ StyledRect { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml index 7caccc83f..417f4147d 100644 --- a/modules/utilities/cards/Record.qml +++ b/modules/utilities/cards/Record.qml @@ -20,8 +20,9 @@ StyledRect { color: Colours.tPalette.m3surfaceContainer property bool actuallyRecording: Recorder.running + readonly property bool recordingBusy: Recorder.running || Recorder.starting property string lastError: "" - property string currentVideoMode: Config.utilities.recording.videoMode + property string currentVideoMode: Recorder.videoMode || Config.utilities.recording.videoMode || "fullscreen" // Computed audio mode based on settings readonly property string currentAudioMode: { @@ -52,7 +53,7 @@ StyledRect { } radius: Appearance.rounding.full - color: root.actuallyRecording ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer + color: root.recordingBusy ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer MaterialIcon { id: icon @@ -61,7 +62,7 @@ StyledRect { anchors.horizontalCenterOffset: -0.5 anchors.verticalCenterOffset: 1.5 text: "screen_record" - color: root.actuallyRecording ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer + color: root.recordingBusy ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer font.pointSize: Appearance.font.size.large } } @@ -81,11 +82,12 @@ StyledRect { Layout.fillWidth: true text: { if (root.lastError !== "") return qsTr("Error: %1").arg(root.lastError); + if (Recorder.starting) return root.startingText(Recorder.videoMode || root.currentVideoMode); if (Recorder.paused) return qsTr("Recording paused"); if (root.actuallyRecording) { - const videoText = root.currentVideoMode; - const audioText = root.currentAudioMode === "none" ? "no audio" : root.currentAudioMode; - return qsTr("Recording %1 - %2").arg(videoText).arg(audioText); + const videoText = root.videoModeLabel(Recorder.videoMode || root.currentVideoMode); + const audioText = root.audioModeLabel(Recorder.audioMode || root.currentAudioMode); + return qsTr("Recording %1 with %2").arg(videoText).arg(audioText); } return qsTr("Recording off"); } @@ -96,7 +98,7 @@ StyledRect { } SplitButton { - disabled: root.actuallyRecording + disabled: root.recordingBusy active: menuItems.find(m => m.mode === Config.utilities.recording.videoMode) ?? menuItems[0] menu.onItemSelected: item => { Config.utilities.recording.videoMode = item.mode; @@ -110,21 +112,21 @@ StyledRect { icon: "fullscreen" text: qsTr("Record fullscreen") activeText: qsTr("Fullscreen") - onClicked: startRecording() + onClicked: startRecording(mode) }, MenuItem { property string mode: "region" icon: "screenshot_region" text: qsTr("Record region") activeText: qsTr("Region") - onClicked: startRecording() + onClicked: startRecording(mode) }, MenuItem { property string mode: "window" icon: "web_asset" text: qsTr("Record window") activeText: qsTr("Window") - onClicked: startRecording() + onClicked: startRecording(mode) } ] } @@ -156,7 +158,7 @@ StyledRect { // Audio Sources Section ColumnLayout { Layout.fillWidth: true - visible: !root.actuallyRecording + visible: !root.recordingBusy spacing: Appearance.spacing.small RowLayout { @@ -171,19 +173,31 @@ StyledRect { Item { Layout.fillWidth: true } IconButton { - icon: root.props.recordingAudioExpanded ? "expand_less" : "expand_more" - type: IconButton.Tonal - font.pointSize: Appearance.font.size.small + icon: root.props.recordingAudioExpanded ? "unfold_less" : "unfold_more" + type: IconButton.Text + label.animate: true onClicked: { root.props.recordingAudioExpanded = !root.props.recordingAudioExpanded; } } } - ColumnLayout { + Item { + id: audioSourcesContainer + Layout.fillWidth: true - visible: root.props.recordingAudioExpanded - spacing: Appearance.spacing.smaller + Layout.preferredHeight: root.props.recordingAudioExpanded ? audioSourcesLayout.implicitHeight : 0 + clip: true + enabled: root.props.recordingAudioExpanded + opacity: root.props.recordingAudioExpanded ? 1 : 0 + visible: root.props.recordingAudioExpanded || height > 0 + + ColumnLayout { + id: audioSourcesLayout + + width: parent.width + y: root.props.recordingAudioExpanded ? 0 : -Appearance.spacing.small + spacing: Appearance.spacing.smaller // System Audio (Default Sink) RowLayout { @@ -288,13 +302,26 @@ StyledRect { } } } + + Behavior on y { + Anim { duration: Appearance.anim.durations.small } + } + } + + Behavior on Layout.preferredHeight { + Anim { type: Anim.DefaultSpatial } + } + + Behavior on opacity { + Anim { duration: Appearance.anim.durations.small } + } } } Loader { id: listOrControls - property bool running: root.actuallyRecording + property bool running: root.recordingBusy Layout.fillWidth: true Layout.preferredHeight: implicitHeight @@ -371,7 +398,7 @@ StyledRect { StyledRect { radius: Appearance.rounding.full - color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error + color: Recorder.starting ? Colours.palette.m3secondary : Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2 implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2 @@ -380,8 +407,8 @@ StyledRect { id: recText anchors.centerIn: parent animate: true - text: Recorder.paused ? "PAUSED" : "REC" - color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError + text: Recorder.starting ? "WAIT" : Recorder.paused ? "PAUSED" : "REC" + color: Recorder.starting ? Colours.palette.m3onSecondary : Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError font.family: Appearance.font.family.mono } @@ -390,7 +417,7 @@ StyledRect { } SequentialAnimation on opacity { - running: !Recorder.paused && root.actuallyRecording + running: !Recorder.starting && !Recorder.paused && root.actuallyRecording alwaysRunToEnd: true loops: Animation.Infinite Anim { @@ -410,6 +437,9 @@ StyledRect { StyledText { text: { + if (Recorder.starting) + return root.startingText(Recorder.videoMode || root.currentVideoMode); + const elapsed = Recorder.elapsed; const hours = Math.floor(elapsed / 3600); const mins = Math.floor((elapsed % 3600) / 60); @@ -429,6 +459,7 @@ StyledRect { } IconButton { + disabled: Recorder.starting label.animate: true icon: Recorder.paused ? "play_arrow" : "pause" toggle: true @@ -450,19 +481,45 @@ StyledRect { } } - function startRecording() { + function videoModeLabel(mode) { + switch (mode) { + case "region": return qsTr("Region"); + case "window": return qsTr("Window"); + default: return qsTr("Fullscreen"); + } + } + + function audioModeLabel(mode) { + switch (mode) { + case "combined": return qsTr("system audio + microphone"); + case "system": return qsTr("system audio"); + case "mic": return qsTr("microphone"); + default: return qsTr("no audio"); + } + } + + function startingText(mode) { + switch (mode) { + case "region": return qsTr("Select a recording region"); + case "window": return qsTr("Select a window to record"); + default: return qsTr("Starting fullscreen recording"); + } + } + + function startRecording(videoMode) { // Clear any previous errors root.lastError = ""; - const videoMode = Config.utilities.recording.videoMode || "fullscreen"; + const selectedVideoMode = videoMode || Config.utilities.recording.videoMode || "fullscreen"; const audioMode = root.currentAudioMode; - root.currentVideoMode = videoMode; + Config.utilities.recording.videoMode = selectedVideoMode; + root.currentVideoMode = selectedVideoMode; - console.log("Starting recording - Video:", videoMode, "Audio:", audioMode); + console.log("Starting recording - Video:", selectedVideoMode, "Audio:", audioMode); // Call Recorder service - const success = Recorder.start(videoMode, audioMode); + const success = Recorder.start(selectedVideoMode, audioMode); if (!success) { root.lastError = "Failed to start recording"; @@ -516,6 +573,6 @@ StyledRect { Component.onCompleted: { // Sync initial state root.actuallyRecording = Recorder.running; - root.currentVideoMode = Config.utilities.recording.videoMode || "fullscreen"; + root.currentVideoMode = Recorder.videoMode || Config.utilities.recording.videoMode || "fullscreen"; } } diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml index b9d757a4d..951f7f09e 100644 --- a/modules/utilities/cards/RecordingList.qml +++ b/modules/utilities/cards/RecordingList.qml @@ -1,23 +1,22 @@ pragma ComponentBehavior: Bound +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Caelestia.Config +import Caelestia.Models import qs.components -import qs.components.controls import qs.components.containers +import qs.components.controls import qs.services -import qs.config import qs.utils -import Caelestia -import Caelestia.Models -import Quickshell -import Quickshell.Widgets -import QtQuick -import QtQuick.Layouts ColumnLayout { id: root required property var props - required property var visibilities + required property DrawerVisibilities visibilities spacing: 0 @@ -28,19 +27,19 @@ ColumnLayout { onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller MaterialIcon { Layout.alignment: Qt.AlignVCenter text: "list" - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } StyledText { Layout.alignment: Qt.AlignVCenter Layout.fillWidth: true text: qsTr("Recordings") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } IconButton { @@ -62,8 +61,8 @@ ColumnLayout { } Layout.fillWidth: true - Layout.rightMargin: -Appearance.spacing.small - implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3) + Layout.rightMargin: -Tokens.spacing.small + implicitHeight: (Tokens.font.size.larger + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) clip: true StyledScrollBar.vertical: StyledScrollBar { @@ -78,14 +77,14 @@ ColumnLayout { anchors.left: list.contentItem.left anchors.right: list.contentItem.right - anchors.rightMargin: Appearance.spacing.small - spacing: Appearance.spacing.small / 2 + anchors.rightMargin: Tokens.spacing.small + spacing: Tokens.spacing.small / 2 Component.onCompleted: baseName = modelData.baseName StyledText { Layout.fillWidth: true - Layout.rightMargin: Appearance.spacing.small / 2 + Layout.rightMargin: Tokens.spacing.small / 2 text: { const time = recording.baseName; const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/); @@ -105,7 +104,7 @@ ColumnLayout { onClicked: { root.visibilities.utilities = false; root.visibilities.sidebar = false; - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]); + Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.playback, recording.modelData.path]); } } @@ -115,7 +114,7 @@ ColumnLayout { onClicked: { root.visibilities.utilities = false; root.visibilities.sidebar = false; - Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]); + Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.explorer, recording.modelData.path]); } } @@ -163,19 +162,20 @@ ColumnLayout { } Loader { + asynchronous: true anchors.centerIn: parent opacity: list.count === 0 ? 1 : 0 active: opacity > 0 sourceComponent: ColumnLayout { - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small MaterialIcon { Layout.alignment: Qt.AlignHCenter text: "scan_delete" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge opacity: root.props.recordingListExpanded ? 1 : 0 scale: root.props.recordingListExpanded ? 1 : 0 @@ -195,7 +195,7 @@ ColumnLayout { } RowLayout { - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller MaterialIcon { Layout.alignment: Qt.AlignHCenter @@ -233,8 +233,7 @@ ColumnLayout { Behavior on implicitHeight { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml index 5b57528bc..8c294eb38 100644 --- a/modules/utilities/cards/Toggles.qml +++ b/modules/utilities/cards/Toggles.qml @@ -1,112 +1,169 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell.Bluetooth +import Caelestia.Config import qs.components import qs.components.controls import qs.services -import qs.config -import qs.modules.controlcenter -import Quickshell -import Quickshell.Bluetooth -import QtQuick -import QtQuick.Layouts +import qs.modules.bar.popouts as BarPopouts StyledRect { id: root - required property var visibilities - required property Item popouts + required property DrawerVisibilities visibilities + required property BarPopouts.Wrapper popouts + + readonly property var quickToggles: { + const seenIds = new Set(); + + return Config.utilities.quickToggles.filter(item => { + if (!(item.enabled ?? true)) + return false; + + if (seenIds.has(item.id)) { + return false; + } + + if (item.id === "vpn") { + return GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); + } + + seenIds.add(item.id); + return true; + }); + } + readonly property int splitIndex: Math.ceil(quickToggles.length / 2) + readonly property bool needExtraRow: quickToggles.length > 6 Layout.fillWidth: true - implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: Colours.tPalette.m3surfaceContainer ColumnLayout { id: layout anchors.fill: parent - anchors.margins: Appearance.padding.large - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.large + spacing: Tokens.spacing.normal StyledText { text: qsTr("Quick Toggles") - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } - RowLayout { - Layout.alignment: Qt.AlignHCenter - spacing: Appearance.spacing.small - - Toggle { - icon: "wifi" - checked: Network.wifiEnabled - onClicked: Network.toggleWifi() - } + QuickToggleRow { + rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles + } - Toggle { - icon: "bluetooth" - checked: Bluetooth.defaultAdapter?.enabled ?? false - onClicked: { - const adapter = Bluetooth.defaultAdapter; - if (adapter) - adapter.enabled = !adapter.enabled; - } - } + QuickToggleRow { + visible: root.needExtraRow + rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] + } + } - Toggle { - icon: "mic" - checked: !Audio.sourceMuted - onClicked: { - const audio = Audio.source?.audio; - if (audio) - audio.muted = !audio.muted; - } - } + component QuickToggleRow: RowLayout { + property var rowModel: [] - Toggle { - icon: "settings" - inactiveOnColour: Colours.palette.m3onSurfaceVariant - toggle: false - onClicked: { - root.visibilities.utilities = false; - root.popouts.detach("network"); - } - } + Layout.fillWidth: true + spacing: Tokens.spacing.small - Toggle { - icon: "gamepad" - checked: GameMode.enabled - onClicked: GameMode.enabled = !GameMode.enabled - } + Repeater { + model: parent.rowModel - Toggle { - icon: "notifications_off" - checked: Notifs.dnd - onClicked: Notifs.dnd = !Notifs.dnd - } + delegate: DelegateChooser { + role: "id" - Toggle { - icon: "vpn_key" - checked: VPN.connected - enabled: !VPN.connecting - visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) - onClicked: VPN.toggle() + DelegateChoice { + roleValue: "wifi" + delegate: Toggle { + icon: "wifi" + checked: Nmcli.wifiEnabled + onClicked: Nmcli.toggleWifi() + } + } + DelegateChoice { + roleValue: "bluetooth" + delegate: Toggle { + icon: "bluetooth" + checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type + onClicked: { + const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type + if (adapter) + adapter.enabled = !adapter.enabled; + } + } + } + DelegateChoice { + roleValue: "mic" + delegate: Toggle { + icon: "mic" + checked: !Audio.sourceMuted + onClicked: { + const audio = Audio.source?.audio; + if (audio) + audio.muted = !audio.muted; + } + } + } + DelegateChoice { + roleValue: "settings" + delegate: Toggle { + icon: "settings" + inactiveOnColour: Colours.palette.m3onSurfaceVariant + toggle: false + onClicked: { + root.visibilities.utilities = false; + root.popouts.detach("network"); + } + } + } + DelegateChoice { + roleValue: "gameMode" + delegate: Toggle { + icon: "gamepad" + checked: GameMode.enabled + onClicked: GameMode.enabled = !GameMode.enabled + } + } + DelegateChoice { + roleValue: "dnd" + delegate: Toggle { + icon: "notifications_off" + checked: Notifs.dnd + onClicked: Notifs.dnd = !Notifs.dnd + } + } + DelegateChoice { + roleValue: "vpn" + delegate: Toggle { + icon: "vpn_key" + checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error" + enabled: !VPN.connecting + toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" + inactiveOnColour: Colours.palette.m3onSurfaceVariant + onClicked: VPN.toggle() + } + } } } } component Toggle: IconButton { Layout.fillWidth: true - Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) - radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal + Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) + radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) toggle: true - radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial - radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial + radiusAnim.easing: Tokens.anim.expressiveFastSpatial Behavior on Layout.preferredWidth { Anim { - duration: Appearance.anim.durations.expressiveFastSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial + type: Anim.FastSpatial } } } diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml index f47550006..5247e7737 100644 --- a/modules/utilities/toasts/ToastItem.qml +++ b/modules/utilities/toasts/ToastItem.qml @@ -1,10 +1,10 @@ +import QtQuick +import QtQuick.Layouts +import Caelestia +import Caelestia.Config import qs.components import qs.components.effects import qs.services -import qs.config -import Caelestia -import QtQuick -import QtQuick.Layouts StyledRect { id: root @@ -13,9 +13,9 @@ StyledRect { anchors.left: parent.left anchors.right: parent.right - implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3successContainer; @@ -50,13 +50,13 @@ StyledRect { id: layout anchors.fill: parent - anchors.margins: Appearance.padding.smaller - anchors.leftMargin: Appearance.padding.normal - anchors.rightMargin: Appearance.padding.normal - spacing: Appearance.spacing.normal + anchors.margins: Tokens.padding.smaller + anchors.leftMargin: Tokens.padding.normal + anchors.rightMargin: Tokens.padding.normal + spacing: Tokens.spacing.normal StyledRect { - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal color: { if (root.modelData.type === Toast.Success) return Colours.palette.m3success; @@ -68,7 +68,7 @@ StyledRect { } implicitWidth: implicitHeight - implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 + implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 MaterialIcon { id: icon @@ -84,7 +84,7 @@ StyledRect { return Colours.palette.m3onError; return Colours.palette.m3onSurfaceVariant; } - font.pointSize: Math.round(Appearance.font.size.large * 1.2) + font.pointSize: Math.round(Tokens.font.size.large * 1.2) } } @@ -106,7 +106,7 @@ StyledRect { return Colours.palette.m3onErrorContainer; return Colours.palette.m3onSurface; } - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal elide: Text.ElideRight } diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index 2915404e0..21f2934e1 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -1,18 +1,29 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.config -import Caelestia -import Quickshell import QtQuick +import Quickshell +import Caelestia +import Caelestia.Config +import qs.components +import qs.services Item { id: root - readonly property int spacing: Appearance.spacing.small + readonly property int spacing: Tokens.spacing.small property bool flag - implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 + function shouldShowToast(toast: Toast): bool { + if (!Notifs.hasFullscreen()) + return true; + if (Config.utilities.toasts.fullscreen === "all") + return true; + if (Config.utilities.toasts.fullscreen === "important") + return toast.type === Toast.Warning || toast.type === Toast.Error; + return false; + } + + implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.normal * 2 implicitHeight: { let h = -spacing; for (let i = 0; i < repeater.count; i++) { @@ -31,10 +42,12 @@ Item { const toasts = []; let count = 0; for (const toast of Toaster.toasts) { + if (!root.shouldShowToast(toast)) + continue; toasts.push(toast); if (!toast.closed) { count++; - if (count > Config.utilities.maxToasts) + if (count > root.Config.utilities.maxToasts) break; } } @@ -98,8 +111,7 @@ Item { properties: "opacity,scale" from: 0 to: 1 - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } ParallelAnimation { @@ -135,8 +147,7 @@ Item { Behavior on anchors.bottomMargin { Anim { - duration: Appearance.anim.durations.expressiveDefaultSpatial - easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + type: Anim.DefaultSpatial } } } diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml index 89acfe6d6..c4fbbd53a 100644 --- a/modules/windowinfo/Buttons.qml +++ b/modules/windowinfo/Buttons.qml @@ -1,9 +1,11 @@ -import qs.components -import qs.services -import qs.config -import Quickshell.Widgets +pragma ComponentBehavior: Bound + import QtQuick import QtQuick.Layouts +import Quickshell.Widgets +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root @@ -12,14 +14,14 @@ ColumnLayout { property bool moveToWsExpanded anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small RowLayout { - Layout.topMargin: Appearance.padding.large - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large + Layout.topMargin: Tokens.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal StyledText { Layout.fillWidth: true @@ -29,17 +31,14 @@ ColumnLayout { StyledRect { color: Colours.palette.m3primary - radius: Appearance.rounding.small + radius: Tokens.rounding.small - implicitWidth: moveToWsIcon.implicitWidth + Appearance.padding.small * 2 - implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small + implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small * 2 + implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.small StateLayer { color: Colours.palette.m3onPrimary - - function onClicked(): void { - root.moveToWsExpanded = !root.moveToWsExpanded; - } + onClicked: root.moveToWsExpanded = !root.moveToWsExpanded } MaterialIcon { @@ -50,27 +49,27 @@ ColumnLayout { animate: true text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right" color: Colours.palette.m3onPrimary - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } WrapperItem { Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0 clip: true - topMargin: Appearance.spacing.normal - bottomMargin: Appearance.spacing.normal + topMargin: Tokens.spacing.normal + bottomMargin: Tokens.spacing.normal GridLayout { id: wsGrid - rowSpacing: Appearance.spacing.smaller - columnSpacing: Appearance.spacing.normal + rowSpacing: Tokens.spacing.smaller + columnSpacing: Tokens.spacing.normal columns: 5 Repeater { @@ -81,14 +80,14 @@ ColumnLayout { readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 readonly property bool isCurrent: root.client?.workspace.id === wsId + onClicked: { + Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); + } + color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer text: wsId disabled: isCurrent - - function onClicked(): void { - Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); - } } } } @@ -100,24 +99,22 @@ ColumnLayout { RowLayout { Layout.fillWidth: true - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large - Layout.bottomMargin: Appearance.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large + Layout.bottomMargin: Tokens.padding.large - spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small + spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.normal : Tokens.spacing.small Button { color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") - - function onClicked(): void { - Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); - } + onClicked: Hypr.dispatch(`togglefloating address:0x${root.client?.address}`) } Loader { - active: root.client?.lastIpcObject.floating + asynchronous: true + active: root.client?.lastIpcObject.floating ?? false Layout.fillWidth: active Layout.leftMargin: active ? 0 : -parent.spacing Layout.rightMargin: active ? 0 : -parent.spacing @@ -126,10 +123,7 @@ ColumnLayout { color: Colours.palette.m3secondaryContainer onColor: Colours.palette.m3onSecondaryContainer text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") - - function onClicked(): void { - Hypr.dispatch(`pin address:0x${root.client?.address}`); - } + onClicked: Hypr.dispatch(`pin address:0x${root.client?.address}`) } } @@ -137,10 +131,7 @@ ColumnLayout { color: Colours.palette.m3errorContainer onColor: Colours.palette.m3onErrorContainer text: qsTr("Kill") - - function onClicked(): void { - Hypr.dispatch(`killwindow address:0x${root.client?.address}`); - } + onClicked: Hypr.dispatch(`killwindow address:0x${root.client?.address}`) } } @@ -149,22 +140,18 @@ ColumnLayout { property alias disabled: stateLayer.disabled property alias text: label.text - function onClicked(): void { - } + signal clicked - radius: Appearance.rounding.small + radius: Tokens.rounding.small Layout.fillWidth: true - implicitHeight: label.implicitHeight + Appearance.padding.small * 2 + implicitHeight: label.implicitHeight + Tokens.padding.small * 2 StateLayer { id: stateLayer color: parent.onColor - - function onClicked(): void { - parent.onClicked(); - } + onClicked: parent.clicked() } StyledText { @@ -174,7 +161,7 @@ ColumnLayout { animate: true color: parent.onColor - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } } diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml index f9ee66a68..1820409f0 100644 --- a/modules/windowinfo/Details.qml +++ b/modules/windowinfo/Details.qml @@ -1,9 +1,9 @@ -import qs.components -import qs.services -import qs.config -import Quickshell.Hyprland import QtQuick import QtQuick.Layouts +import Quickshell.Hyprland +import Caelestia.Config +import qs.components +import qs.services ColumnLayout { id: root @@ -11,15 +11,15 @@ ColumnLayout { required property HyprlandToplevel client anchors.fill: parent - spacing: Appearance.spacing.small + spacing: Tokens.spacing.small Label { - Layout.topMargin: Appearance.padding.large * 2 + Layout.topMargin: Tokens.padding.large * 2 text: root.client?.title ?? qsTr("No active client") wrapMode: Text.WrapAtWordBoundaryOrAnywhere - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large font.weight: 500 } @@ -27,16 +27,16 @@ ColumnLayout { text: root.client?.lastIpcObject.class ?? qsTr("No active client") color: Colours.palette.m3tertiary - font.pointSize: Appearance.font.size.larger + font.pointSize: Tokens.font.size.larger } StyledRect { Layout.fillWidth: true Layout.preferredHeight: 1 - Layout.leftMargin: Appearance.padding.large * 2 - Layout.rightMargin: Appearance.padding.large * 2 - Layout.topMargin: Appearance.spacing.normal - Layout.bottomMargin: Appearance.spacing.large + Layout.leftMargin: Tokens.padding.large * 2 + Layout.rightMargin: Tokens.padding.large * 2 + Layout.topMargin: Tokens.spacing.normal + Layout.bottomMargin: Tokens.spacing.large color: Colours.palette.m3secondary } @@ -130,11 +130,11 @@ ColumnLayout { required property string text property alias color: icon.color - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large Layout.fillWidth: true - spacing: Appearance.spacing.smaller + spacing: Tokens.spacing.smaller MaterialIcon { id: icon @@ -149,13 +149,13 @@ ColumnLayout { text: detail.text elide: Text.ElideRight - font.pointSize: Appearance.font.size.normal + font.pointSize: Tokens.font.size.normal } } component Label: StyledText { - Layout.leftMargin: Appearance.padding.large - Layout.rightMargin: Appearance.padding.large + Layout.leftMargin: Tokens.padding.large + Layout.rightMargin: Tokens.padding.large Layout.fillWidth: true elide: Text.ElideRight horizontalAlignment: Text.AlignHCenter diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml index 4cc0aab86..fed858915 100644 --- a/modules/windowinfo/Preview.qml +++ b/modules/windowinfo/Preview.qml @@ -1,13 +1,13 @@ pragma ComponentBehavior: Bound -import qs.components -import qs.services -import qs.config -import Quickshell -import Quickshell.Wayland -import Quickshell.Hyprland import QtQuick import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Quickshell.Wayland +import Caelestia.Config +import qs.components +import qs.services Item { id: root @@ -15,7 +15,7 @@ Item { required property ShellScreen screen required property HyprlandToplevel client - Layout.preferredWidth: preview.implicitWidth + Appearance.padding.large * 2 + Layout.preferredWidth: preview.implicitWidth + Tokens.padding.large * 2 Layout.fillHeight: true StyledClippingRect { @@ -24,15 +24,16 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.top: parent.top anchors.bottom: label.top - anchors.topMargin: Appearance.padding.large - anchors.bottomMargin: Appearance.spacing.normal + anchors.topMargin: Tokens.padding.large + anchors.bottomMargin: Tokens.spacing.normal implicitWidth: view.implicitWidth color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.small + radius: Tokens.rounding.small Loader { + asynchronous: true anchors.centerIn: parent active: !root.client @@ -43,14 +44,14 @@ Item { Layout.alignment: Qt.AlignHCenter text: "web_asset_off" color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge * 3 + font.pointSize: Tokens.font.size.extraLarge * 3 } StyledText { Layout.alignment: Qt.AlignHCenter text: qsTr("No active client") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.extraLarge + font.pointSize: Tokens.font.size.extraLarge font.weight: 500 } @@ -58,7 +59,7 @@ Item { Layout.alignment: Qt.AlignHCenter text: qsTr("Try switching to a window") color: Colours.palette.m3outline - font.pointSize: Appearance.font.size.large + font.pointSize: Tokens.font.size.large } } } @@ -68,7 +69,7 @@ Item { anchors.centerIn: parent - captureSource: root.client?.wayland ?? null + captureSource: root.client?.wayland ?? null // qmllint disable unresolved-type live: true constraintSize.width: root.client ? parent.height * Math.min(root.screen.width / root.screen.height, root.client?.lastIpcObject.size[0] / root.client?.lastIpcObject.size[1]) : parent.height @@ -81,7 +82,7 @@ Item { anchors.horizontalCenter: parent.horizontalCenter anchors.bottom: parent.bottom - anchors.bottomMargin: Appearance.padding.large + anchors.bottomMargin: Tokens.padding.large animate: true text: { diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml index 919b3fbb1..7ee35c29b 100644 --- a/modules/windowinfo/WindowInfo.qml +++ b/modules/windowinfo/WindowInfo.qml @@ -1,10 +1,10 @@ -import qs.components -import qs.services -import qs.config -import Quickshell -import Quickshell.Hyprland import QtQuick import QtQuick.Layouts +import Quickshell +import Quickshell.Hyprland +import Caelestia.Config +import qs.components +import qs.services Item { id: root @@ -13,15 +13,15 @@ Item { required property HyprlandToplevel client implicitWidth: child.implicitWidth - implicitHeight: screen.height * Config.winfo.sizes.heightMult + implicitHeight: screen.height * Tokens.sizes.winfo.heightMult RowLayout { id: child anchors.fill: parent - anchors.margins: Appearance.padding.large + anchors.margins: Tokens.padding.large - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal Preview { screen: root.screen @@ -29,9 +29,9 @@ Item { } ColumnLayout { - spacing: Appearance.spacing.normal + spacing: Tokens.spacing.normal - Layout.preferredWidth: Config.winfo.sizes.detailsWidth + Layout.preferredWidth: Tokens.sizes.winfo.detailsWidth Layout.fillHeight: true StyledRect { @@ -39,7 +39,7 @@ Item { Layout.fillHeight: true color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Details { client: root.client @@ -51,7 +51,7 @@ Item { Layout.preferredHeight: buttons.implicitHeight color: Colours.tPalette.m3surfaceContainer - radius: Appearance.rounding.normal + radius: Tokens.rounding.normal Buttons { id: buttons diff --git a/nix/default.nix b/nix/default.nix index 67747b2d9..3a153dd0c 100644 --- a/nix/default.nix +++ b/nix/default.nix @@ -126,8 +126,6 @@ in prePatch = '' substituteInPlace assets/pam.d/fprint \ --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so - substituteInPlace shell.qml \ - --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' ''; postInstall = '' diff --git a/plugin/src/Caelestia/Blobs/CMakeLists.txt b/plugin/src/Caelestia/Blobs/CMakeLists.txt new file mode 100644 index 000000000..9506f7f44 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/CMakeLists.txt @@ -0,0 +1,20 @@ +qml_module(caelestia-blobs + URI Caelestia.Blobs + SOURCES + blobgroup.cpp + blobshape.cpp + blobrect.cpp + blobinvertedrect.cpp + blobmaterial.cpp + LIBRARIES + Qt::Quick +) + +qt_add_shaders(caelestia-blobs "blob_shaders" + BATCHABLE OPTIMIZED NOHLSL NOMSL + GLSL "300es,330" + PREFIX "/" + FILES + shaders/blob.frag + shaders/blob.vert +) diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp new file mode 100644 index 000000000..a4703c860 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp @@ -0,0 +1,104 @@ +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" +#include "blobshape.hpp" + +BlobGroup::BlobGroup(QObject* parent) + : QObject(parent) {} + +BlobGroup::~BlobGroup() { + for (auto* shape : std::as_const(m_shapes)) + shape->m_group = nullptr; + if (m_invertedRect) + static_cast(m_invertedRect)->m_group = nullptr; +} + +void BlobGroup::setSmoothing(qreal s) { + if (qFuzzyCompare(m_smoothing, s)) + return; + m_smoothing = s; + emit smoothingChanged(); + markDirty(); +} + +void BlobGroup::setColor(const QColor& c) { + if (m_color == c) + return; + m_color = c; + emit colorChanged(); + markDirty(); +} + +void BlobGroup::addShape(BlobShape* shape) { + if (!shape || m_shapes.contains(shape)) + return; + m_shapes.append(shape); + markDirty(); +} + +void BlobGroup::removeShape(BlobShape* shape) { + m_shapes.removeOne(shape); + markDirty(); +} + +void BlobGroup::setInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect == rect) + return; + m_invertedRect = rect; + markDirty(); +} + +void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) { + if (m_invertedRect != rect) + return; + m_invertedRect = nullptr; + markDirty(); +} + +void BlobGroup::markDirty() { + m_physicsUpdated = false; + for (auto* shape : std::as_const(m_shapes)) { + shape->polish(); + shape->update(); + } + if (m_invertedRect) { + static_cast(m_invertedRect)->polish(); + static_cast(m_invertedRect)->update(); + } +} + +void BlobGroup::markShapeDirty(BlobShape* source) { + m_physicsUpdated = false; + + source->polish(); + source->update(); + + // Use cached padded rects to find spatial neighbors + const float pad = static_cast(m_smoothing) * 2.0f; + const QRectF srcRect(static_cast(source->m_cachedPaddedX - pad), + static_cast(source->m_cachedPaddedY - pad), static_cast(source->m_cachedPaddedW + pad * 2.0f), + static_cast(source->m_cachedPaddedH + pad * 2.0f)); + + for (auto* shape : std::as_const(m_shapes)) { + if (shape == source) + continue; + const QRectF otherRect(static_cast(shape->m_cachedPaddedX), static_cast(shape->m_cachedPaddedY), + static_cast(shape->m_cachedPaddedW), static_cast(shape->m_cachedPaddedH)); + if (srcRect.intersects(otherRect)) { + shape->polish(); + shape->update(); + } + } + + if (m_invertedRect && static_cast(m_invertedRect) != source) { + static_cast(m_invertedRect)->polish(); + static_cast(m_invertedRect)->update(); + } +} + +void BlobGroup::ensurePhysicsUpdated() { + if (m_physicsUpdated) + return; + m_physicsUpdated = true; + for (auto* shape : std::as_const(m_shapes)) + shape->updatePhysics(); +} diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp new file mode 100644 index 000000000..e09125a96 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include +#include +#include +#include + +class BlobShape; +class BlobInvertedRect; + +class BlobGroup : public QObject { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) + Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) + +public: + explicit BlobGroup(QObject* parent = nullptr); + ~BlobGroup() override; + + qreal smoothing() const { return m_smoothing; } + + void setSmoothing(qreal s); + + QColor color() const { return m_color; } + + void setColor(const QColor& c); + + void addShape(BlobShape* shape); + void removeShape(BlobShape* shape); + + void setInvertedRect(BlobInvertedRect* rect); + void clearInvertedRect(BlobInvertedRect* rect); + + const QList& shapes() const { return m_shapes; } + + BlobInvertedRect* invertedRect() const { return m_invertedRect; } + + void markDirty(); + void markShapeDirty(BlobShape* source); + void ensurePhysicsUpdated(); + +signals: + void smoothingChanged(); + void colorChanged(); + +private: + qreal m_smoothing = 32.0; + QColor m_color{ 0x44, 0x88, 0xff }; + QList m_shapes; + BlobInvertedRect* m_invertedRect = nullptr; + bool m_physicsUpdated = false; +}; diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp new file mode 100644 index 000000000..46ee73a4a --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp @@ -0,0 +1,184 @@ +#include "blobinvertedrect.hpp" +#include "blobgroup.hpp" +#include "blobmaterial.hpp" + +#include +#include + +#include +#include + +BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) + : BlobShape(parent) {} + +static void setFrameIndices(quint16* idx) { + // Top strip: 0-1-4, 1-5-4 + idx[0] = 0; + idx[1] = 1; + idx[2] = 4; + idx[3] = 1; + idx[4] = 5; + idx[5] = 4; + // Right strip: 1-2-5, 2-6-5 + idx[6] = 1; + idx[7] = 2; + idx[8] = 5; + idx[9] = 2; + idx[10] = 6; + idx[11] = 5; + // Bottom strip: 2-3-6, 3-7-6 + idx[12] = 2; + idx[13] = 3; + idx[14] = 6; + idx[15] = 3; + idx[16] = 7; + idx[17] = 6; + // Left strip: 3-0-7, 0-4-7 + idx[18] = 3; + idx[19] = 0; + idx[20] = 7; + idx[21] = 0; + idx[22] = 4; + idx[23] = 7; +} + +QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + delete oldNode; + return nullptr; + } + + const float pad = static_cast(m_group->smoothing()); + + // Compute inner hole boundary in local coords + // Inset past the inner border edge by 2x smoothing to cover the blend zone + const float inset = pad * 2.0f; + const float holeLeft = static_cast(m_borderLeft) + inset; + const float holeTop = static_cast(m_borderTop) + inset; + const float holeRight = static_cast(width() - m_borderRight) - inset; + const float holeBot = static_cast(height() - m_borderBottom) - inset; + + // If the hole is too small or invalid, fall back to full quad + if (holeLeft >= holeRight || holeTop >= holeBot) + return BlobShape::updatePaintNode(oldNode, nullptr); + + auto* node = static_cast(oldNode); + + const bool needsRebuild = !node || node->geometry()->vertexCount() != 8; + + if (needsRebuild) { + delete oldNode; + node = new QSGGeometryNode; + + auto* geometry = + new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType); + geometry->setDrawingMode(QSGGeometry::DrawTriangles); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + setFrameIndices(geometry->indexDataAsUShort()); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // Outer bounds (local coords) + const float x0 = static_cast(m_localPaddedRect.x()); + const float y0 = static_cast(m_localPaddedRect.y()); + const float x1 = x0 + static_cast(m_localPaddedRect.width()); + const float y1 = y0 + static_cast(m_localPaddedRect.height()); + const float w = x1 - x0; + const float h = y1 - y0; + + // Update vertex positions and texture coordinates + auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); + + // Outer corners + v[0].set(x0, y0, 0.0f, 0.0f); + v[1].set(x1, y0, 1.0f, 0.0f); + v[2].set(x1, y1, 1.0f, 1.0f); + v[3].set(x0, y1, 0.0f, 1.0f); + // Inner corners (hole) + v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); + v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); + v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); + v[7].set(holeLeft, holeBot, (holeLeft - x0) / w, (holeBot - y0) / h); + + node->markDirty(QSGNode::DirtyGeometry); + + // Update material uniforms + auto* material = static_cast(node->material()); + material->m_paddedX = m_cachedPaddedX; + material->m_paddedY = m_cachedPaddedY; + material->m_paddedW = m_cachedPaddedW; + material->m_paddedH = m_cachedPaddedH; + material->m_smoothFactor = pad; + material->m_myIndex = m_cachedMyIndex; + material->m_color = m_group->color(); + material->m_hasInverted = m_cachedHasInverted ? 1 : 0; + material->m_invertedRadius = m_cachedInvertedRadius; + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); + + const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = m_cachedRects[i]; + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} + +BlobInvertedRect::~BlobInvertedRect() { + if (m_group) + m_group->clearInvertedRect(this); +} + +void BlobInvertedRect::setBorderLeft(qreal v) { + if (qFuzzyCompare(m_borderLeft, v)) + return; + m_borderLeft = v; + emit borderLeftChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderRight(qreal v) { + if (qFuzzyCompare(m_borderRight, v)) + return; + m_borderRight = v; + emit borderRightChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderTop(qreal v) { + if (qFuzzyCompare(m_borderTop, v)) + return; + m_borderTop = v; + emit borderTopChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::setBorderBottom(qreal v) { + if (qFuzzyCompare(m_borderBottom, v)) + return; + m_borderBottom = v; + emit borderBottomChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobInvertedRect::registerWithGroup() { + if (m_group) + m_group->setInvertedRect(this); +} + +void BlobInvertedRect::unregisterFromGroup() { + if (m_group) + m_group->clearInvertedRect(this); +} diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp new file mode 100644 index 000000000..f7fa6c0a5 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp @@ -0,0 +1,54 @@ +#pragma once + +#include "blobshape.hpp" + +#include + +class BlobInvertedRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged) + Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged) + Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged) + Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged) + +public: + explicit BlobInvertedRect(QQuickItem* parent = nullptr); + ~BlobInvertedRect() override; + + qreal borderLeft() const { return m_borderLeft; } + + void setBorderLeft(qreal v); + + qreal borderRight() const { return m_borderRight; } + + void setBorderRight(qreal v); + + qreal borderTop() const { return m_borderTop; } + + void setBorderTop(qreal v); + + qreal borderBottom() const { return m_borderBottom; } + + void setBorderBottom(qreal v); + +signals: + void borderLeftChanged(); + void borderRightChanged(); + void borderTopChanged(); + void borderBottomChanged(); + +protected: + bool isInvertedRect() const override { return true; } + + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + + void registerWithGroup() override; + void unregisterFromGroup() override; + +private: + qreal m_borderLeft = 0; + qreal m_borderRight = 0; + qreal m_borderTop = 0; + qreal m_borderBottom = 0; +}; diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp new file mode 100644 index 000000000..721a2532a --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp @@ -0,0 +1,102 @@ +#include "blobmaterial.hpp" + +#include + +static_assert(sizeof(decltype(BlobRectData::excludeMask)) == sizeof(float), + "BlobMaterial packs excludeMask into a float slot via memcpy"); + +QSGMaterialType* BlobMaterial::type() const { + static QSGMaterialType s_type; + return &s_type; +} + +QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const { + return new BlobMaterialShader; +} + +int BlobMaterial::compare(const QSGMaterial* other) const { + if (this < other) + return -1; + if (this > other) + return 1; + return 0; +} + +BlobMaterialShader::BlobMaterialShader() { + setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb")); + setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); +} + +bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { + Q_UNUSED(oldMaterial); + auto* mat = static_cast(newMaterial); + QByteArray* buf = state.uniformData(); + Q_ASSERT(buf->size() >= 1440); + + if (state.isMatrixDirty()) { + const QMatrix4x4 m = state.combinedMatrix(); + memcpy(buf->data(), m.constData(), 64); + } + if (state.isOpacityDirty()) { + const float opacity = state.opacity(); + memcpy(buf->data() + 64, &opacity, 4); + } + + // Padded rect (offset 68) + memcpy(buf->data() + 68, &mat->m_paddedX, 4); + memcpy(buf->data() + 72, &mat->m_paddedY, 4); + memcpy(buf->data() + 76, &mat->m_paddedW, 4); + memcpy(buf->data() + 80, &mat->m_paddedH, 4); + + // Smooth factor (offset 84) + memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); + + // Rect count (offset 88) + memcpy(buf->data() + 88, &mat->m_rectCount, 4); + + // My index (offset 92) + memcpy(buf->data() + 92, &mat->m_myIndex, 4); + + // Color as vec4 (offset 96, 16 bytes) + const float color[4] = { + static_cast(mat->m_color.redF()), + static_cast(mat->m_color.greenF()), + static_cast(mat->m_color.blueF()), + static_cast(mat->m_color.alphaF()), + }; + memcpy(buf->data() + 96, color, 16); + + // Has inverted (offset 112) + memcpy(buf->data() + 112, &mat->m_hasInverted, 4); + + // Inverted radius (offset 116) + memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); + + // Padding at 120-127 (skip) + + // Inverted outer (offset 128, 16 bytes) + memcpy(buf->data() + 128, mat->m_invertedOuter, 16); + + // Inverted inner (offset 144, 16 bytes) + memcpy(buf->data() + 144, mat->m_invertedInner, 16); + + // Rect data (offset 160, each rect = 5 vec4s = 80 bytes) + const int count = qMin(mat->m_rectCount, 16); + for (int i = 0; i < count; ++i) { + const auto& r = mat->m_rects[i]; + const int base = 160 + i * 80; + // Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt) + float maskAsFloat; + memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); + const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; + const float d1[4] = { maskAsFloat, r.offsetX, r.offsetY, r.minEig }; + const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; + memcpy(buf->data() + base, d0, 16); + memcpy(buf->data() + base + 16, d1, 16); + memcpy(buf->data() + base + 32, r.invDeform, 16); + memcpy(buf->data() + base + 48, d3, 16); + memcpy(buf->data() + base + 64, r.radius, 16); + } + + return true; +} diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp new file mode 100644 index 000000000..bf1eda756 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include + +struct BlobRectData { + float cx = 0, cy = 0, hw = 0, hh = 0; + float offsetX = 0, offsetY = 0; + float minEig = 1.0f; + // Inverse of 2x2 deformation matrix, column-major for GLSL + float invDeform[4] = { 1, 0, 0, 1 }; + // Screen-space AABB half-extents of the deformed rect + float screenHalfX = 0, screenHalfY = 0; + // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU + float radius[4] = { 0, 0, 0, 0 }; + // Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. + // Used by the shader to skip smin between excluded pairs. + int excludeMask = 0; +}; + +class BlobMaterial : public QSGMaterial { +public: + QSGMaterialType* type() const override; + QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override; + int compare(const QSGMaterial* other) const override; + + float m_paddedX = 0; + float m_paddedY = 0; + float m_paddedW = 0; + float m_paddedH = 0; + float m_smoothFactor = 32.0f; + int m_rectCount = 0; + int m_myIndex = -2; + QColor m_color{ 0x44, 0x88, 0xff }; + int m_hasInverted = 0; + float m_invertedRadius = 0; + float m_invertedOuter[4] = {}; + float m_invertedInner[4] = {}; + BlobRectData m_rects[16] = {}; +}; + +class BlobMaterialShader : public QSGMaterialShader { +public: + BlobMaterialShader(); + bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override; +}; diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp new file mode 100644 index 000000000..15486d83c --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.cpp @@ -0,0 +1,245 @@ +#include "blobrect.hpp" +#include "blobgroup.hpp" + +#include +#include + +BlobRect::BlobRect(QQuickItem* parent) + : BlobShape(parent) {} + +BlobRect::~BlobRect() { + if (m_group) + m_group->removeShape(this); +} + +void BlobRect::updatePolish() { + BlobShape::updatePolish(); + + if (m_physicsActive) { + // Check if deformation is visually imperceptible + float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); + float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); + + if (totalDelta < 0.004f && totalVel < 0.05f) { + // Snap to rest, no visible deformation + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; + m_deformMatrix = QMatrix4x4(); + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + m_physicsActive = false; + } else { + QMetaObject::invokeMethod( + this, + [this]() { + if (m_physicsActive && m_group) + m_group->markDirty(); + }, + Qt::QueuedConnection); + } + } +} + +void BlobRect::updatePhysics() { + const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); + + if (!m_hasPrevPos) { + m_prevScenePos = scenePos; + m_elapsed.start(); + m_hasPrevPos = true; + return; + } + + const float dt = static_cast(m_elapsed.restart()) / 1000.0f; + if (dt > 0.1f || dt < 0.001f) { + m_prevScenePos = scenePos; + // Still check atRest on skipped frames to avoid getting stuck + if (m_physicsActive) + checkAtRest(0.0f); + return; + } + + const float velX = static_cast(scenePos.x() - m_prevScenePos.x()) / dt; + const float velY = static_cast(scenePos.y() - m_prevScenePos.y()) / dt; + m_prevScenePos = scenePos; + + const float speed = std::sqrt(velX * velX + velY * velY); + + if (!m_physicsActive) { + if (speed < 5.0f) + return; + m_physicsActive = true; + } + + // Compute target deformation matrix from velocity + // R(θ) * diag(stretch, compress) * R(θ)^T + const float kStretchFactor = static_cast(m_deformScale); + constexpr float kMaxStretch = 0.35f; + + float target00 = 1.0f; + float target01 = 0.0f; + float target11 = 1.0f; + + if (speed > 5.0f) { + const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); + const float targetCompress = 1.0f / targetStretch; + + const float cosA = velX / speed; + const float sinA = velY / speed; + const float cos2 = cosA * cosA; + const float sin2 = sinA * sinA; + const float cs = cosA * sinA; + + target00 = targetStretch * cos2 + targetCompress * sin2; + target01 = (targetStretch - targetCompress) * cs; + target11 = targetStretch * sin2 + targetCompress * cos2; + } + + // Underdamped spring on each matrix component + const float kStiffness = static_cast(m_stiffness); + const float kDamping = static_cast(m_damping); + + const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; + m_dmVel00 += accel00 * dt; + m_dm00 += m_dmVel00 * dt; + + const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; + m_dmVel01 += accel01 * dt; + m_dm01 += m_dmVel01 * dt; + + const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; + m_dmVel11 += accel11 * dt; + m_dm11 += m_dmVel11 * dt; + + m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + + checkAtRest(speed); +} + +void BlobRect::setTopLeftRadius(qreal r) { + if (!qFuzzyCompare(m_topLeftRadius, r)) { + m_topLeftRadius = r; + emit topLeftRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setTopRightRadius(qreal r) { + if (!qFuzzyCompare(m_topRightRadius, r)) { + m_topRightRadius = r; + emit topRightRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setBottomLeftRadius(qreal r) { + if (!qFuzzyCompare(m_bottomLeftRadius, r)) { + m_bottomLeftRadius = r; + emit bottomLeftRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::setBottomRightRadius(qreal r) { + if (!qFuzzyCompare(m_bottomRightRadius, r)) { + m_bottomRightRadius = r; + emit bottomRightRadiusChanged(); + if (m_group) + m_group->markDirty(); + } +} + +void BlobRect::cornerRadii(float out[4]) const { + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto base = std::min(static_cast(m_radius), maxR); + out[0] = std::min(m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base, maxR); + out[1] = std::min(m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base, maxR); + out[2] = std::min(m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base, maxR); + out[3] = std::min(m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base, maxR); +} + +bool BlobRect::isExcluded(const BlobShape* other) const { + for (const auto& ptr : m_exclude) { + if (ptr == other) + return true; + } + return false; +} + +QQmlListProperty BlobRect::exclude() { + return QQmlListProperty( + this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); +} + +void BlobRect::excludeAppend(QQmlListProperty* prop, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_exclude.append(rect); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +qsizetype BlobRect::excludeCount(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + return self->m_exclude.size(); +} + +BlobRect* BlobRect::excludeAt(QQmlListProperty* prop, qsizetype index) { + auto* self = static_cast(prop->object); + return self->m_exclude.at(index); +} + +void BlobRect::excludeClear(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) + return; + self->m_exclude.clear(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect::excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect) { + auto* self = static_cast(prop->object); + self->m_exclude[index] = rect; + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect::excludeRemoveLast(QQmlListProperty* prop) { + auto* self = static_cast(prop->object); + if (self->m_exclude.isEmpty()) + return; + self->m_exclude.removeLast(); + if (self->m_group) + self->m_group->markDirty(); + emit self->excludeChanged(); +} + +void BlobRect::checkAtRest(float speed) { + constexpr float kEpsilon = 0.002f; + const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && + std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && + std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f; + + if (atRest) { + m_dm00 = 1.0f; + m_dm01 = 0.0f; + m_dm11 = 1.0f; + m_dmVel00 = 0.0f; + m_dmVel01 = 0.0f; + m_dmVel11 = 0.0f; + m_deformMatrix = QMatrix4x4(); // identity + emit rawDeformMatrixChanged(); + updateCenteredDeformMatrix(); + m_physicsActive = false; + } +} diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp new file mode 100644 index 000000000..d2d6ad453 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobrect.hpp @@ -0,0 +1,126 @@ +#pragma once + +#include "blobshape.hpp" + +#include +#include +#include +#include + +class BlobRect : public BlobShape { + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged) + Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) + Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) + Q_PROPERTY(QQmlListProperty exclude READ exclude NOTIFY excludeChanged) + Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) + Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) + Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) + Q_PROPERTY( + qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged) + +public: + explicit BlobRect(QQuickItem* parent = nullptr); + ~BlobRect() override; + + qreal stiffness() const { return m_stiffness; } + + void setStiffness(qreal s) { + if (!qFuzzyCompare(m_stiffness, s)) { + m_stiffness = s; + emit stiffnessChanged(); + } + } + + qreal damping() const { return m_damping; } + + void setDamping(qreal d) { + if (!qFuzzyCompare(m_damping, d)) { + m_damping = d; + emit dampingChanged(); + } + } + + qreal deformScale() const { return m_deformScale; } + + void setDeformScale(qreal s) { + if (!qFuzzyCompare(m_deformScale, s)) { + m_deformScale = s; + emit deformScaleChanged(); + } + } + + QQmlListProperty exclude(); + + bool isExcluded(const BlobShape* other) const override; + void cornerRadii(float out[4]) const override; + + qreal topLeftRadius() const { return m_topLeftRadius; } + + void setTopLeftRadius(qreal r); + + qreal topRightRadius() const { return m_topRightRadius; } + + void setTopRightRadius(qreal r); + + qreal bottomLeftRadius() const { return m_bottomLeftRadius; } + + void setBottomLeftRadius(qreal r); + + qreal bottomRightRadius() const { return m_bottomRightRadius; } + + void setBottomRightRadius(qreal r); + +signals: + void stiffnessChanged(); + void dampingChanged(); + void deformScaleChanged(); + void excludeChanged(); + void topLeftRadiusChanged(); + void topRightRadiusChanged(); + void bottomLeftRadiusChanged(); + void bottomRightRadiusChanged(); + +protected: + void updatePolish() override; + void updatePhysics() override; + +private: + void checkAtRest(float speed); + + // Physics state + QPointF m_prevScenePos; + QElapsedTimer m_elapsed; + bool m_physicsActive = false; + bool m_hasPrevPos = false; + + // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, + // m11) Rest state is identity: m00=1, m01=0, m11=1 + float m_dm00 = 1.0f; + float m_dm01 = 0.0f; + float m_dm11 = 1.0f; + + // Spring velocities for each component + float m_dmVel00 = 0.0f; + float m_dmVel01 = 0.0f; + float m_dmVel11 = 0.0f; + + qreal m_stiffness = 200.0; + qreal m_damping = 16.0; + qreal m_deformScale = 0.0005; + + qreal m_topLeftRadius = -1; + qreal m_topRightRadius = -1; + qreal m_bottomLeftRadius = -1; + qreal m_bottomRightRadius = -1; + + QList> m_exclude; + + static void excludeAppend(QQmlListProperty* prop, BlobRect* rect); + static qsizetype excludeCount(QQmlListProperty* prop); + static BlobRect* excludeAt(QQmlListProperty* prop, qsizetype index); + static void excludeClear(QQmlListProperty* prop); + static void excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); + static void excludeRemoveLast(QQmlListProperty* prop); +}; diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp new file mode 100644 index 000000000..cfa11c44c --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.cpp @@ -0,0 +1,392 @@ +#include "blobshape.hpp" +#include "blobgroup.hpp" +#include "blobinvertedrect.hpp" + +#include +#include + +#include +#include + +static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { + // Bounding box of the deformed shape: |M * corners| + const float dm00 = dm(0, 0), dm01 = dm(0, 1); + const float dm10 = dm(1, 0), dm11 = dm(1, 1); + const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh; + const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh; + const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3)); + const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3)); + return std::max(extraX, extraY); +} + +static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) { + const float dx = std::abs(px - cx) - hw; + const float dy = std::abs(py - cy) - hh; + const float mdx = std::max(dx, 0.0f); + const float mdy = std::max(dy, 0.0f); + return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f); +} + +static float cpuSmoothstep(float edge0, float edge1, float x) { + const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); + return t * t * (3.0f - 2.0f * t); +} + +BlobShape::BlobShape(QQuickItem* parent) + : QQuickItem(parent) { + setFlag(ItemHasContents); +} + +void BlobShape::setGroup(BlobGroup* g) { + if (m_group == g) + return; + if (m_group && isComponentComplete()) + unregisterFromGroup(); + m_group = g; + if (m_group && isComponentComplete()) + registerWithGroup(); + emit groupChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::setRadius(qreal r) { + if (qFuzzyCompare(m_radius, r)) + return; + m_radius = r; + emit radiusChanged(); + if (m_group) + m_group->markDirty(); +} + +void BlobShape::componentComplete() { + QQuickItem::componentComplete(); + if (m_group) + registerWithGroup(); +} + +void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + updateCenteredDeformMatrix(); + if (m_group) { + // Accumulate sub-pixel drift so slow movements don't desync the shader + m_pendingDx += static_cast(newGeometry.x() - oldGeometry.x()); + m_pendingDy += static_cast(newGeometry.y() - oldGeometry.y()); + const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); + const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); + if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) { + m_pendingDx = 0; + m_pendingDy = 0; + m_group->markShapeDirty(this); + } + } +} + +void BlobShape::updateCenteredDeformMatrix() { + const auto cx = static_cast(width()) * 0.5f; + const auto cy = static_cast(height()) * 0.5f; + QMatrix4x4 result; + result.translate(cx, cy); + result *= m_deformMatrix; + result.translate(-cx, -cy); + if (m_centeredDeformMatrix != result) { + m_centeredDeformMatrix = result; + emit deformMatrixChanged(); + } +} + +void BlobShape::cornerRadii(float out[4]) const { + const auto maxR = static_cast(std::min(width(), height())) * 0.5f; + const auto r = std::min(static_cast(m_radius), maxR); + out[0] = r; + out[1] = r; + out[2] = r; + out[3] = r; +} + +void BlobShape::registerWithGroup() { + if (m_group) + m_group->addShape(this); +} + +void BlobShape::unregisterFromGroup() { + if (m_group) + m_group->removeShape(this); +} + +void BlobShape::updatePolish() { + if (!m_group) + return; + + // Ensure all shapes have up-to-date physics (only once per frame) + m_group->ensurePhysicsUpdated(); + + const QPointF scenePos = mapToScene(QPointF(0, 0)); + const float pad = static_cast(m_group->smoothing()); + + if (isInvertedRect()) { + m_cachedPaddedX = static_cast(scenePos.x()); + m_cachedPaddedY = static_cast(scenePos.y()); + m_cachedPaddedW = static_cast(width()); + m_cachedPaddedH = static_cast(height()); + m_localPaddedRect = QRectF(0, 0, width(), height()); + } else { + const float hw = static_cast(width()) * 0.5f; + const float hh = static_cast(height()) * 0.5f; + const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh); + + m_cachedPaddedX = static_cast(scenePos.x()) - totalPad; + m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; + m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; + m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; + m_localPaddedRect = QRectF(static_cast(-totalPad), static_cast(-totalPad), + width() + 2.0 * static_cast(totalPad), height() + 2.0 * static_cast(totalPad)); + } + + // Filter nearby normal rects + m_cachedRects.clear(); + m_cachedMyIndex = -2; + const QRectF myPadded(static_cast(m_cachedPaddedX), static_cast(m_cachedPaddedY), + static_cast(m_cachedPaddedW), static_cast(m_cachedPaddedH)); + + // Track shape pointers parallel to m_cachedRects for pairwise exclusion lookups + QVector rectShapes; + rectShapes.reserve(m_group->shapes().size()); + + for (BlobShape* other : m_group->shapes()) { + if (other->isInvertedRect()) + continue; + + // Skip zero-size rects + if (other->width() <= 0 || other->height() <= 0) + continue; + + if (isExcluded(other)) + continue; + + const QPointF otherScene = other->mapToScene(QPointF(0, 0)); + + bool include = false; + if (isInvertedRect()) { + include = true; + } else { + const float otherHW = static_cast(other->width()) * 0.5f; + const float otherHH = static_cast(other->height()) * 0.5f; + const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); + const QRectF otherPadded(otherScene.x() - static_cast(otherPad), + otherScene.y() - static_cast(otherPad), other->width() + 2.0 * static_cast(otherPad), + other->height() + 2.0 * static_cast(otherPad)); + include = myPadded.intersects(otherPadded); + } + + if (include) { + if (other == this) + m_cachedMyIndex = static_cast(m_cachedRects.size()); + + const QMatrix4x4& dm = other->m_deformMatrix; + const float a = dm(0, 0), b = dm(1, 0); + const float c = dm(0, 1), d = dm(1, 1); + + BlobRectData r; + r.cx = static_cast(otherScene.x() + other->width() / 2.0); + r.cy = static_cast(otherScene.y() + other->height() / 2.0); + r.hw = static_cast(other->width() / 2.0); + r.hh = static_cast(other->height() / 2.0); + other->cornerRadii(r.radius); + r.offsetX = dm(0, 3); + r.offsetY = dm(1, 3); + + // Pre-compute inverse deformation matrix + const float det = a * d - c * b; + const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; + r.invDeform[0] = d * invDet; + r.invDeform[1] = -b * invDet; + r.invDeform[2] = -c * invDet; + r.invDeform[3] = a * invDet; + + // Pre-compute minimum eigenvalue (avoids per-pixel sqrt) + const float halfTr = 0.5f * (a + d); + const float halfDiff = 0.5f * (a - d); + r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c); + + // Pre-compute screen-space AABB half-extents + r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh; + r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; + + m_cachedRects.append(r); + rectShapes.append(other); + } + } + + if (isInvertedRect()) + m_cachedMyIndex = -1; + + // Compute pairwise exclude masks. Bit j in entry i is set iff rect i excludes rect j + // or rect j excludes rect i. The shader uses this to avoid smin between excluded pairs. + const auto cachedCount = m_cachedRects.size(); + for (qsizetype i = 0; i < cachedCount; ++i) { + int mask = 0; + BlobShape* si = rectShapes[i]; + for (qsizetype j = 0; j < cachedCount; ++j) { + if (j == i) + continue; + BlobShape* sj = rectShapes[j]; + if (si->isExcluded(sj) || sj->isExcluded(si)) + mask |= (1 << j); + } + m_cachedRects[i].excludeMask = mask; + } + + // Cache inverted rect data + m_cachedHasInverted = false; + m_cachedInvertedRadius = 0; + memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); + memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); + + auto* inv = m_group->invertedRect(); + if (inv) { + const QPointF invScene = inv->mapToScene(QPointF(0, 0)); + const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); + const float outerCY = static_cast(invScene.y() + inv->height() / 2.0); + const float outerHW = static_cast(inv->width() / 2.0); + const float outerHH = static_cast(inv->height() / 2.0); + + const float innerCX = outerCX + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); + const float innerCY = outerCY + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); + const float innerHW = outerHW - static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); + const float innerHH = outerHH - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); + + // Check if this rect is near the border (within 2x smoothing of inner edge) + bool nearBorder = isInvertedRect(); + if (!nearBorder) { + const float margin = pad * 2.0f; + const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f; + const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f; + const float myHW = m_cachedPaddedW * 0.5f; + const float myHH = m_cachedPaddedH * 0.5f; + // Near border if any edge of padded rect is within margin of inner edge + nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) || + (myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin); + } + + if (nearBorder) { + m_cachedHasInverted = true; + m_cachedInvertedRadius = static_cast(inv->radius()); + + m_cachedInvertedOuter[0] = outerCX; + m_cachedInvertedOuter[1] = outerCY; + m_cachedInvertedOuter[2] = outerHW; + m_cachedInvertedOuter[3] = outerHH; + + m_cachedInvertedInner[0] = innerCX; + m_cachedInvertedInner[1] = innerCY; + m_cachedInvertedInner[2] = innerHW; + m_cachedInvertedInner[3] = innerHH; + } + } + + // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) + const float smoothFactor = pad; + constexpr float minR = 2.0f; + const auto rectCount = m_cachedRects.size(); + for (qsizetype i = 0; i < rectCount; ++i) { + auto& ri = m_cachedRects[i]; + const int riExcludeMask = ri.excludeMask; + float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; + + const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; + const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; + const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; + const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; + + for (qsizetype j = 0; j < rectCount; ++j) { + if (j == i) + continue; + if (riExcludeMask & (1 << j)) + continue; + const auto& rj = m_cachedRects[j]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); + } + + if (m_cachedHasInverted) { + const float icx = m_cachedInvertedInner[0]; + const float icy = m_cachedInvertedInner[1]; + const float ihw = m_cachedInvertedInner[2]; + const float ihh = m_cachedInvertedInner[3]; + fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); + fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); + fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); + fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); + } + + // Combine base radii with fill factors into effective per-corner radii + ri.radius[0] = std::max(ri.radius[0] * fTr, minR); + ri.radius[1] = std::max(ri.radius[1] * fBr, minR); + ri.radius[2] = std::max(ri.radius[2] * fBl, minR); + ri.radius[3] = std::max(ri.radius[3] * fTl, minR); + } +} + +QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { + if (!m_group) { + delete oldNode; + return nullptr; + } + + auto* node = static_cast(oldNode); + if (!node) { + node = new QSGGeometryNode; + + auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); + geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); + node->setGeometry(geometry); + node->setFlag(QSGNode::OwnsGeometry); + + auto* material = new BlobMaterial; + material->setFlag(QSGMaterial::Blending); + node->setMaterial(material); + node->setFlag(QSGNode::OwnsMaterial); + } + + // Update geometry + auto* geometry = node->geometry(); + auto* v = geometry->vertexDataAsTexturedPoint2D(); + + const float x0 = static_cast(m_localPaddedRect.x()); + const float y0 = static_cast(m_localPaddedRect.y()); + const float x1 = x0 + static_cast(m_localPaddedRect.width()); + const float y1 = y0 + static_cast(m_localPaddedRect.height()); + + v[0].set(x0, y0, 0.0f, 0.0f); + v[1].set(x1, y0, 1.0f, 0.0f); + v[2].set(x0, y1, 0.0f, 1.0f); + v[3].set(x1, y1, 1.0f, 1.0f); + + node->markDirty(QSGNode::DirtyGeometry); + + // Update material + auto* material = static_cast(node->material()); + material->m_paddedX = m_cachedPaddedX; + material->m_paddedY = m_cachedPaddedY; + material->m_paddedW = m_cachedPaddedW; + material->m_paddedH = m_cachedPaddedH; + material->m_smoothFactor = static_cast(m_group->smoothing()); + material->m_myIndex = m_cachedMyIndex; + material->m_color = m_group->color(); + material->m_hasInverted = m_cachedHasInverted ? 1 : 0; + material->m_invertedRadius = m_cachedInvertedRadius; + memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); + memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); + + const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); + material->m_rectCount = count; + for (int i = 0; i < count; ++i) + material->m_rects[i] = m_cachedRects[i]; + + node->markDirty(QSGNode::DirtyMaterial); + + return node; +} diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp new file mode 100644 index 000000000..c05a40d86 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/blobshape.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include "blobmaterial.hpp" + +#include +#include +#include + +class BlobGroup; + +class BlobShape : public QQuickItem { + Q_OBJECT + Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) + Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) + Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) + Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged) + + friend class BlobGroup; + +public: + explicit BlobShape(QQuickItem* parent = nullptr); + ~BlobShape() override = default; + + BlobGroup* group() const { return m_group; } + + void setGroup(BlobGroup* g); + + qreal radius() const { return m_radius; } + + void setRadius(qreal r); + + QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } + + QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } + +signals: + void groupChanged(); + void radiusChanged(); + void deformMatrixChanged(); + void rawDeformMatrixChanged(); + +protected: + void componentComplete() override; + void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; + void updatePolish() override; + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; + + virtual bool isInvertedRect() const { return false; } + + virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } + + virtual void cornerRadii(float out[4]) const; + + virtual void updatePhysics() {} + + virtual void registerWithGroup(); + virtual void unregisterFromGroup(); + void updateCenteredDeformMatrix(); + + BlobGroup* m_group = nullptr; + qreal m_radius = 0; + QMatrix4x4 m_deformMatrix; // identity by default + QMatrix4x4 m_centeredDeformMatrix; + + // Cached data from updatePolish + float m_cachedPaddedX = 0; + float m_cachedPaddedY = 0; + float m_cachedPaddedW = 0; + float m_cachedPaddedH = 0; + QRectF m_localPaddedRect; + QVector m_cachedRects; + int m_cachedMyIndex = -2; + float m_pendingDx = 0; + float m_pendingDy = 0; + bool m_cachedHasInverted = false; + float m_cachedInvertedRadius = 0; + float m_cachedInvertedOuter[4] = {}; + float m_cachedInvertedInner[4] = {}; +}; diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag new file mode 100644 index 000000000..c002fd259 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag @@ -0,0 +1,251 @@ +#version 440 + +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { + vec2 d = abs(p - center) - halfSize + vec2(radius); + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius; +} + +float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { + // r = (topRight, bottomRight, bottomLeft, topLeft) + p -= center; + r.xy = (p.x > 0.0) ? r.xy : r.wz; + r.x = (p.y > 0.0) ? r.y : r.x; + vec2 q = abs(p) - halfSize + r.x; + return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; +} + +float sdBox(vec2 p, vec2 center, vec2 halfSize) { + vec2 d = abs(p - center) - halfSize; + return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); +} + +float smin(float a, float b, float k) { + // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) + float h = max(k - abs(a - b), 0.0) / k; + return min(a, b) - h * h * h * k * (1.0/6.0); +} + +float smax(float a, float b, float k) { + float h = max(k - abs(a - b), 0.0) / k; + return max(a, b) + h * h * h * k * (1.0/6.0); +} + +float smaxSharpA(float a, float b, float k) { + // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). + // Used for the frame outer edge so it always fills to the edges. + float h = max(k - abs(a - b), 0.0) / k; + float blend = h * h * h * k * (1.0/6.0); + blend *= smoothstep(0.0, k * 0.5, -a); + return max(a, b) + blend; +} + +void main() { + vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); + + // Phase 1: compute per-rect SDF, track owner. We can't smin yet because excluded + // pairs need to skip the smooth blend, which requires pairwise pass below. + float dArr[16]; + int owner = -2; + float minDist = 1e10; + + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; // cx, cy, hw, hh + vec4 props = rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig + vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix + vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 + vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) + + // Offset center for asymmetric deformation + vec2 center = rect.xy + props.yz; + + // AABB early-out: skip rects far from this pixel + vec2 extent = sh.xy + vec2(smoothFactor * 1.5); + if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) { + dArr[i] = 1e10; + continue; + } + + // Apply pre-computed inverse deformation to the evaluation point + mat2 invDeform = mat2(invDm.xy, invDm.zw); + vec2 transformedPixel = center + invDeform * (pixel - center); + + // Use pre-computed effective per-corner radii + float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); + + // Use pre-computed minimum eigenvalue for SDF correction + d *= max(props.w, 0.01); + + // Scale SDF on the axis facing a nearby border to narrow the smin blend zone + // in that direction only, without reducing k (which would cause sharp edges). + if (hasInverted != 0) { + vec2 screenHalf = sh.xy; + + float distY0 = (center.y + screenHalf.y) - (invertedInner.y - invertedInner.w); + float distY1 = (invertedInner.y + invertedInner.w) - (center.y - screenHalf.y); + float distX0 = (center.x + screenHalf.x) - (invertedInner.x - invertedInner.z); + float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); + + // 0 = far from border, 1 = at border (max compression) + float yProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distY0), + smoothstep(0.0, smoothFactor, distY1) + ); + float xProx = 1.0 - min( + smoothstep(0.0, smoothFactor, distX0), + smoothstep(0.0, smoothFactor, distX1) + ); + + // Smooth axis weights: gradient-based at corners, face-based inside. + vec2 q = abs(pixel - center) - screenHalf; + vec2 qp = max(q, vec2(0.0)); + float cornerLen = length(qp); + + // Gradient direction in corner region (smooth 90-degree rotation) + float gradX = qp.x / max(cornerLen, 0.001); + float gradY = qp.y / max(cornerLen, 0.001); + + // Smooth face weights for inside/edge (no hard branch) + float faceY = smoothstep(-4.0, 4.0, q.y - q.x); + float faceX = 1.0 - faceY; + + // Blend: gradient in corner region, face-based inside + float t = smoothstep(0.0, 2.0, cornerLen); + float xWeight = mix(faceX, gradX, t); + float yWeight = mix(faceY, gradY, t); + + float boost = 3.0; + float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost; + d *= scale; + } + + dArr[i] = d; + if (d < smoothFactor && d < minDist) { + minDist = d; + owner = i; + } + } + + // Phase 2: hard-min baseline over all rects. + float mergedSdf = 1e10; + for (int i = 0; i < rectCount; i++) { + mergedSdf = min(mergedSdf, dArr[i]); + } + + // Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin <= min, + // so taking the min over all non-excluded pair smins gives the smoothly-merged SDF. + for (int i = 0; i < rectCount; i++) { + if (dArr[i] >= 1e9) + continue; + int excludeMask = floatBitsToInt(rectData[i * 5 + 1].x); + for (int j = i + 1; j < rectCount; j++) { + if (dArr[j] >= 1e9) + continue; + if ((excludeMask & (1 << j)) != 0) + continue; + // smin only deviates from min within smoothFactor + if (abs(dArr[i] - dArr[j]) >= smoothFactor) + continue; + mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); + } + } + + if (hasInverted != 0) { + float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; + float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); + + // Border sinks: track the opposite rect edge, clamped to border thickness + float innerTop = invertedInner.y - invertedInner.w; + float innerBot = invertedInner.y + invertedInner.w; + float innerLeft = invertedInner.x - invertedInner.z; + float innerRight = invertedInner.x + invertedInner.z; + float outerTop = invertedOuter.y - invertedOuter.w; + float outerBot = invertedOuter.y + invertedOuter.w; + float outerLeft = invertedOuter.x - invertedOuter.z; + float outerRight = invertedOuter.x + invertedOuter.z; + + float sinkValue = 0.0; + for (int i = 0; i < rectCount; i++) { + vec4 rect = rectData[i * 5]; + vec4 sinkProps = rectData[i * 5 + 1]; + vec2 sinkSh = rectData[i * 5 + 3].xy; + + // Screen-space center (with offset) and pre-computed AABB half-extents + vec2 ctr = rect.xy + sinkProps.yz; + + // Delay sink to absorb smin blend depth (cubic smin max = k/6) + float preOff = smoothFactor * (1.0/6.0); + + // Top border: track rect's BOTTOM edge, only within border thickness + float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); + + // Bottom border: track rect's TOP edge + float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot); + + // Left border: track rect's RIGHT edge + float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft); + + // Right border: track rect's LEFT edge + float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight); + + // Lateral distance from pixel to rect's extent along each edge + float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); + float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); + + // Perpendicular proximity: full strength in border, fade inside inner area + float topZone = 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); + float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); + float leftZone = 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); + float rightZone = smoothstep(innerRight - smoothFactor, innerRight, pixel.x); + + float s = smoothFactor * 2.0; + float sink = max( + max(topPen * smoothstep(s, 0.0, hLat) * topZone, + botPen * smoothstep(s, 0.0, hLat) * botZone), + max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, + rightPen * smoothstep(s, 0.0, vLat) * rightZone) + ); + sinkValue = max(sinkValue, sink); + } + + dInner -= sinkValue; + + float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); + + mergedSdf = smin(mergedSdf, dFrame, smoothFactor); + if (dFrame < minDist) { + owner = -1; + } + } + + // Each renderer only outputs pixels it owns, but allow rendering + // blend zones to prevent gaps (mergedSdf < smoothFactor means in blend) + // myIndex == -1: inverted rect renders border-owned pixels + // myIndex >= 0: individual rect renders its owned pixels + if (owner != myIndex && mergedSdf > smoothFactor) + discard; + + float fw = fwidth(mergedSdf); + float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); + fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; +} diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.vert b/plugin/src/Caelestia/Blobs/shaders/blob.vert new file mode 100644 index 000000000..e71d81042 --- /dev/null +++ b/plugin/src/Caelestia/Blobs/shaders/blob.vert @@ -0,0 +1,29 @@ +#version 440 + +layout(location = 0) in vec4 qt_VertexPosition; +layout(location = 1) in vec2 qt_VertexTexCoord; + +layout(location = 0) out vec2 qt_TexCoord0; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + float paddedX; + float paddedY; + float paddedW; + float paddedH; + float smoothFactor; + int rectCount; + int myIndex; + vec4 color; + int hasInverted; + float invertedRadius; + vec4 invertedOuter; + vec4 invertedInner; + vec4 rectData[80]; +}; + +void main() { + gl_Position = qt_Matrix * qt_VertexPosition; + qt_TexCoord0 = qt_VertexTexCoord; +} diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt index e4a020124..6e73f4fe3 100644 --- a/plugin/src/Caelestia/CMakeLists.txt +++ b/plugin/src/Caelestia/CMakeLists.txt @@ -1,20 +1,24 @@ -find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) +find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) find_package(PkgConfig REQUIRED) pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) -pkg_check_modules(Cava IMPORTED_TARGET libcava REQUIRED) +pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) +if(NOT Cava_FOUND) + pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) +endif() set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") qt_standard_project_setup(REQUIRES 6.9) function(qml_module arg_TARGET) - cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES") + cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;LIBRARIES") qt_add_qml_module(${arg_TARGET} URI ${arg_URI} VERSION ${VERSION} SOURCES ${arg_SOURCES} + QML_FILES ${arg_QML_FILES} ) qt_query_qml_module(${arg_TARGET} @@ -34,9 +38,24 @@ function(qml_module arg_TARGET) install(FILES "${module_qmldir}" DESTINATION "${module_dir}") install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") - target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES}) + target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) endfunction() +add_library(caelestia-pch INTERFACE) +target_precompile_headers(caelestia-pch INTERFACE + + + + + + + + + + + +) + qml_module(caelestia URI Caelestia SOURCES @@ -54,6 +73,10 @@ qml_module(caelestia PkgConfig::Qalculate ) +add_subdirectory(Components) +add_subdirectory(Config) add_subdirectory(Internal) add_subdirectory(Models) add_subdirectory(Services) +add_subdirectory(Blobs) +add_subdirectory(Images) diff --git a/plugin/src/Caelestia/Components/CMakeLists.txt b/plugin/src/Caelestia/Components/CMakeLists.txt new file mode 100644 index 000000000..f880d3183 --- /dev/null +++ b/plugin/src/Caelestia/Components/CMakeLists.txt @@ -0,0 +1,7 @@ +qml_module(caelestia-components + URI Caelestia.Components + SOURCES + lazylistview.hpp lazylistview.cpp + LIBRARIES + Qt::Quick +) diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp new file mode 100644 index 000000000..36b49301f --- /dev/null +++ b/plugin/src/Caelestia/Components/lazylistview.cpp @@ -0,0 +1,1108 @@ +#include "lazylistview.hpp" + +#include +#include +#include + +namespace { + +constexpr int ASYNC_BATCH_CREATE = 2; +constexpr int ASYNC_BATCH_DESTROY = 4; + +} // namespace + +namespace caelestia::components { + +// --- LazyListViewAttached --- + +LazyListViewAttached::LazyListViewAttached(QObject* parent) + : QObject(parent) {} + +qreal LazyListViewAttached::preferredHeight() const { + return m_preferredHeight; +} + +void LazyListViewAttached::setPreferredHeight(qreal height) { + if (qFuzzyCompare(m_preferredHeight + 1.0, height + 1.0)) + return; + m_preferredHeight = height; + emit preferredHeightChanged(); +} + +qreal LazyListViewAttached::visibleHeight() const { + return m_visibleHeight; +} + +void LazyListViewAttached::setVisibleHeight(qreal height) { + if (qFuzzyCompare(m_visibleHeight + 1.0, height + 1.0)) + return; + m_visibleHeight = height; + emit visibleHeightChanged(); +} + +bool LazyListViewAttached::ready() const { + return m_ready; +} + +void LazyListViewAttached::setReady(bool ready) { + if (m_ready == ready) + return; + m_ready = ready; + emit readyChanged(); +} + +bool LazyListViewAttached::adding() const { + return m_adding; +} + +void LazyListViewAttached::setAdding(bool adding) { + if (m_adding == adding) + return; + m_adding = adding; + emit addingChanged(); +} + +bool LazyListViewAttached::removing() const { + return m_removing; +} + +void LazyListViewAttached::setRemoving(bool removing) { + if (m_removing == removing) + return; + m_removing = removing; + emit removingChanged(); +} + +bool LazyListViewAttached::trackViewport() const { + return m_trackViewport; +} + +void LazyListViewAttached::setTrackViewport(bool track) { + if (m_trackViewport == track) + return; + m_trackViewport = track; + emit trackViewportChanged(); +} + +// --- LazyListView --- + +LazyListView::LazyListView(QQuickItem* parent) + : QQuickItem(parent) { + setFlag(ItemHasContents, false); +} + +LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) { + return new LazyListViewAttached(object); +} + +LazyListView::~LazyListView() { + for (auto& entry : m_delegates) + destroyDelegate(entry); + for (auto& entry : m_dyingDelegates) + destroyDelegate(entry); +} + +// --- Model & Delegate --- + +QAbstractItemModel* LazyListView::model() const { + return m_model; +} + +void LazyListView::setModel(QAbstractItemModel* model) { + if (m_model == model) + return; + + if (m_model) + disconnectModel(); + + m_model = model; + + if (m_model) + connectModel(); + + resetContent(); + emit modelChanged(); +} + +QQmlComponent* LazyListView::delegate() const { + return m_delegate; +} + +void LazyListView::setDelegate(QQmlComponent* delegate) { + if (m_delegate == delegate) + return; + + m_delegate = delegate; + resetContent(); + emit delegateChanged(); +} + +// --- Layout --- + +qreal LazyListView::spacing() const { + return m_spacing; +} + +void LazyListView::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing, spacing)) + return; + m_spacing = spacing; + emit spacingChanged(); + polish(); +} + +qreal LazyListView::contentHeight() const { + return m_contentHeight; +} + +qreal LazyListView::layoutHeight() const { + return m_layoutHeight; +} + +qreal LazyListView::contentY() const { + return m_contentY; +} + +void LazyListView::setContentY(qreal contentY) { + if (qFuzzyCompare(m_contentY, contentY)) + return; + m_contentY = contentY; + emit contentYChanged(); + polish(); +} + +// --- Viewport --- + +QRectF LazyListView::viewport() const { + return m_viewport; +} + +void LazyListView::setViewport(const QRectF& viewport) { + if (m_viewport == viewport) + return; + m_viewport = viewport; + emit viewportChanged(); + if (m_useCustomViewport) + polish(); +} + +bool LazyListView::useCustomViewport() const { + return m_useCustomViewport; +} + +void LazyListView::setUseCustomViewport(bool use) { + if (m_useCustomViewport == use) + return; + m_useCustomViewport = use; + emit useCustomViewportChanged(); + polish(); +} + +qreal LazyListView::cacheBuffer() const { + return m_cacheBuffer; +} + +void LazyListView::setCacheBuffer(qreal buffer) { + if (qFuzzyCompare(m_cacheBuffer, buffer)) + return; + m_cacheBuffer = buffer; + emit cacheBufferChanged(); + polish(); +} + +// --- Sizing --- + +qreal LazyListView::estimatedHeight() const { + return m_estimatedHeight; +} + +void LazyListView::setEstimatedHeight(qreal height) { + if (qFuzzyCompare(m_estimatedHeight, height)) + return; + m_estimatedHeight = height; + emit estimatedHeightChanged(); + polish(); +} + +bool LazyListView::asynchronous() const { + return m_asynchronous; +} + +void LazyListView::setAsynchronous(bool async) { + if (m_asynchronous == async) + return; + m_asynchronous = async; + emit asynchronousChanged(); +} + +qreal LazyListView::effectiveEstimatedHeight() const { + if (m_estimatedHeight >= 0) + return m_estimatedHeight; + if (m_knownHeightCount > 0) + return m_knownHeightSum / m_knownHeightCount; + return 40; +} + +void LazyListView::trackHeight(qreal height) { + m_knownHeightSum += height; + ++m_knownHeightCount; +} + +void LazyListView::untrackHeight(qreal height) { + m_knownHeightSum -= height; + --m_knownHeightCount; +} + +qreal LazyListView::delegateHeight(QQuickItem* item) { + if (!item) + return 0; + + auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (attached && attached->preferredHeight() >= 0) + return attached->preferredHeight(); + + return item->implicitHeight(); +} + +qreal LazyListView::delegateVisibleHeight(QQuickItem* item) { + if (!item) + return 0; + + auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (attached) { + if (attached->visibleHeight() >= 0) + return attached->visibleHeight(); + if (attached->preferredHeight() >= 0) + return attached->preferredHeight(); + } + + return item->implicitHeight(); +} + +bool LazyListView::isDelegateReady(QQuickItem* item) { + if (!item) + return false; + auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); + return !att || att->ready(); +} + +// --- Animation Durations --- + +int LazyListView::removeDuration() const { + return m_removeDuration; +} + +void LazyListView::setRemoveDuration(int duration) { + if (m_removeDuration == duration) + return; + m_removeDuration = duration; + emit removeDurationChanged(); +} + +int LazyListView::readyDelay() const { + return m_readyDelay; +} + +void LazyListView::setReadyDelay(int delay) { + if (m_readyDelay == delay) + return; + m_readyDelay = delay; + emit readyDelayChanged(); +} + +// --- State --- + +int LazyListView::count() const { + return m_model ? m_model->rowCount() : 0; +} + +// --- QQuickItem Overrides --- + +void LazyListView::componentComplete() { + QQuickItem::componentComplete(); + m_componentComplete = true; + resetContent(); +} + +void LazyListView::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { + QQuickItem::geometryChange(newGeometry, oldGeometry); + + if (!m_componentComplete) + return; + + if (!qFuzzyCompare(newGeometry.width(), oldGeometry.width())) { + for (auto& entry : m_delegates) { + if (entry.item) + entry.item->setWidth(newGeometry.width()); + } + } + + polish(); +} + +void LazyListView::updatePolish() { + if (!m_componentComplete || !m_model || !m_delegate) + return; + + // Flush pending inserts — make items visible and clear the adding flag + // so enter animations begin. When readyDelay > 0 the entire insert is + // deferred so delegates have time to lay out before appearing. + for (auto& entry : m_delegates) { + if (!entry.pendingInsert || !entry.item) + continue; + + if (m_readyDelay > 0) { + if (!entry.readyDelayStarted) { + entry.readyDelayStarted = true; + auto* item = entry.item; + QTimer::singleShot(m_readyDelay, this, [this, item] { + auto indexIt = m_itemToIndex.find(item); + if (indexIt == m_itemToIndex.end()) + return; + const int idx = indexIt.value(); + auto it = m_delegates.find(idx); + if (it == m_delegates.end() || it->item != item || !it->pendingInsert) + return; + + it->pendingInsert = false; + it->readyDelayStarted = false; + + // Set initial y to visual position (based on current visible heights) + if (idx >= 0 && idx < static_cast(m_layout.size())) { + qreal visualY = 0; + bool hasVisItem = false; + for (int i = 0; i < static_cast(m_layout.size()); ++i) { + qreal h; + auto dit = m_delegates.find(i); + if (dit != m_delegates.end() && dit->item) + h = delegateVisibleHeight(dit->item); + else + h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); + if (h > 0) { + if (hasVisItem) + visualY += m_spacing; + hasVisItem = true; + } + if (i == idx) + break; + if (h > 0) + visualY += h; + } + item->setY(visualY - m_contentY); + } + + item->setVisible(true); + auto* att = + qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (att) { + att->setAdding(false); + att->setReady(true); + } + + // Animate from visual position to layout position + if (idx >= 0 && idx < static_cast(m_layout.size())) + item->setProperty("y", m_layout[idx].targetY - m_contentY); + + polish(); + }); + } + continue; + } + + entry.pendingInsert = false; + entry.item->setVisible(true); + auto* att = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); + if (att) { + att->setAdding(false); + att->setReady(true); + } + } + + relayout(); + syncDelegates(); + + // Clear isNew flags — the add animation only plays for items created + // during the same polish cycle as their model insertion, not for + // delegates created later when scrolling items into the viewport. + for (auto& record : m_layout) + record.isNew = false; + + // Position delegates — QML Behavior on y handles the animation + for (auto& entry : m_delegates) { + if (!entry.item || entry.pendingRemoval || entry.pendingInsert) + continue; + + const int idx = entry.modelIndex; + if (idx < 0 || idx >= static_cast(m_layout.size())) + continue; + + if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height)) + continue; + + // Use setProperty to go through the QML property system, + // which triggers Behaviors (setY bypasses them). + entry.item->setProperty("y", m_layout[idx].targetY - m_contentY); + } +} + +// --- Layout Engine --- + +void LazyListView::relayout() { + // Layout positioning uses preferredHeight (final/non-animated). + // Only add spacing between items with non-zero height. + qreal y = 0; + bool hasLayoutItem = false; + for (auto& record : m_layout) { + const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); + if (layoutH > 0) { + if (hasLayoutItem) + y += m_spacing; + hasLayoutItem = true; + record.targetY = y; + y += layoutH; + } else { + record.targetY = y; + } + } + + if (!qFuzzyCompare(m_layoutHeight + 1.0, y + 1.0)) { + m_layoutHeight = y; + emit layoutHeightChanged(); + } + + // Content height tracks actual visible heights so scrolling follows animations. + // Only add spacing between items with non-zero visible height. + qreal visY = 0; + bool hasVisItem = false; + for (int i = 0; i < static_cast(m_layout.size()); ++i) { + qreal h; + auto dit = m_delegates.find(i); + if (dit != m_delegates.end() && dit->item) + h = delegateVisibleHeight(dit->item); + else + h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); + if (h > 0) { + if (hasVisItem) + visY += m_spacing; + hasVisItem = true; + visY += h; + } + } + + // Account for dying delegates still visually present + for (const auto& dying : std::as_const(m_dyingDelegates)) { + if (!dying.item) + continue; + const qreal dyingH = delegateVisibleHeight(dying.item); + if (dyingH > 0) + visY = std::max(visY, dying.item->y() + dyingH); + } + + if (!qFuzzyCompare(m_contentHeight + 1.0, visY + 1.0)) { + m_contentHeight = visY; + emit contentHeightChanged(); + } +} + +QRectF LazyListView::effectiveViewport() const { + QRectF vp; + if (m_useCustomViewport) + vp = m_viewport; + else + vp = QRectF(0, m_contentY, width(), height()); + + // During Flickable overshoot the viewport can extend entirely beyond content bounds, + // causing all delegates to be culled. Clamp so it always overlaps [0, layoutHeight]. + // Only needed for the built-in viewport — custom viewports represent the actual + // visible area and may legitimately lie entirely outside the content. + if (!m_useCustomViewport && m_layoutHeight > 0) { + const qreal top = std::min(vp.y(), m_layoutHeight); + const qreal bottom = std::max(vp.y() + vp.height(), 0.0); + if (bottom > top) + vp = QRectF(vp.x(), top, vp.width(), bottom - top); + } + + vp.adjust(0, -m_cacheBuffer, 0, m_cacheBuffer); + + // Trim the cache-buffered viewport to [0, layoutHeight]. No items exist outside + // those bounds, so extending past them wastes budget and can cause edge thrashing + // when a large cache buffer reaches the opposite end of the content. + if (m_layoutHeight > 0) { + const qreal top = std::max(vp.y(), 0.0); + const qreal bottom = std::min(vp.y() + vp.height(), m_layoutHeight); + if (top < bottom) + vp = QRectF(vp.x(), top, vp.width(), bottom - top); + else + return {}; + } + + return vp; +} + +std::pair LazyListView::computeVisibleRange() const { + if (m_layout.isEmpty()) + return { -1, -1 }; + + const auto vp = effectiveViewport(); + if (vp.isEmpty()) + return { -1, -1 }; + + const qreal vpTop = vp.y(); + const qreal vpBottom = vp.y() + vp.height(); + + // Binary search for first visible item + int lo = 0; + int hi = static_cast(m_layout.size()) - 1; + int first = static_cast(m_layout.size()); + + while (lo <= hi) { + const int mid = lo + (hi - lo) / 2; + const auto& record = m_layout[mid]; + const qreal itemBottom = record.targetY + (record.heightKnown ? record.height : effectiveEstimatedHeight()); + + if (itemBottom >= vpTop) { + first = mid; + hi = mid - 1; + } else { + lo = mid + 1; + } + } + + if (first >= static_cast(m_layout.size())) + return { -1, -1 }; + + // Linear scan for last visible item + int last = first; + for (int i = first; i < static_cast(m_layout.size()); ++i) { + if (m_layout[i].targetY > vpBottom) + break; + last = i; + } + + return { first, last }; +} + +// --- Delegate Lifecycle --- + +void LazyListView::syncDelegates() { + const auto [first, last] = computeVisibleRange(); + + // Collect indices that should be alive + QSet visibleIndices; + if (first >= 0) { + for (int i = first; i <= last; ++i) + visibleIndices.insert(i); + } + + // Collect delegates to destroy — only if visually outside the viewport + const auto vp = effectiveViewport(); + QList toRemove; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + if (visibleIndices.contains(it.key())) + continue; + if (!it->item || vp.isEmpty()) { + toRemove.append(it.key()); + continue; + } + const qreal itemTop = it->item->y(); + const qreal itemBottom = itemTop + delegateVisibleHeight(it->item); + if (itemBottom < vp.top() || itemTop > vp.bottom()) + toRemove.append(it.key()); + } + + // Batch destroy + const int destroyBudget = m_asynchronous ? ASYNC_BATCH_DESTROY : static_cast(toRemove.size()); + QVector removedEntries; + removedEntries.reserve(std::min(destroyBudget, static_cast(toRemove.size()))); + int destroyed = 0; + for (int idx : toRemove) { + if (destroyed >= destroyBudget) + break; + auto entry = m_delegates.take(idx); + if (entry.item) + m_itemToIndex.remove(entry.item); + removedEntries.append(std::move(entry)); + ++destroyed; + } + for (auto& entry : removedEntries) + destroyDelegate(entry); + + // Collect indices to create + QList toCreate; + if (first >= 0) { + for (int i = first; i <= last; ++i) { + if (!m_delegates.contains(i)) + toCreate.append(i); + } + } + + // Batch create + const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast(toCreate.size()); + int created = 0; + for (int i : toCreate) { + if (created >= createBudget) + break; + + auto entry = createDelegate(i); + if (entry.item) { + // Height tracking and viewport compensation are deferred + // until the delegate signals ready via readyChanged. + entry.pendingInsert = true; + entry.item->setY(m_layout[i].targetY - m_contentY); + m_itemToIndex.insert(entry.item, i); + m_delegates.insert(i, std::move(entry)); + ++created; + } + } + + // Pending inserts need to become visible on the next frame, and + // async mode may have remaining create/destroy work. + if (created > 0 || (m_asynchronous && (destroyed < static_cast(toRemove.size()) || + created < static_cast(toCreate.size())))) + polish(); +} + +LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { + DelegateEntry entry; + entry.modelIndex = modelIndex; + + if (!m_delegate || !m_model) + return entry; + + const auto roleNames = m_model->roleNames(); + + // Use the delegate component's creation context for beginCreate + // so bound components (pragma ComponentBehavior: Bound) are accepted. + auto* compContext = m_delegate->creationContext(); + if (!compContext) + compContext = qmlContext(this); + if (!compContext) + return entry; + + auto* obj = m_delegate->beginCreate(compContext); + entry.item = qobject_cast(obj); + + if (!entry.item) { + if (obj) + m_delegate->completeCreate(); + delete obj; + return entry; + } + + // Build initial properties from model data + const auto index = m_model->index(modelIndex, 0); + QVariantMap initialProps; + bool hasModelData = false; + + for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { + const auto name = QString::fromUtf8(it.value()); + initialProps.insert(name, m_model->data(index, it.key())); + if (name == QStringLiteral("modelData")) + hasModelData = true; + } + initialProps.insert(QStringLiteral("index"), modelIndex); + + if (!hasModelData) { + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + initialProps.insert(QStringLiteral("modelData"), m_model->data(index, role)); + } + + m_delegate->setInitialProperties(entry.item, initialProps); + + entry.item->setParentItem(this); + entry.item->setWidth(width()); + + // Only set adding = true for genuinely new model items (not viewport entries). + // Cleared on the next frame in updatePolish when the item becomes visible. + if (modelIndex < static_cast(m_layout.size()) && m_layout[modelIndex].isNew) { + auto* addingAttached = + qobject_cast(qmlAttachedPropertiesObject(entry.item, true)); + if (addingAttached) + addingAttached->setAdding(true); + } + + m_delegate->completeCreate(); + + // Keep adding=true and hide — flushed on the next frame in updatePolish + entry.item->setVisible(false); + + // Height-change handler — uses m_itemToIndex for O(1) lookup. + // Ignored while the delegate is not yet ready. + auto onHeightChanged = [this, item = entry.item] { + if (!isDelegateReady(item)) + return; + auto indexIt = m_itemToIndex.find(item); + if (indexIt == m_itemToIndex.end()) + return; + const int idx = indexIt.value(); + auto delegateIt = m_delegates.find(idx); + if (delegateIt == m_delegates.end() || delegateIt->item != item) + return; + const qreal h = delegateHeight(item); + if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) { + const qreal oldH = m_layout[idx].height; + const bool wasKnown = m_layout[idx].heightKnown; + m_layout[idx].height = h; + m_layout[idx].heightKnown = true; + if (wasKnown) + untrackHeight(oldH); + trackHeight(h); + + // If this tracked item is above the viewport, emit a + // compensation delta so the consumer can adjust scroll. + if (wasKnown) { + auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (att && att->trackViewport()) { + const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; + if (m_layout[idx].targetY < vpTop) + emit viewportAdjustNeeded(h - oldH); + } + } + + if (!m_relayoutPending) { + m_relayoutPending = true; + QTimer::singleShot(0, this, [this] { + m_relayoutPending = false; + relayout(); + polish(); + }); + } + } + }; + + // Watch implicitHeight as fallback + connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); + + // Watch attached properties if the delegate uses them + auto* attached = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); + if (attached) { + connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); + connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] { + polish(); + }); + connect(attached, &LazyListViewAttached::readyChanged, this, [this, item = entry.item] { + auto indexIt = m_itemToIndex.find(item); + if (indexIt == m_itemToIndex.end()) + return; + const int idx = indexIt.value(); + if (idx >= static_cast(m_layout.size())) + return; + auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); + if (!att || !att->ready()) + return; + + const qreal h = delegateHeight(item); + const qreal oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight(); + if (m_layout[idx].heightKnown) + untrackHeight(m_layout[idx].height); + m_layout[idx].height = h; + m_layout[idx].heightKnown = true; + trackHeight(h); + + if (att->trackViewport() && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) { + const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; + if (m_layout[idx].targetY < vpTop) + emit viewportAdjustNeeded(h - oldLayoutH); + } + + polish(); + }); + } + + return entry; +} + +void LazyListView::destroyDelegate(DelegateEntry& entry) { + if (entry.item) { + entry.item->setParentItem(nullptr); + entry.item->setVisible(false); + entry.item->deleteLater(); + entry.item = nullptr; + } +} + +void LazyListView::updateDelegateData(DelegateEntry& entry) { + if (!m_model || !entry.item) + return; + + const auto roleNames = m_model->roleNames(); + const auto index = m_model->index(entry.modelIndex, 0); + bool hasModelData = false; + + for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { + const auto name = QString::fromUtf8(it.value()); + entry.item->setProperty(name.toUtf8().constData(), m_model->data(index, it.key())); + if (name == QStringLiteral("modelData")) + hasModelData = true; + } + + entry.item->setProperty("index", entry.modelIndex); + + if (!hasModelData) { + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + entry.item->setProperty("modelData", m_model->data(index, role)); + } +} + +// --- Model Connection --- + +void LazyListView::connectModel() { + if (!m_model) + return; + + m_modelConnections = { + connect(m_model, &QAbstractItemModel::rowsInserted, this, &LazyListView::onRowsInserted), + connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &LazyListView::onRowsAboutToBeRemoved), + connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LazyListView::onRowsRemoved), + connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), + connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), + connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), + connect(m_model, &QAbstractItemModel::layoutChanged, this, + [this] { + for (auto& entry : m_delegates) + updateDelegateData(entry); + polish(); + }), + connect(m_model, &QObject::destroyed, this, + [this] { + m_model = nullptr; + resetContent(); + emit modelChanged(); + }), + }; +} + +void LazyListView::disconnectModel() { + for (auto& conn : m_modelConnections) + disconnect(conn); + m_modelConnections.clear(); +} + +void LazyListView::resetContent() { + // Stop all animations and destroy all delegates + for (auto& entry : m_delegates) + destroyDelegate(entry); + m_delegates.clear(); + m_itemToIndex.clear(); + + for (auto& entry : m_dyingDelegates) + destroyDelegate(entry); + m_dyingDelegates.clear(); + + // Reset pending state + m_knownHeightSum = 0; + m_knownHeightCount = 0; + + // Rebuild layout from model + m_layout.clear(); + if (m_model && m_componentComplete) { + const int rows = m_model->rowCount(); + m_layout.resize(rows); + for (int i = 0; i < rows; ++i) { + m_layout[i].height = 0; + m_layout[i].heightKnown = false; + } + emit countChanged(); + } + + polish(); +} + +void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last) { + if (parent.isValid()) + return; + + const int insertCount = last - first + 1; + // Insert new layout records + m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false, true }); + + // Shift existing delegate indices + QHash shifted; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + int newIdx = it.key() >= first ? it.key() + insertCount : it.key(); + auto entry = std::move(it.value()); + entry.modelIndex = newIdx; + if (entry.item) { + entry.item->setProperty("index", newIdx); + m_itemToIndex[entry.item] = newIdx; + } + shifted.insert(newIdx, std::move(entry)); + } + m_delegates = std::move(shifted); + + emit countChanged(); + polish(); +} + +void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { + if (parent.isValid()) + return; + + for (int i = first; i <= last; ++i) { + if (!m_delegates.contains(i)) + continue; + + auto entry = m_delegates.take(i); + if (entry.item) + m_itemToIndex.remove(entry.item); + entry.pendingRemoval = true; + + // Never made visible — skip remove animation + if (entry.pendingInsert) { + destroyDelegate(entry); + continue; + } + + if (m_removeDuration > 0 && entry.item) { + auto* attached = + qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); + if (attached) + attached->setRemoving(true); + + // Schedule destruction after the remove animation duration + auto* item = entry.item; + QTimer::singleShot(m_removeDuration, this, [this, item] { + for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) { + if (it->item == item) { + destroyDelegate(*it); + m_dyingDelegates.erase(it); + return; + } + } + }); + m_dyingDelegates.append(std::move(entry)); + } else { + destroyDelegate(entry); + } + } +} + +void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) { + if (parent.isValid()) + return; + + const int removeCount = last - first + 1; + + // Untrack known heights being removed + for (int i = first; i <= last; ++i) { + if (m_layout[i].heightKnown) + untrackHeight(m_layout[i].height); + } + + // Remove layout records + m_layout.remove(first, removeCount); + + // Shift remaining delegate indices down + QHash shifted; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + int newIdx = it.key() > last ? it.key() - removeCount : it.key(); + auto entry = std::move(it.value()); + entry.modelIndex = newIdx; + if (entry.item) { + entry.item->setProperty("index", newIdx); + m_itemToIndex[entry.item] = newIdx; + } + shifted.insert(newIdx, std::move(entry)); + } + m_delegates = std::move(shifted); + + emit countChanged(); + polish(); +} + +void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { + if (parent.isValid() || destination.isValid()) + return; + + const int count = end - start + 1; + const int dest = row > start ? row - count : row; + + // Reorder layout records + QVector moved; + moved.reserve(count); + for (int i = start; i <= end; ++i) + moved.append(m_layout[i]); + m_layout.remove(start, count); + for (int i = 0; i < count; ++i) + m_layout.insert(dest + i, moved[i]); + + // Remap delegate indices to match new model order + QHash remapped; + for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { + int oldIdx = it.key(); + int newIdx = oldIdx; + + if (oldIdx >= start && oldIdx <= end) { + newIdx = dest + (oldIdx - start); + } else { + if (oldIdx > end) + newIdx -= count; + if (newIdx >= dest) + newIdx += count; + } + + auto entry = std::move(it.value()); + entry.modelIndex = newIdx; + if (entry.item) { + entry.item->setProperty("index", newIdx); + m_itemToIndex[entry.item] = newIdx; + } + remapped.insert(newIdx, std::move(entry)); + } + m_delegates = std::move(remapped); + + polish(); +} + +void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) { + Q_UNUSED(roles) + + if (topLeft.parent().isValid()) + return; + + for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { + if (m_delegates.contains(i)) + updateDelegateData(m_delegates[i]); + } +} + +void LazyListView::onModelReset() { + if (!m_model) { + resetContent(); + return; + } + + const int newRows = m_model->rowCount(); + const int oldRows = static_cast(m_layout.size()); + + // Check if the model data actually changed + if (newRows == oldRows) { + const auto roleNames = m_model->roleNames(); + const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); + bool changed = false; + + for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) { + if (!it->item || it.key() >= newRows) { + changed = true; + break; + } + const auto newData = m_model->data(m_model->index(it.key(), 0), role); + const auto oldData = it->item->property("modelData"); + if (newData != oldData) { + changed = true; + break; + } + } + + if (!changed) { + // Model content unchanged, just refresh delegate data + for (auto& entry : m_delegates) + updateDelegateData(entry); + return; + } + } + + resetContent(); +} + +} // namespace caelestia::components diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp new file mode 100644 index 000000000..e0746db2c --- /dev/null +++ b/plugin/src/Caelestia/Components/lazylistview.hpp @@ -0,0 +1,243 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace caelestia::components { + +class LazyListViewAttached : public QObject { + Q_OBJECT + + Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) + Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged) + Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) + Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) + Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) + Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged) + +public: + explicit LazyListViewAttached(QObject* parent = nullptr); + + [[nodiscard]] qreal preferredHeight() const; + void setPreferredHeight(qreal height); + + [[nodiscard]] qreal visibleHeight() const; + void setVisibleHeight(qreal height); + + [[nodiscard]] bool ready() const; + void setReady(bool ready); + + [[nodiscard]] bool adding() const; + void setAdding(bool adding); + + [[nodiscard]] bool removing() const; + void setRemoving(bool removing); + + [[nodiscard]] bool trackViewport() const; + void setTrackViewport(bool track); + +signals: + void preferredHeightChanged(); + void visibleHeightChanged(); + void readyChanged(); + void addingChanged(); + void removingChanged(); + void trackViewportChanged(); + +private: + qreal m_preferredHeight = -1; + qreal m_visibleHeight = -1; + bool m_ready = false; + bool m_adding = false; + bool m_removing = false; + bool m_trackViewport = false; +}; + +class LazyListView : public QQuickItem { + Q_OBJECT + QML_ELEMENT + QML_ATTACHED(LazyListViewAttached) + + // Model & Delegate + Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged) + Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) + + // Layout + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged) + Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged) + Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) + + // Viewport & Lazy Loading + Q_PROPERTY(QRectF viewport READ viewport WRITE setViewport NOTIFY viewportChanged) + Q_PROPERTY(bool useCustomViewport READ useCustomViewport WRITE setUseCustomViewport NOTIFY useCustomViewportChanged) + Q_PROPERTY(qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged) + + // Sizing + Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged) + + // Async + Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged) + + // Animation Durations + Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged) + Q_PROPERTY(int readyDelay READ readyDelay WRITE setReadyDelay NOTIFY readyDelayChanged) + + // State + Q_PROPERTY(int count READ count NOTIFY countChanged) + +public: + explicit LazyListView(QQuickItem* parent = nullptr); + ~LazyListView() override; + + static LazyListViewAttached* qmlAttachedProperties(QObject* object); + + // Model & Delegate + [[nodiscard]] QAbstractItemModel* model() const; + void setModel(QAbstractItemModel* model); + + [[nodiscard]] QQmlComponent* delegate() const; + void setDelegate(QQmlComponent* delegate); + + // Layout + [[nodiscard]] qreal spacing() const; + void setSpacing(qreal spacing); + + [[nodiscard]] qreal contentHeight() const; + [[nodiscard]] qreal layoutHeight() const; + + [[nodiscard]] qreal contentY() const; + void setContentY(qreal contentY); + + // Viewport + [[nodiscard]] QRectF viewport() const; + void setViewport(const QRectF& viewport); + + [[nodiscard]] bool useCustomViewport() const; + void setUseCustomViewport(bool use); + + [[nodiscard]] qreal cacheBuffer() const; + void setCacheBuffer(qreal buffer); + + // Sizing + [[nodiscard]] qreal estimatedHeight() const; + void setEstimatedHeight(qreal height); + + // Async + [[nodiscard]] bool asynchronous() const; + void setAsynchronous(bool async); + + // Animation Durations + [[nodiscard]] int removeDuration() const; + void setRemoveDuration(int duration); + + [[nodiscard]] int readyDelay() const; + void setReadyDelay(int delay); + + // State + [[nodiscard]] int count() const; +signals: + void modelChanged(); + void delegateChanged(); + void spacingChanged(); + void contentHeightChanged(); + void layoutHeightChanged(); + void contentYChanged(); + void viewportChanged(); + void useCustomViewportChanged(); + void cacheBufferChanged(); + void estimatedHeightChanged(); + void asynchronousChanged(); + void removeDurationChanged(); + void readyDelayChanged(); + void countChanged(); + void viewportAdjustNeeded(qreal delta); + +protected: + void componentComplete() override; + void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; + void updatePolish() override; + +private: + struct ItemRecord { + qreal targetY = 0; + qreal height = 0; + bool heightKnown = false; + bool isNew = false; + }; + + struct DelegateEntry { + int modelIndex = -1; + QQuickItem* item = nullptr; + bool pendingRemoval = false; + bool pendingInsert = false; + bool readyDelayStarted = false; + }; + + // Layout + void relayout(); + [[nodiscard]] std::pair computeVisibleRange() const; + [[nodiscard]] QRectF effectiveViewport() const; + [[nodiscard]] qreal effectiveEstimatedHeight() const; + [[nodiscard]] static qreal delegateHeight(QQuickItem* item); + [[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item); + [[nodiscard]] static bool isDelegateReady(QQuickItem* item); + void trackHeight(qreal height); + void untrackHeight(qreal height); + + // Delegate lifecycle + void syncDelegates(); + DelegateEntry createDelegate(int modelIndex); + void destroyDelegate(DelegateEntry& entry); + void updateDelegateData(DelegateEntry& entry); + + // Model connection + void connectModel(); + void disconnectModel(); + void resetContent(); + void onRowsInserted(const QModelIndex& parent, int first, int last); + void onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); + void onRowsRemoved(const QModelIndex& parent, int first, int last); + void onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row); + void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles); + void onModelReset(); + + // Members + QAbstractItemModel* m_model = nullptr; + QQmlComponent* m_delegate = nullptr; + + qreal m_spacing = 0; + qreal m_contentHeight = 0; + qreal m_layoutHeight = 0; + qreal m_contentY = 0; + + QRectF m_viewport; + bool m_useCustomViewport = false; + qreal m_cacheBuffer = 0; + + qreal m_estimatedHeight = -1; + qreal m_knownHeightSum = 0; + int m_knownHeightCount = 0; + bool m_asynchronous = false; + + int m_removeDuration = 300; + int m_readyDelay = 0; + + QVector m_layout; + QHash m_delegates; + QHash m_itemToIndex; + QVector m_dyingDelegates; + + bool m_componentComplete = false; + bool m_relayoutPending = false; + + QList m_modelConnections; +}; + +} // namespace caelestia::components diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt new file mode 100644 index 000000000..4b26ea02d --- /dev/null +++ b/plugin/src/Caelestia/Config/CMakeLists.txt @@ -0,0 +1,32 @@ +qml_module(caelestia-config + URI Caelestia.Config + SOURCES + config.cpp + configattached.cpp + configobject.cpp + rootconfig.cpp + appearanceconfig.cpp + tokens.cpp + tokensattached.cpp + anim.cpp + monitorconfigmanager.cpp + backgroundconfig.hpp + barconfig.hpp + borderconfig.hpp + controlcenterconfig.hpp + dashboardconfig.hpp + generalconfig.hpp + launcherconfig.hpp + lockconfig.hpp + notifsconfig.hpp + osdconfig.hpp + serviceconfig.hpp + sessionconfig.hpp + sidebarconfig.hpp + userpaths.hpp + utilitiesconfig.hpp + winfoconfig.hpp + LIBRARIES + Qt::Quick + Qt::QuickControls2 +) diff --git a/plugin/src/Caelestia/Config/anim.cpp b/plugin/src/Caelestia/Config/anim.cpp new file mode 100644 index 000000000..e307e2f9b --- /dev/null +++ b/plugin/src/Caelestia/Config/anim.cpp @@ -0,0 +1,117 @@ +#include "anim.hpp" +#include "appearanceconfig.hpp" +#include "tokens.hpp" + +#include + +namespace caelestia::config { + +AnimTokens::AnimTokens(QObject* parent) + : QObject(parent) {} + +QEasingCurve AnimTokens::emphasized() const { + return m_emphasized; +} + +QEasingCurve AnimTokens::emphasizedAccel() const { + return m_emphasizedAccel; +} + +QEasingCurve AnimTokens::emphasizedDecel() const { + return m_emphasizedDecel; +} + +QEasingCurve AnimTokens::standard() const { + return m_standard; +} + +QEasingCurve AnimTokens::standardAccel() const { + return m_standardAccel; +} + +QEasingCurve AnimTokens::standardDecel() const { + return m_standardDecel; +} + +QEasingCurve AnimTokens::expressiveFastSpatial() const { + return m_expressiveFastSpatial; +} + +QEasingCurve AnimTokens::expressiveDefaultSpatial() const { + return m_expressiveDefaultSpatial; +} + +QEasingCurve AnimTokens::expressiveSlowSpatial() const { + return m_expressiveSlowSpatial; +} + +QEasingCurve AnimTokens::expressiveFastEffects() const { + return m_expressiveFastEffects; +} + +QEasingCurve AnimTokens::expressiveDefaultEffects() const { + return m_expressiveDefaultEffects; +} + +QEasingCurve AnimTokens::expressiveSlowEffects() const { + return m_expressiveSlowEffects; +} + +AnimDurations* AnimTokens::durations() const { + return m_durations; +} + +QEasingCurve AnimTokens::buildCurve(const QList& points) { + QEasingCurve curve(QEasingCurve::BezierSpline); + + // Points come in pairs of (x, y) forming cubic bezier segments. + // Each segment needs 3 control points: c1, c2, endPoint. + // So 6 values per segment: c1x, c1y, c2x, c2y, endX, endY. + for (int i = 0; i + 5 < points.size(); i += 6) { + QPointF c1(points[i], points[i + 1]); + QPointF c2(points[i + 2], points[i + 3]); + QPointF end(points[i + 4], points[i + 5]); + curve.addCubicBezierSegment(c1, c2, end); + } + + return curve; +} + +void AnimTokens::rebuildCurves() { + if (!m_curves) + return; + + m_emphasized = buildCurve(m_curves->emphasized()); + m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel()); + m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel()); + m_standard = buildCurve(m_curves->standard()); + m_standardAccel = buildCurve(m_curves->standardAccel()); + m_standardDecel = buildCurve(m_curves->standardDecel()); + m_expressiveFastSpatial = buildCurve(m_curves->expressiveFastSpatial()); + m_expressiveDefaultSpatial = buildCurve(m_curves->expressiveDefaultSpatial()); + m_expressiveSlowSpatial = buildCurve(m_curves->expressiveSlowSpatial()); + m_expressiveFastEffects = buildCurve(m_curves->expressiveFastEffects()); + m_expressiveDefaultEffects = buildCurve(m_curves->expressiveDefaultEffects()); + m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects()); + + emit curvesChanged(); +} + +void AnimTokens::bindCurves(AnimCurves* curves) { + m_curves = curves; + + // Rebuild when any curve control points change + connect(curves, &AnimCurves::propertiesChanged, this, &AnimTokens::rebuildCurves); + + rebuildCurves(); +} + +void AnimTokens::bindDurations(AnimDurations* durations) { + if (m_durations == durations) + return; + + m_durations = durations; + emit durationsChanged(); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/anim.hpp b/plugin/src/Caelestia/Config/anim.hpp new file mode 100644 index 000000000..895b8e067 --- /dev/null +++ b/plugin/src/Caelestia/Config/anim.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::config { + +class AnimCurves; +class AnimDurations; + +class AnimTokens : public QObject { + Q_OBJECT + Q_MOC_INCLUDE("tokens.hpp") // AnimCurves + Q_MOC_INCLUDE("appearanceconfig.hpp") // AnimDurations + QML_ANONYMOUS + + Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve emphasizedAccel READ emphasizedAccel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve emphasizedDecel READ emphasizedDecel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve standard READ standard NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve standardAccel READ standardAccel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve standardDecel READ standardDecel NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveFastSpatial READ expressiveFastSpatial NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveFastEffects READ expressiveFastEffects NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY curvesChanged) + Q_PROPERTY(QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY curvesChanged) + + Q_PROPERTY(caelestia::config::AnimDurations* durations READ durations NOTIFY durationsChanged) + +public: + explicit AnimTokens(QObject* parent = nullptr); + + void bindCurves(AnimCurves* curves); + void bindDurations(AnimDurations* durations); + + [[nodiscard]] QEasingCurve emphasized() const; + [[nodiscard]] QEasingCurve emphasizedAccel() const; + [[nodiscard]] QEasingCurve emphasizedDecel() const; + [[nodiscard]] QEasingCurve standard() const; + [[nodiscard]] QEasingCurve standardAccel() const; + [[nodiscard]] QEasingCurve standardDecel() const; + [[nodiscard]] QEasingCurve expressiveFastSpatial() const; + [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const; + [[nodiscard]] QEasingCurve expressiveSlowSpatial() const; + [[nodiscard]] QEasingCurve expressiveFastEffects() const; + [[nodiscard]] QEasingCurve expressiveDefaultEffects() const; + [[nodiscard]] QEasingCurve expressiveSlowEffects() const; + [[nodiscard]] AnimDurations* durations() const; + +signals: + void curvesChanged(); + void durationsChanged(); + +private: + void rebuildCurves(); + static QEasingCurve buildCurve(const QList& points); + + AnimCurves* m_curves = nullptr; + AnimDurations* m_durations = nullptr; + + QEasingCurve m_emphasized; + QEasingCurve m_emphasizedAccel; + QEasingCurve m_emphasizedDecel; + QEasingCurve m_standard; + QEasingCurve m_standardAccel; + QEasingCurve m_standardDecel; + QEasingCurve m_expressiveFastSpatial; + QEasingCurve m_expressiveDefaultSpatial; + QEasingCurve m_expressiveSlowSpatial; + QEasingCurve m_expressiveFastEffects; + QEasingCurve m_expressiveDefaultEffects; + QEasingCurve m_expressiveSlowEffects; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp new file mode 100644 index 000000000..59228c671 --- /dev/null +++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp @@ -0,0 +1,183 @@ +#include "appearanceconfig.hpp" +#include "tokens.hpp" + +#include + +namespace caelestia::config { + +// Helper: connect all changed signals from a token object to a single valuesChanged signal, +// plus connect the local scaleChanged signal. +template static void connectTokenSignals(Source* source, Target* target) { + const auto* meta = source->metaObject(); + + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + + if (prop.hasNotifySignal()) + QObject::connect(source, prop.notifySignal(), target, + target->metaObject()->method(target->metaObject()->indexOfSignal("valuesChanged()"))); + } + + QObject::connect(target, &Target::scaleChanged, target, &Target::valuesChanged); +} + +// AppearanceRounding + +void AppearanceRounding::bindTokens(RoundingTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AppearanceRounding::extraSmall() const { + return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; +} + +int AppearanceRounding::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AppearanceRounding::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AppearanceRounding::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +int AppearanceRounding::full() const { + return m_tokens ? static_cast(m_tokens->full() * m_scale) : 0; +} + +// AppearanceSpacing + +void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AppearanceSpacing::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AppearanceSpacing::smaller() const { + return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; +} + +int AppearanceSpacing::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AppearanceSpacing::larger() const { + return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +} + +int AppearanceSpacing::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +// AppearancePadding + +void AppearancePadding::bindTokens(PaddingTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AppearancePadding::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AppearancePadding::smaller() const { + return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; +} + +int AppearancePadding::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AppearancePadding::larger() const { + return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +} + +int AppearancePadding::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +// FontSize + +void FontSize::bindTokens(FontSizeTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int FontSize::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int FontSize::smaller() const { + return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; +} + +int FontSize::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int FontSize::larger() const { + return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; +} + +int FontSize::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +int FontSize::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +// AnimDurations + +void AnimDurations::bindTokens(AnimDurationTokens* tokens) { + m_tokens = tokens; + connectTokenSignals(tokens, this); +} + +int AnimDurations::small() const { + return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; +} + +int AnimDurations::normal() const { + return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; +} + +int AnimDurations::large() const { + return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; +} + +int AnimDurations::extraLarge() const { + return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; +} + +int AnimDurations::expressiveFastSpatial() const { + return m_tokens ? static_cast(m_tokens->expressiveFastSpatial() * m_scale) : 0; +} + +int AnimDurations::expressiveDefaultSpatial() const { + return m_tokens ? static_cast(m_tokens->expressiveDefaultSpatial() * m_scale) : 0; +} + +int AnimDurations::expressiveSlowSpatial() const { + return m_tokens ? static_cast(m_tokens->expressiveSlowSpatial() * m_scale) : 0; +} + +int AnimDurations::expressiveFastEffects() const { + return m_tokens ? static_cast(m_tokens->expressiveFastEffects() * m_scale) : 0; +} + +int AnimDurations::expressiveDefaultEffects() const { + return m_tokens ? static_cast(m_tokens->expressiveDefaultEffects() * m_scale) : 0; +} + +int AnimDurations::expressiveSlowEffects() const { + return m_tokens ? static_cast(m_tokens->expressiveSlowEffects() * m_scale) : 0; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp new file mode 100644 index 000000000..3446ea72c --- /dev/null +++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp @@ -0,0 +1,259 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +// Forward declare token types from advancedconfig.hpp +class RoundingTokens; +class SpacingTokens; +class PaddingTokens; +class FontSizeTokens; +class AnimDurationTokens; + +class AppearanceRounding : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int full READ full NOTIFY valuesChanged) + +public: + explicit AppearanceRounding(QObject* parent = nullptr) + : ConfigObject(parent) {} + + void bindTokens(RoundingTokens* tokens); + + [[nodiscard]] int extraSmall() const; + [[nodiscard]] int small() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int large() const; + [[nodiscard]] int full() const; + +signals: + void valuesChanged(); + +private: + RoundingTokens* m_tokens = nullptr; +}; + +class AppearanceSpacing : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + +public: + explicit AppearanceSpacing(QObject* parent = nullptr) + : ConfigObject(parent) {} + + void bindTokens(SpacingTokens* tokens); + + [[nodiscard]] int small() const; + [[nodiscard]] int smaller() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int larger() const; + [[nodiscard]] int large() const; + +signals: + void valuesChanged(); + +private: + SpacingTokens* m_tokens = nullptr; +}; + +class AppearancePadding : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + +public: + explicit AppearancePadding(QObject* parent = nullptr) + : ConfigObject(parent) {} + + void bindTokens(PaddingTokens* tokens); + + [[nodiscard]] int small() const; + [[nodiscard]] int smaller() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int larger() const; + [[nodiscard]] int large() const; + +signals: + void valuesChanged(); + +private: + PaddingTokens* m_tokens = nullptr; +}; + +class FontFamily : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(QString, sans, QStringLiteral("Rubik")) + CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF")) + CONFIG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) + CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) + +public: + explicit FontFamily(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class FontSize : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + +public: + explicit FontSize(QObject* parent = nullptr) + : ConfigObject(parent) {} + + void bindTokens(FontSizeTokens* tokens); + + [[nodiscard]] int small() const; + [[nodiscard]] int smaller() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int larger() const; + [[nodiscard]] int large() const; + [[nodiscard]] int extraLarge() const; + +signals: + void valuesChanged(); + +private: + FontSizeTokens* m_tokens = nullptr; +}; + +class AppearanceFont : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_SUBOBJECT(FontFamily, family) + CONFIG_SUBOBJECT(FontSize, size) + +public: + explicit AppearanceFont(QObject* parent = nullptr) + : ConfigObject(parent) + , m_family(new FontFamily(this)) + , m_size(new FontSize(this)) {} +}; + +class AnimDurations : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(qreal, scale, 1) + + Q_PROPERTY(int small READ small NOTIFY valuesChanged) + Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) + Q_PROPERTY(int large READ large NOTIFY valuesChanged) + Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) + Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY valuesChanged) + Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY valuesChanged) + Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY valuesChanged) + Q_PROPERTY(int expressiveFastEffects READ expressiveFastEffects NOTIFY valuesChanged) + Q_PROPERTY(int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY valuesChanged) + Q_PROPERTY(int expressiveSlowEffects READ expressiveSlowEffects NOTIFY valuesChanged) + +public: + explicit AnimDurations(QObject* parent = nullptr) + : ConfigObject(parent) {} + + void bindTokens(AnimDurationTokens* tokens); + + [[nodiscard]] int small() const; + [[nodiscard]] int normal() const; + [[nodiscard]] int large() const; + [[nodiscard]] int extraLarge() const; + [[nodiscard]] int expressiveFastSpatial() const; + [[nodiscard]] int expressiveDefaultSpatial() const; + [[nodiscard]] int expressiveSlowSpatial() const; + [[nodiscard]] int expressiveFastEffects() const; + [[nodiscard]] int expressiveDefaultEffects() const; + [[nodiscard]] int expressiveSlowEffects() const; + +signals: + void valuesChanged(); + +private: + AnimDurationTokens* m_tokens = nullptr; +}; + +class AppearanceAnim : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_SUBOBJECT(AnimDurations, durations) + +public: + explicit AppearanceAnim(QObject* parent = nullptr) + : ConfigObject(parent) + , m_durations(new AnimDurations(this)) {} +}; + +class AppearanceTransparency : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(bool, enabled, false) + CONFIG_GLOBAL_PROPERTY(qreal, base, 0.85) + CONFIG_GLOBAL_PROPERTY(qreal, layers, 0.4) + +public: + explicit AppearanceTransparency(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class AppearanceConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, deformScale, 1) + CONFIG_SUBOBJECT(AppearanceRounding, rounding) + CONFIG_SUBOBJECT(AppearanceSpacing, spacing) + CONFIG_SUBOBJECT(AppearancePadding, padding) + CONFIG_SUBOBJECT(AppearanceFont, font) + CONFIG_SUBOBJECT(AppearanceAnim, anim) + CONFIG_SUBOBJECT(AppearanceTransparency, transparency) + +public: + explicit AppearanceConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_rounding(new AppearanceRounding(this)) + , m_spacing(new AppearanceSpacing(this)) + , m_padding(new AppearancePadding(this)) + , m_font(new AppearanceFont(this)) + , m_anim(new AppearanceAnim(this)) + , m_transparency(new AppearanceTransparency(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/backgroundconfig.hpp b/plugin/src/Caelestia/Config/backgroundconfig.hpp new file mode 100644 index 000000000..743cd787a --- /dev/null +++ b/plugin/src/Caelestia/Config/backgroundconfig.hpp @@ -0,0 +1,84 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class DesktopClockBackground : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(qreal, opacity, 0.7) + CONFIG_PROPERTY(bool, blur, true) + +public: + explicit DesktopClockBackground(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class DesktopClockShadow : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(qreal, opacity, 0.7) + CONFIG_PROPERTY(qreal, blur, 0.4) + +public: + explicit DesktopClockShadow(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class DesktopClock : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(qreal, scale, 1.0) + CONFIG_PROPERTY(QString, position, QStringLiteral("bottom-right")) + CONFIG_PROPERTY(bool, invertColors, false) + CONFIG_SUBOBJECT(DesktopClockBackground, background) + CONFIG_SUBOBJECT(DesktopClockShadow, shadow) + +public: + explicit DesktopClock(QObject* parent = nullptr) + : ConfigObject(parent) + , m_background(new DesktopClockBackground(this)) + , m_shadow(new DesktopClockShadow(this)) {} +}; + +class BackgroundVisualiser : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, false) + CONFIG_PROPERTY(bool, autoHide, true) + CONFIG_PROPERTY(bool, blur, false) + CONFIG_PROPERTY(qreal, rounding, 1) + CONFIG_PROPERTY(qreal, spacing, 1) + +public: + explicit BackgroundVisualiser(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BackgroundConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(bool, wallpaperEnabled, true) + CONFIG_SUBOBJECT(DesktopClock, desktopClock) + CONFIG_SUBOBJECT(BackgroundVisualiser, visualiser) + +public: + explicit BackgroundConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_desktopClock(new DesktopClock(this)) + , m_visualiser(new BackgroundVisualiser(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp new file mode 100644 index 000000000..43d43205a --- /dev/null +++ b/plugin/src/Caelestia/Config/barconfig.hpp @@ -0,0 +1,166 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +using Qt::StringLiterals::operator""_s; + +class BarScrollActions : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, workspaces, true) + CONFIG_PROPERTY(bool, volume, true) + CONFIG_PROPERTY(bool, brightness, true) + +public: + explicit BarScrollActions(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarPopouts : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, activeWindow, true) + CONFIG_PROPERTY(bool, tray, true) + CONFIG_PROPERTY(bool, statusIcons, true) + +public: + explicit BarPopouts(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarWorkspaces : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, shown, 5) + CONFIG_PROPERTY(bool, activeIndicator, true) + CONFIG_PROPERTY(bool, occupiedBg, false) + CONFIG_PROPERTY(bool, showWindows, true) + CONFIG_PROPERTY(bool, showWindowsOnSpecialWorkspaces, true) + CONFIG_PROPERTY(int, maxWindowIcons, 5) + CONFIG_PROPERTY(bool, activeTrail, false) + CONFIG_GLOBAL_PROPERTY(bool, perMonitorWorkspaces, true) + CONFIG_PROPERTY(QString, label, u" "_s) + CONFIG_PROPERTY(QString, occupiedLabel, u"󰮯"_s) + CONFIG_PROPERTY(QString, activeLabel, u"󰮯"_s) + CONFIG_PROPERTY(QString, capitalisation, u"preserve"_s) + CONFIG_GLOBAL_PROPERTY(QVariantList, specialWorkspaceIcons) + CONFIG_GLOBAL_PROPERTY(QVariantList, windowIcons, + { vmap({ + { u"regex"_s, u"steam(_app_(default|[0-9]+))?"_s }, + { u"icon"_s, u"sports_esports"_s }, + }) }) + +public: + explicit BarWorkspaces(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarActiveWindow : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, compact, false) + CONFIG_PROPERTY(bool, inverted, false) + CONFIG_PROPERTY(bool, showOnHover, true) + +public: + explicit BarActiveWindow(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarTray : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, background, false) + CONFIG_PROPERTY(bool, recolour, false) + CONFIG_PROPERTY(bool, compact, false) + CONFIG_GLOBAL_PROPERTY(QVariantList, iconSubs) + CONFIG_GLOBAL_PROPERTY(QStringList, hiddenIcons) + +public: + explicit BarTray(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarStatus : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, showAudio, false) + CONFIG_PROPERTY(bool, showMicrophone, false) + CONFIG_PROPERTY(bool, showKbLayout, false) + CONFIG_PROPERTY(bool, showNetwork, true) + CONFIG_PROPERTY(bool, showWifi, true) + CONFIG_PROPERTY(bool, showBluetooth, true) + CONFIG_PROPERTY(bool, showBattery, true) + CONFIG_PROPERTY(bool, showLockStatus, true) + +public: + explicit BarStatus(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarClock : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, background, false) + CONFIG_PROPERTY(bool, showDate, false) + CONFIG_PROPERTY(bool, showIcon, true) + +public: + explicit BarClock(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class BarConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, persistent, true) + CONFIG_PROPERTY(bool, showOnHover, true) + CONFIG_PROPERTY(int, dragThreshold, 20) + CONFIG_SUBOBJECT(BarScrollActions, scrollActions) + CONFIG_SUBOBJECT(BarPopouts, popouts) + CONFIG_SUBOBJECT(BarWorkspaces, workspaces) + CONFIG_SUBOBJECT(BarActiveWindow, activeWindow) + CONFIG_SUBOBJECT(BarTray, tray) + CONFIG_SUBOBJECT(BarStatus, status) + CONFIG_SUBOBJECT(BarClock, clock) + CONFIG_PROPERTY(QVariantList, entries, + { + vmap({ { u"id"_s, u"logo"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"workspaces"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"activeWindow"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"tray"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"clock"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"statusIcons"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"power"_s }, { u"enabled"_s, true } }), + }) + CONFIG_PROPERTY(QStringList, excludedScreens) + +public: + explicit BarConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_scrollActions(new BarScrollActions(this)) + , m_popouts(new BarPopouts(this)) + , m_workspaces(new BarWorkspaces(this)) + , m_activeWindow(new BarActiveWindow(this)) + , m_tray(new BarTray(this)) + , m_status(new BarStatus(this)) + , m_clock(new BarClock(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/borderconfig.hpp b/plugin/src/Caelestia/Config/borderconfig.hpp new file mode 100644 index 000000000..9de604e82 --- /dev/null +++ b/plugin/src/Caelestia/Config/borderconfig.hpp @@ -0,0 +1,29 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class BorderConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, thickness, 10) + CONFIG_PROPERTY(int, rounding, 25) + CONFIG_PROPERTY(int, smoothing, 32) + + Q_PROPERTY(int minThickness READ minThickness CONSTANT) + Q_PROPERTY(int clampedThickness READ clampedThickness NOTIFY thicknessChanged) + +public: + explicit BorderConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} + + [[nodiscard]] static int minThickness() { return 2; } + + [[nodiscard]] int clampedThickness() const { return std::max(minThickness(), m_thickness); } +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp new file mode 100644 index 000000000..ff939f998 --- /dev/null +++ b/plugin/src/Caelestia/Config/config.cpp @@ -0,0 +1,120 @@ +#include "config.hpp" +#include "appearanceconfig.hpp" +#include "backgroundconfig.hpp" +#include "barconfig.hpp" +#include "borderconfig.hpp" +#include "controlcenterconfig.hpp" +#include "dashboardconfig.hpp" +#include "generalconfig.hpp" +#include "launcherconfig.hpp" +#include "lockconfig.hpp" +#include "monitorconfigmanager.hpp" +#include "notifsconfig.hpp" +#include "osdconfig.hpp" +#include "serviceconfig.hpp" +#include "sessionconfig.hpp" +#include "sidebarconfig.hpp" +#include "tokens.hpp" +#include "userpaths.hpp" +#include "utilitiesconfig.hpp" +#include "winfoconfig.hpp" + +#include +#include + +namespace caelestia::config { + +namespace { + +QString configDir() { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); +} + +} // namespace + +GlobalConfig::GlobalConfig(QObject* parent) + : RootConfig(parent) + , m_appearance(new AppearanceConfig(this)) + , m_general(new GeneralConfig(this)) + , m_background(new BackgroundConfig(this)) + , m_bar(new BarConfig(this)) + , m_border(new BorderConfig(this)) + , m_dashboard(new DashboardConfig(this)) + , m_controlCenter(new ControlCenterConfig(this)) + , m_launcher(new LauncherConfig(this)) + , m_notifs(new NotifsConfig(this)) + , m_osd(new OsdConfig(this)) + , m_session(new SessionConfig(this)) + , m_winfo(new WInfoConfig(this)) + , m_lock(new LockConfig(this)) + , m_utilities(new UtilitiesConfig(this)) + , m_sidebar(new SidebarConfig(this)) + , m_services(new ServiceConfig(this)) + , m_paths(new UserPaths(this)) { + setupFileBackend(configDir() + QStringLiteral("shell.json")); +} + +GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) + : RootConfig(parent) + , m_appearance(new AppearanceConfig(this)) + , m_general(new GeneralConfig(this)) + , m_background(new BackgroundConfig(this)) + , m_bar(new BarConfig(this)) + , m_border(new BorderConfig(this)) + , m_dashboard(new DashboardConfig(this)) + , m_controlCenter(new ControlCenterConfig(this)) + , m_launcher(new LauncherConfig(this)) + , m_notifs(new NotifsConfig(this)) + , m_osd(new OsdConfig(this)) + , m_session(new SessionConfig(this)) + , m_winfo(new WInfoConfig(this)) + , m_lock(new LockConfig(this)) + , m_utilities(new UtilitiesConfig(this)) + , m_sidebar(new SidebarConfig(this)) + , m_services(new ServiceConfig(this)) + , m_paths(new UserPaths(this)) { + if (!filePath.isEmpty()) + setupFileBackend(filePath, screen); + if (fallback) + syncFromGlobal(fallback); + + // Bind appearance computed properties to token base values + bindAppearanceTokens(); +} + +GlobalConfig* GlobalConfig::instance() { + static GlobalConfig instance; + instance.bindAppearanceTokens(); + return &instance; +} + +GlobalConfig* GlobalConfig::defaults() { + if (!m_defaults) + m_defaults = new GlobalConfig(nullptr, QString(), QString(), this); + return m_defaults; +} + +void GlobalConfig::bindAppearanceTokens() { + if (m_tokensBound) + return; + + qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: binding appearance to token values"; + auto* const tokenAppearance = TokenConfig::instance()->appearance(); + m_appearance->rounding()->bindTokens(tokenAppearance->rounding()); + m_appearance->spacing()->bindTokens(tokenAppearance->spacing()); + m_appearance->padding()->bindTokens(tokenAppearance->padding()); + m_appearance->font()->size()->bindTokens(tokenAppearance->fontSize()); + m_appearance->anim()->durations()->bindTokens(tokenAppearance->animDurations()); + m_tokensBound = true; +} + +GlobalConfig* GlobalConfig::forScreen(const QString& screen) { + return MonitorConfigManager::instance()->configForScreen(screen); +} + +GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { + QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); + return instance(); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp new file mode 100644 index 000000000..248a3f225 --- /dev/null +++ b/plugin/src/Caelestia/Config/config.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include "rootconfig.hpp" + +#include + +namespace caelestia::config { + +class AppearanceConfig; +class BackgroundConfig; +class BarConfig; +class BorderConfig; +class ControlCenterConfig; +class DashboardConfig; +class GeneralConfig; +class LauncherConfig; +class LockConfig; +class NotifsConfig; +class OsdConfig; +class ServiceConfig; +class SessionConfig; +class SidebarConfig; +class UserPaths; +class UtilitiesConfig; +class WInfoConfig; + +class GlobalConfig : public RootConfig { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("backgroundconfig.hpp") + Q_MOC_INCLUDE("barconfig.hpp") + Q_MOC_INCLUDE("borderconfig.hpp") + Q_MOC_INCLUDE("controlcenterconfig.hpp") + Q_MOC_INCLUDE("dashboardconfig.hpp") + Q_MOC_INCLUDE("generalconfig.hpp") + Q_MOC_INCLUDE("launcherconfig.hpp") + Q_MOC_INCLUDE("lockconfig.hpp") + Q_MOC_INCLUDE("notifsconfig.hpp") + Q_MOC_INCLUDE("osdconfig.hpp") + Q_MOC_INCLUDE("serviceconfig.hpp") + Q_MOC_INCLUDE("sessionconfig.hpp") + Q_MOC_INCLUDE("sidebarconfig.hpp") + Q_MOC_INCLUDE("userpaths.hpp") + Q_MOC_INCLUDE("utilitiesconfig.hpp") + Q_MOC_INCLUDE("winfoconfig.hpp") + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_SUBOBJECT(AppearanceConfig, appearance) + CONFIG_SUBOBJECT(GeneralConfig, general) + CONFIG_SUBOBJECT(BackgroundConfig, background) + CONFIG_SUBOBJECT(BarConfig, bar) + CONFIG_SUBOBJECT(BorderConfig, border) + CONFIG_SUBOBJECT(DashboardConfig, dashboard) + CONFIG_SUBOBJECT(ControlCenterConfig, controlCenter) + CONFIG_SUBOBJECT(LauncherConfig, launcher) + CONFIG_SUBOBJECT(NotifsConfig, notifs) + CONFIG_SUBOBJECT(OsdConfig, osd) + CONFIG_SUBOBJECT(SessionConfig, session) + CONFIG_SUBOBJECT(WInfoConfig, winfo) + CONFIG_SUBOBJECT(LockConfig, lock) + CONFIG_SUBOBJECT(UtilitiesConfig, utilities) + CONFIG_SUBOBJECT(SidebarConfig, sidebar) + CONFIG_SUBOBJECT(ServiceConfig, services) + CONFIG_SUBOBJECT(UserPaths, paths) + +public: + static GlobalConfig* instance(); + [[nodiscard]] Q_INVOKABLE GlobalConfig* defaults(); + [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); + static GlobalConfig* create(QQmlEngine*, QJSEngine*); + + void bindAppearanceTokens(); + +private: + friend class MonitorConfigManager; + explicit GlobalConfig(QObject* parent = nullptr); + explicit GlobalConfig( + GlobalConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); + + GlobalConfig* m_defaults = nullptr; + bool m_tokensBound = false; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp new file mode 100644 index 000000000..8d229049b --- /dev/null +++ b/plugin/src/Caelestia/Config/configattached.cpp @@ -0,0 +1,96 @@ +#include "configattached.hpp" +#include "config.hpp" +#include "monitorconfigmanager.hpp" + +#include + +namespace caelestia::config { + +Config::Config(QObject* parent) + : QQuickAttachedPropertyPropagator(parent) { + initialize(); +} + +void Config::classBegin() {} + +void Config::componentComplete() { + m_complete = true; +} + +QString Config::screen() const { + return m_screen; +} + +void Config::inheritScreen(const QString& screen) { + if (screen == m_screen) + return; + + m_screen = screen; + + if (m_screen.isEmpty()) + m_config = nullptr; + else + m_config = MonitorConfigManager::instance()->configForScreen(m_screen); + + propagateScreen(); + emit sourceChanged(); +} + +void Config::propagateScreen() { + const auto children = attachedChildren(); + for (auto* const child : children) { + auto* const config = qobject_cast(child); + if (config) + config->inheritScreen(m_screen); + } +} + +void Config::attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { + Q_UNUSED(oldParent); + auto* const config = qobject_cast(newParent); + if (config) + inheritScreen(config->screen()); +} + +#define CONFIG_ATTACHED_GETTER(Type, name) \ + const Type* Config::name() const { \ + if (m_config) \ + return m_config->name(); \ + /* Suppress warnings before component is complete if attached to a QQuickItem. */ \ + /* Raw QObjects are unable to inherit the screen (only QQuickItems can). */ \ + if ((m_complete || !qobject_cast(parent())) && parent()) \ + qCWarning(lcConfig, "Config.%s accessed without a screen set on %s", #name, \ + parent()->metaObject()->className()); \ + return GlobalConfig::instance()->name(); \ + } + +CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) +CONFIG_ATTACHED_GETTER(GeneralConfig, general) +CONFIG_ATTACHED_GETTER(BackgroundConfig, background) +CONFIG_ATTACHED_GETTER(BarConfig, bar) +CONFIG_ATTACHED_GETTER(BorderConfig, border) +CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard) +CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter) +CONFIG_ATTACHED_GETTER(LauncherConfig, launcher) +CONFIG_ATTACHED_GETTER(NotifsConfig, notifs) +CONFIG_ATTACHED_GETTER(OsdConfig, osd) +CONFIG_ATTACHED_GETTER(SessionConfig, session) +CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) +CONFIG_ATTACHED_GETTER(LockConfig, lock) +CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) +CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) +CONFIG_ATTACHED_GETTER(ServiceConfig, services) +CONFIG_ATTACHED_GETTER(UserPaths, paths) + +#undef CONFIG_ATTACHED_GETTER + +GlobalConfig* Config::forScreen(const QString& screen) { + return GlobalConfig::forScreen(screen); +} + +Config* Config::qmlAttachedProperties(QObject* object) { + return new Config(object); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp new file mode 100644 index 000000000..815b10805 --- /dev/null +++ b/plugin/src/Caelestia/Config/configattached.hpp @@ -0,0 +1,98 @@ +#pragma once + +#include "config.hpp" + +#include + +namespace caelestia::config { + +class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + QML_ELEMENT + QML_UNCREATABLE("") + QML_ATTACHED(Config) + Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("backgroundconfig.hpp") + Q_MOC_INCLUDE("barconfig.hpp") + Q_MOC_INCLUDE("borderconfig.hpp") + Q_MOC_INCLUDE("controlcenterconfig.hpp") + Q_MOC_INCLUDE("dashboardconfig.hpp") + Q_MOC_INCLUDE("generalconfig.hpp") + Q_MOC_INCLUDE("launcherconfig.hpp") + Q_MOC_INCLUDE("lockconfig.hpp") + Q_MOC_INCLUDE("notifsconfig.hpp") + Q_MOC_INCLUDE("osdconfig.hpp") + Q_MOC_INCLUDE("serviceconfig.hpp") + Q_MOC_INCLUDE("sessionconfig.hpp") + Q_MOC_INCLUDE("sidebarconfig.hpp") + Q_MOC_INCLUDE("userpaths.hpp") + Q_MOC_INCLUDE("utilitiesconfig.hpp") + Q_MOC_INCLUDE("winfoconfig.hpp") + + Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BarConfig* bar READ bar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::BorderConfig* border READ border NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) + +public: + explicit Config(QObject* parent = nullptr); + + [[nodiscard]] QString screen() const; + void inheritScreen(const QString& screen); + + [[nodiscard]] const AppearanceConfig* appearance() const; + [[nodiscard]] const GeneralConfig* general() const; + [[nodiscard]] const BackgroundConfig* background() const; + [[nodiscard]] const BarConfig* bar() const; + [[nodiscard]] const BorderConfig* border() const; + [[nodiscard]] const DashboardConfig* dashboard() const; + [[nodiscard]] const ControlCenterConfig* controlCenter() const; + [[nodiscard]] const LauncherConfig* launcher() const; + [[nodiscard]] const NotifsConfig* notifs() const; + [[nodiscard]] const OsdConfig* osd() const; + [[nodiscard]] const SessionConfig* session() const; + [[nodiscard]] const WInfoConfig* winfo() const; + [[nodiscard]] const LockConfig* lock() const; + [[nodiscard]] const UtilitiesConfig* utilities() const; + [[nodiscard]] const SidebarConfig* sidebar() const; + [[nodiscard]] const ServiceConfig* services() const; + [[nodiscard]] const UserPaths* paths() const; + + [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); + + static Config* qmlAttachedProperties(QObject* object); + +signals: + void sourceChanged(); + +protected: + void attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; + +private: + void classBegin() override; + void componentComplete() override; + + void propagateScreen(); + + bool m_complete = false; + QString m_screen; + GlobalConfig* m_config = nullptr; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp new file mode 100644 index 000000000..030ecabe8 --- /dev/null +++ b/plugin/src/Caelestia/Config/configobject.cpp @@ -0,0 +1,316 @@ +#include "configobject.hpp" + +#include +#include +#include +#include +#include +#include + +namespace caelestia::config { + +Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg) + +// ConfigObject + +ConfigObject::ConfigObject(QObject* parent) + : QObject(parent) {} + +void ConfigObject::loadFromJson(const QJsonObject& obj) { + const auto* meta = metaObject(); + + qCDebug(lcConfig) << "Loading JSON into" << meta->className() << "with" << obj.keys().size() + << "keys:" << obj.keys(); + + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + const auto key = QString::fromUtf8(prop.name()); + + if (!obj.contains(key)) + continue; + + if (isGlobalOnly(key)) + qCWarning(lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", + qUtf8Printable(propertyPath(key))); + + const auto jsonVal = obj.value(key); + + // Recurse into sub-objects + auto current = prop.read(this); + auto* subObj = current.value(); + + if (subObj) { + qCDebug(lcConfig) << " Recursing into sub-object" << key; + subObj->loadFromJson(jsonVal.toObject()); + continue; + } + + // Skip read-only properties + if (!prop.isWritable()) + continue; + + // Handle QStringList explicitly (QJsonArray → QStringList needs manual conversion) + if (prop.metaType().id() == QMetaType::QStringList) { + QStringList list; + const auto jsonArr = jsonVal.toArray(); + for (const auto& v : jsonArr) + list.append(v.toString()); + prop.write(this, QVariant::fromValue(list)); + m_loadedKeys.insert(key); + qCDebug(lcConfig) << " Loaded" << key << "=" << list; + continue; + } + + // For all other types, let Qt's variant conversion handle it + prop.write(this, jsonVal.toVariant()); + m_loadedKeys.insert(key); + qCDebug(lcConfig) << " Loaded" << key << "=" << jsonVal.toVariant(); + } +} + +QJsonObject ConfigObject::toJsonObject() const { + QJsonObject obj; + const auto* meta = metaObject(); + + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + const auto prop = meta->property(i); + + if (!prop.isReadable()) + continue; + + const auto key = QString::fromUtf8(prop.name()); + + if (isGlobalOnly(key)) + continue; + + const auto value = prop.read(this); + + // Recurse into sub-objects — include only if they have loaded keys + if (value.canView()) { + auto* const subObj = value.value(); + if (subObj) { + auto subJson = subObj->toJsonObject(); + if (!subJson.isEmpty()) + obj.insert(key, subJson); + } + continue; + } + + // Only include properties that were explicitly loaded + if (!m_loadedKeys.contains(key)) + continue; + + if (!prop.isWritable()) + continue; + + if (prop.metaType().id() == QMetaType::QStringList) { + QJsonArray arr; + const auto strList = value.toStringList(); + for (const auto& s : strList) + arr.append(s); + obj.insert(key, arr); + continue; + } + + if (prop.metaType().id() == QMetaType::QVariantList) { + obj.insert(key, QJsonArray::fromVariantList(value.toList())); + continue; + } + + obj.insert(key, QJsonValue::fromVariant(value)); + } + + return obj; +} + +void ConfigObject::clearLoadedKeys() { + m_loadedKeys.clear(); + + const auto* meta = metaObject(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + if (isGlobalOnly(QString::fromUtf8(prop.name()))) + continue; + auto value = prop.read(this); + auto* subObj = value.value(); + if (subObj) + subObj->clearLoadedKeys(); + } +} + +void ConfigObject::syncFromGlobal(ConfigObject* global) { + m_global = global; + + const auto* meta = metaObject(); + qCDebug(lcConfig) << "Syncing" << meta->className() << "from global, loaded keys:" << m_loadedKeys; + + // Connect batched change signal (single connection per ConfigObject pair) + connect(global, &ConfigObject::propertiesChanged, this, &ConfigObject::onGlobalPropertiesChanged); + + // Initial sync: copy all non-loaded property values from global + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + const auto key = QString::fromUtf8(prop.name()); + + if (isGlobalOnly(key)) + continue; + + auto current = prop.read(this); + auto* subObj = current.value(); + + if (subObj) { + auto globalVal = prop.read(global); + auto* globalSub = globalVal.value(); + if (globalSub) + subObj->syncFromGlobal(globalSub); + continue; + } + + if (!prop.isWritable()) + continue; + + if (!m_loadedKeys.contains(key)) { + auto val = prop.read(global); + prop.write(this, val); + m_loadedKeys.remove(key); // setter added it — remove since this is a synced value + qCDebug(lcConfig) << " Synced" << key << "=" << val << "from global"; + } else { + qCDebug(lcConfig) << " Keeping loaded" << key << "=" << prop.read(this); + } + } +} + +void ConfigObject::resyncFromGlobal() { + if (!m_global) + return; + + const auto* meta = metaObject(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + const auto key = QString::fromUtf8(prop.name()); + + if (isGlobalOnly(key)) + continue; + + auto current = prop.read(this); + auto* subObj = current.value(); + + if (subObj) { + subObj->resyncFromGlobal(); + continue; + } + + if (!prop.isWritable()) + continue; + + if (!m_loadedKeys.contains(key)) { + prop.write(this, prop.read(m_global)); + m_loadedKeys.remove(key); // setter added it — remove since this is a synced value + } + } +} + +QString ConfigObject::propertyPath(const QString& name) const { + QStringList parts; + parts.append(name); + + const QObject* obj = this; + while (auto* parentObj = obj->parent()) { + auto* parentConfig = qobject_cast(parentObj); + if (!parentConfig) + break; + + // Find which property name this child is on the parent + const auto* meta = parentConfig->metaObject(); + bool found = false; + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + auto val = prop.read(parentObj); + if (val.value() == obj) { + parts.prepend(QString::fromUtf8(prop.name())); + found = true; + break; + } + } + + if (!found) + break; + + obj = parentObj; + } + + return parts.join(QLatin1Char('.')); +} + +bool ConfigObject::isPropertyLoaded(const QString& name) const { + return m_loadedKeys.contains(name); +} + +bool ConfigObject::isOverlay() const { + return m_global != nullptr; +} + +bool ConfigObject::isGlobalOnly(const QString& name) const { + return isOverlay() && m_globalOnlyKeys.contains(name); +} + +void ConfigObject::markPropertyLoaded(const QString& name) { + m_loadedKeys.insert(name); +} + +void ConfigObject::resetOption(const QString& name) { + m_loadedKeys.remove(name); + + // If synced from global, re-copy the global value + if (m_global) { + int idx = metaObject()->indexOfProperty(name.toUtf8().constData()); + if (idx >= 0) { + auto prop = metaObject()->property(idx); + if (prop.isWritable()) + prop.write(this, prop.read(m_global)); + } + } +} + +void ConfigObject::onGlobalPropertiesChanged(const QMap& changed) { + for (auto it = changed.begin(); it != changed.end(); ++it) { + if (m_loadedKeys.contains(it.key()) || isGlobalOnly(it.key())) + continue; + + int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); + if (idx >= 0) { + metaObject()->property(idx).write(this, it.value()); + m_loadedKeys.remove(it.key()); // setter added it — remove since this is a synced value + qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value() + << "from global change"; + } + } +} + +void ConfigObject::markGlobalOnly(const QString& name) { + m_globalOnlyKeys.insert(name); +} + +void ConfigObject::notifyPropertyChanged(const QString& name, const QVariant& value) { + m_pendingChanges.insert(name, value); + + if (!m_batchTimer) { + m_batchTimer = new QTimer(this); + m_batchTimer->setSingleShot(true); + m_batchTimer->setInterval(0); + connect(m_batchTimer, &QTimer::timeout, this, &ConfigObject::emitBatchedChanges); + } + + m_batchTimer->start(); +} + +void ConfigObject::emitBatchedChanges() { + if (m_pendingChanges.isEmpty()) + return; + + auto changes = std::move(m_pendingChanges); + m_pendingChanges.clear(); + emit propertiesChanged(changes); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp new file mode 100644 index 000000000..c4f4428b3 --- /dev/null +++ b/plugin/src/Caelestia/Config/configobject.hpp @@ -0,0 +1,143 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace caelestia::config { + +inline QVariantMap vmap(std::initializer_list> entries) { + QVariantMap map; + for (const auto& [key, value] : entries) + map.insert(std::move(key), std::move(value)); + return map; +} + +} // namespace caelestia::config + +// Declares a serialized config property with getter, setter (change-detected), signal, and member. +#define CONFIG_PROPERTY(Type, name, ...) \ + Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ + \ +public: \ + [[nodiscard]] Type name() const { \ + return m_##name; \ + } \ + void set_##name(const Type& val) { \ + if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ + markPropertyLoaded(QStringLiteral(#name)); \ + Q_EMIT name##Changed(); \ + notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ + } \ + } \ + Q_SIGNAL void name##Changed(); \ + \ +private: \ + Type m_##name __VA_OPT__(= __VA_ARGS__); + +// Declares a CONSTANT sub-object property. Initialize the member in the constructor. +#define CONFIG_SUBOBJECT(Type, name) \ + Q_PROPERTY(caelestia::config::Type* name READ name CONSTANT) \ + \ +public: \ + [[nodiscard]] Type* name() const { \ + return m_##name; \ + } \ + \ +private: \ + Type* m_##name = nullptr; + +// Like CONFIG_PROPERTY but warns on read/write when accessed on a per-monitor overlay. +#define CONFIG_GLOBAL_PROPERTY(Type, name, ...) \ + Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ + \ +public: \ + [[nodiscard]] Type name() const { \ + if (isOverlay()) \ + qCWarning(caelestia::config::lcConfig, "Reading global-only option '%s' on per-monitor overlay", \ + qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ + return m_##name; \ + } \ + void set_##name(const Type& val) { \ + if (isOverlay()) \ + qCWarning(caelestia::config::lcConfig, "Writing global-only option '%s' on per-monitor overlay", \ + qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ + if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ + markPropertyLoaded(QStringLiteral(#name)); \ + Q_EMIT name##Changed(); \ + notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ + } \ + } \ + Q_SIGNAL void name##Changed(); \ + \ +private: \ + Type m_##name __VA_OPT__(= __VA_ARGS__); \ + const bool m_##name##_go = [this] { \ + markGlobalOnly(QStringLiteral(#name)); \ + return true; \ + }(); + +namespace caelestia::config { + +Q_DECLARE_LOGGING_CATEGORY(lcConfig) + +class ConfigObject : public QObject { + Q_OBJECT + +public: + explicit ConfigObject(QObject* parent = nullptr); + + void loadFromJson(const QJsonObject& obj); + [[nodiscard]] QJsonObject toJsonObject() const; + + // Per-monitor overlay support (Qt Resolve Mask pattern). + void syncFromGlobal(ConfigObject* global); + void resyncFromGlobal(); + void clearLoadedKeys(); + + [[nodiscard]] bool isPropertyLoaded(const QString& name) const; + [[nodiscard]] QString propertyPath(const QString& name) const; + [[nodiscard]] bool isOverlay() const; + // Returns true only on overlays — global singleton always returns false. + [[nodiscard]] bool isGlobalOnly(const QString& name) const; + + Q_INVOKABLE void resetOption(const QString& name); + + template static bool updateMember(T& member, const T& value) { + if constexpr (std::is_floating_point_v) { + if (qFuzzyCompare(member + 1.0, value + 1.0)) + return false; + } else { + if (member == value) + return false; + } + member = value; + return true; + } + +signals: + void propertiesChanged(const QMap& changed); + +protected: + void markPropertyLoaded(const QString& name); + void markGlobalOnly(const QString& name); + void notifyPropertyChanged(const QString& name, const QVariant& value); + +private: + void onGlobalPropertiesChanged(const QMap& changed); + void emitBatchedChanges(); + + // Per-monitor overlay state + ConfigObject* m_global = nullptr; + QSet m_loadedKeys; + QSet m_globalOnlyKeys; + QMap m_pendingChanges; + QTimer* m_batchTimer = nullptr; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/controlcenterconfig.hpp b/plugin/src/Caelestia/Config/controlcenterconfig.hpp new file mode 100644 index 000000000..80b40b87a --- /dev/null +++ b/plugin/src/Caelestia/Config/controlcenterconfig.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +// ControlCenterConfig has no serialized properties (serializer returns {}) +// All properties are in AdvancedConfig.controlCenter +class ControlCenterConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + +public: + explicit ControlCenterConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/dashboardconfig.hpp b/plugin/src/Caelestia/Config/dashboardconfig.hpp new file mode 100644 index 000000000..cb872d1dd --- /dev/null +++ b/plugin/src/Caelestia/Config/dashboardconfig.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class DashboardPerformance : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, showBattery, true) + CONFIG_PROPERTY(bool, showGpu, true) + CONFIG_PROPERTY(bool, showCpu, true) + CONFIG_PROPERTY(bool, showMemory, true) + CONFIG_PROPERTY(bool, showStorage, true) + CONFIG_PROPERTY(bool, showNetwork, true) + +public: + explicit DashboardPerformance(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class DashboardConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(bool, showOnHover, true) + CONFIG_PROPERTY(bool, showDashboard, true) + CONFIG_PROPERTY(bool, showMedia, true) + CONFIG_PROPERTY(bool, showPerformance, true) + CONFIG_PROPERTY(bool, showWeather, true) + CONFIG_GLOBAL_PROPERTY(int, mediaUpdateInterval, 500) + CONFIG_GLOBAL_PROPERTY(int, resourceUpdateInterval, 1000) + CONFIG_PROPERTY(int, dragThreshold, 50) + CONFIG_SUBOBJECT(DashboardPerformance, performance) + +public: + explicit DashboardConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_performance(new DashboardPerformance(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp new file mode 100644 index 000000000..859302fef --- /dev/null +++ b/plugin/src/Caelestia/Config/generalconfig.hpp @@ -0,0 +1,108 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +using Qt::StringLiterals::operator""_s; + +class GeneralApps : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(QStringList, terminal, { u"foot"_s }) + CONFIG_GLOBAL_PROPERTY(QStringList, audio, { u"pavucontrol"_s }) + CONFIG_GLOBAL_PROPERTY(QStringList, playback, { u"mpv"_s }) + CONFIG_GLOBAL_PROPERTY(QStringList, explorer, { u"thunar"_s }) + +public: + explicit GeneralApps(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class GeneralIdle : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(bool, lockBeforeSleep, true) + CONFIG_GLOBAL_PROPERTY(bool, inhibitWhenAudio, true) + CONFIG_GLOBAL_PROPERTY(QVariantList, timeouts, + { + vmap({ + { u"timeout"_s, 180 }, + { u"idleAction"_s, u"lock"_s }, + }), + vmap({ + { u"timeout"_s, 300 }, + { u"idleAction"_s, u"dpms off"_s }, + { u"returnAction"_s, u"dpms on"_s }, + }), + vmap({ + { u"timeout"_s, 600 }, + { u"idleAction"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, + }), + }) + +public: + explicit GeneralIdle(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class GeneralBattery : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(QVariantList, warnLevels, + { + vmap({ + { u"level"_s, 20 }, + { u"title"_s, u"Low battery"_s }, + { u"message"_s, u"You might want to plug in a charger"_s }, + { u"icon"_s, u"battery_android_frame_2"_s }, + }), + vmap({ + { u"level"_s, 10 }, + { u"title"_s, u"Did you see the previous message?"_s }, + { u"message"_s, u"You should probably plug in a charger now"_s }, + { u"icon"_s, u"battery_android_frame_1"_s }, + }), + vmap({ + { u"level"_s, 5 }, + { u"title"_s, u"Critical battery level"_s }, + { u"message"_s, u"PLUG THE CHARGER RIGHT NOW!!"_s }, + { u"icon"_s, u"battery_android_alert"_s }, + { u"critical"_s, true }, + }), + }) + CONFIG_GLOBAL_PROPERTY(int, criticalLevel, 3) + +public: + explicit GeneralBattery(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class GeneralConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(QString, logo) + CONFIG_PROPERTY(bool, showOverFullscreen, false) + CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) + CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) + CONFIG_SUBOBJECT(GeneralApps, apps) + CONFIG_SUBOBJECT(GeneralIdle, idle) + CONFIG_SUBOBJECT(GeneralBattery, battery) + +public: + explicit GeneralConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_apps(new GeneralApps(this)) + , m_idle(new GeneralIdle(this)) + , m_battery(new GeneralBattery(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp new file mode 100644 index 000000000..049e213ae --- /dev/null +++ b/plugin/src/Caelestia/Config/launcherconfig.hpp @@ -0,0 +1,135 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +using Qt::StringLiterals::operator""_s; + +class LauncherUseFuzzy : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(bool, apps, false) + CONFIG_GLOBAL_PROPERTY(bool, actions, false) + CONFIG_GLOBAL_PROPERTY(bool, schemes, false) + CONFIG_GLOBAL_PROPERTY(bool, variants, false) + CONFIG_GLOBAL_PROPERTY(bool, wallpapers, false) + +public: + explicit LauncherUseFuzzy(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class LauncherConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(bool, showOnHover, false) + CONFIG_PROPERTY(int, maxShown, 7) + CONFIG_PROPERTY(int, maxWallpapers, 9) + CONFIG_GLOBAL_PROPERTY(QString, specialPrefix, u"@"_s) + CONFIG_GLOBAL_PROPERTY(QString, actionPrefix, u">"_s) + CONFIG_GLOBAL_PROPERTY(bool, enableDangerousActions, false) + CONFIG_PROPERTY(int, dragThreshold, 50) + CONFIG_GLOBAL_PROPERTY(bool, vimKeybinds, false) + CONFIG_GLOBAL_PROPERTY(QStringList, favouriteApps) + CONFIG_GLOBAL_PROPERTY(QStringList, hiddenApps) + CONFIG_SUBOBJECT(LauncherUseFuzzy, useFuzzy) + CONFIG_GLOBAL_PROPERTY(QVariantList, actions, + { + vmap({ + { u"name"_s, u"Calculator"_s }, + { u"icon"_s, u"calculate"_s }, + { u"description"_s, u"Do simple math equations (powered by Qalc)"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"calc"_s } }, + }), + vmap({ + { u"name"_s, u"Scheme"_s }, + { u"icon"_s, u"palette"_s }, + { u"description"_s, u"Change the current colour scheme"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"scheme"_s } }, + }), + vmap({ + { u"name"_s, u"Wallpaper"_s }, + { u"icon"_s, u"image"_s }, + { u"description"_s, u"Change the current wallpaper"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"wallpaper"_s } }, + }), + vmap({ + { u"name"_s, u"Variant"_s }, + { u"icon"_s, u"colors"_s }, + { u"description"_s, u"Change the current scheme variant"_s }, + { u"command"_s, QStringList{ u"autocomplete"_s, u"variant"_s } }, + }), + vmap({ + { u"name"_s, u"Random"_s }, + { u"icon"_s, u"casino"_s }, + { u"description"_s, u"Switch to a random wallpaper"_s }, + { u"command"_s, QStringList{ u"caelestia"_s, u"wallpaper"_s, u"-r"_s } }, + }), + vmap({ + { u"name"_s, u"Light"_s }, + { u"icon"_s, u"light_mode"_s }, + { u"description"_s, u"Change the scheme to light mode"_s }, + { u"command"_s, QStringList{ u"setMode"_s, u"light"_s } }, + }), + vmap({ + { u"name"_s, u"Dark"_s }, + { u"icon"_s, u"dark_mode"_s }, + { u"description"_s, u"Change the scheme to dark mode"_s }, + { u"command"_s, QStringList{ u"setMode"_s, u"dark"_s } }, + }), + vmap({ + { u"name"_s, u"Shutdown"_s }, + { u"icon"_s, u"power_settings_new"_s }, + { u"description"_s, u"Shutdown the system"_s }, + { u"command"_s, QStringList{ u"systemctl"_s, u"poweroff"_s } }, + { u"dangerous"_s, true }, + }), + vmap({ + { u"name"_s, u"Reboot"_s }, + { u"icon"_s, u"cached"_s }, + { u"description"_s, u"Reboot the system"_s }, + { u"command"_s, QStringList{ u"systemctl"_s, u"reboot"_s } }, + { u"dangerous"_s, true }, + }), + vmap({ + { u"name"_s, u"Logout"_s }, + { u"icon"_s, u"exit_to_app"_s }, + { u"description"_s, u"Log out of the current session"_s }, + { u"command"_s, QStringList{ u"loginctl"_s, u"terminate-user"_s, u""_s } }, + { u"dangerous"_s, true }, + }), + vmap({ + { u"name"_s, u"Lock"_s }, + { u"icon"_s, u"lock"_s }, + { u"description"_s, u"Lock the current session"_s }, + { u"command"_s, QStringList{ u"loginctl"_s, u"lock-session"_s } }, + }), + vmap({ + { u"name"_s, u"Sleep"_s }, + { u"icon"_s, u"bedtime"_s }, + { u"description"_s, u"Suspend then hibernate"_s }, + { u"command"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, + }), + vmap({ + { u"name"_s, u"Settings"_s }, + { u"icon"_s, u"settings"_s }, + { u"description"_s, u"Configure the shell"_s }, + { u"command"_s, QStringList{ u"caelestia"_s, u"shell"_s, u"controlCenter"_s, u"open"_s } }, + }), + }) + +public: + explicit LauncherConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_useFuzzy(new LauncherUseFuzzy(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp new file mode 100644 index 000000000..0d8aa6ba7 --- /dev/null +++ b/plugin/src/Caelestia/Config/lockconfig.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class LockConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, recolourLogo, false) + CONFIG_GLOBAL_PROPERTY(bool, enableFprint, true) + CONFIG_GLOBAL_PROPERTY(int, maxFprintTries, 3) + CONFIG_PROPERTY(bool, hideNotifs, false) + +public: + explicit LockConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp new file mode 100644 index 000000000..fb1745ec6 --- /dev/null +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp @@ -0,0 +1,64 @@ +#include "monitorconfigmanager.hpp" +#include "config.hpp" +#include "tokens.hpp" + +#include + +namespace caelestia::config { + +namespace { + +QString monitorConfigDir(const QString& screen) { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + + QStringLiteral("/caelestia/monitors/") + screen + QStringLiteral("/"); +} + +} // namespace + +MonitorConfigManager::MonitorConfigManager(QObject* parent) + : QObject(parent) {} + +MonitorConfigManager* MonitorConfigManager::instance() { + static MonitorConfigManager instance; + return &instance; +} + +MonitorConfigManager* MonitorConfigManager::create(QQmlEngine*, QJSEngine*) { + QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); + return instance(); +} + +GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) { + auto& overlay = m_overlays[screen]; + if (!overlay.config) { + auto dir = monitorConfigDir(screen); + overlay.config = new GlobalConfig(GlobalConfig::instance(), dir + QStringLiteral("shell.json"), screen, this); + + auto* const global = GlobalConfig::instance(); + connect(overlay.config, &GlobalConfig::loaded, global, &GlobalConfig::loaded); + connect(overlay.config, &GlobalConfig::saved, global, &GlobalConfig::saved); + connect(overlay.config, &GlobalConfig::loadFailed, global, &GlobalConfig::loadFailed); + connect(overlay.config, &GlobalConfig::saveFailed, global, &GlobalConfig::saveFailed); + connect(overlay.config, &GlobalConfig::unknownOption, global, &GlobalConfig::unknownOption); + } + return overlay.config; +} + +TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) { + auto& overlay = m_overlays[screen]; + if (!overlay.tokens) { + auto dir = monitorConfigDir(screen); + overlay.tokens = + new TokenConfig(TokenConfig::instance(), dir + QStringLiteral("shell-tokens.json"), screen, this); + + auto* const global = TokenConfig::instance(); + connect(overlay.tokens, &TokenConfig::loaded, global, &TokenConfig::loaded); + connect(overlay.tokens, &TokenConfig::saved, global, &TokenConfig::saved); + connect(overlay.tokens, &TokenConfig::loadFailed, global, &TokenConfig::loadFailed); + connect(overlay.tokens, &TokenConfig::saveFailed, global, &TokenConfig::saveFailed); + connect(overlay.tokens, &TokenConfig::unknownOption, global, &TokenConfig::unknownOption); + } + return overlay.tokens; +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.hpp b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp new file mode 100644 index 000000000..70bbdc0d3 --- /dev/null +++ b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp @@ -0,0 +1,35 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::config { + +class GlobalConfig; +class TokenConfig; + +class MonitorConfigManager : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + static MonitorConfigManager* instance(); + static MonitorConfigManager* create(QQmlEngine*, QJSEngine*); + + [[nodiscard]] Q_INVOKABLE GlobalConfig* configForScreen(const QString& screen); + [[nodiscard]] Q_INVOKABLE TokenConfig* tokensForScreen(const QString& screen); + +private: + explicit MonitorConfigManager(QObject* parent = nullptr); + + struct ScreenOverlay { + GlobalConfig* config = nullptr; + TokenConfig* tokens = nullptr; + }; + + QHash m_overlays; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/notifsconfig.hpp b/plugin/src/Caelestia/Config/notifsconfig.hpp new file mode 100644 index 000000000..cdac94bdf --- /dev/null +++ b/plugin/src/Caelestia/Config/notifsconfig.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include "configobject.hpp" + +#include + +namespace caelestia::config { + +class NotifsConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(bool, expire, true) + CONFIG_GLOBAL_PROPERTY(QString, fullscreen, QStringLiteral("on")) + CONFIG_GLOBAL_PROPERTY(int, defaultExpireTimeout, 5000) + CONFIG_GLOBAL_PROPERTY(int, fullscreenExpireTimeout, 2000) + CONFIG_PROPERTY(qreal, clearThreshold, 0.3) + CONFIG_PROPERTY(int, expandThreshold, 20) + CONFIG_GLOBAL_PROPERTY(bool, actionOnClick, false) + CONFIG_PROPERTY(int, groupPreviewNum, 3) + CONFIG_PROPERTY(bool, openExpanded, false) + +public: + explicit NotifsConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/osdconfig.hpp b/plugin/src/Caelestia/Config/osdconfig.hpp new file mode 100644 index 000000000..18770294f --- /dev/null +++ b/plugin/src/Caelestia/Config/osdconfig.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class OsdConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, hideDelay, 2000) + CONFIG_PROPERTY(bool, enableBrightness, true) + CONFIG_PROPERTY(bool, enableMicrophone, false) + +public: + explicit OsdConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp new file mode 100644 index 000000000..9e976ab2a --- /dev/null +++ b/plugin/src/Caelestia/Config/rootconfig.cpp @@ -0,0 +1,258 @@ +#include "rootconfig.hpp" + +#include +#include +#include +#include +#include +#include + +namespace caelestia::config { + +namespace { + +QString watchRoot() { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); +} + +} // namespace + +RootConfig::RootConfig(QObject* parent) + : ConfigObject(parent) {} + +bool RootConfig::recentlySaved() const { + return m_recentlySaved; +} + +QStringList RootConfig::collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json) { + QStringList unknown; + const auto* meta = obj->metaObject(); + + QSet known; + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) + known.insert(QString::fromUtf8(meta->property(i).name())); + + for (auto it = json.begin(); it != json.end(); ++it) { + if (!known.contains(it.key())) { + unknown.append(it.key()); + } else if (it.value().isObject()) { + int idx = meta->indexOfProperty(it.key().toUtf8().constData()); + if (idx >= 0) { + auto prop = meta->property(idx); + auto value = prop.read(obj); + auto* subObj = value.value(); + if (subObj) { + const auto subUnknown = collectUnknownKeys(subObj, it.value().toObject()); + for (const auto& subKey : subUnknown) + unknown.append(it.key() + QStringLiteral(".") + subKey); + } + } + } + } + + return unknown; +} + +void RootConfig::setupFileBackend(const QString& path, const QString& screen) { + m_filePath = path; + m_screen = screen; + + m_watcher = new QFileSystemWatcher(this); + m_saveTimer = new QTimer(this); + m_cooldownTimer = new QTimer(this); + m_retryTimer = new QTimer(this); + + m_retryTimer->setSingleShot(true); + m_retryTimer->setInterval(50); + connect(m_retryTimer, &QTimer::timeout, this, &RootConfig::reload); + + m_saveTimer->setSingleShot(true); + m_saveTimer->setInterval(500); + connect(m_saveTimer, &QTimer::timeout, this, [this] { + QDir().mkpath(QFileInfo(m_filePath).absolutePath()); + + QFile file(m_filePath); + if (!file.open(QIODevice::WriteOnly)) { + auto err = QStringLiteral("Failed to write %1: %2").arg(m_filePath, file.errorString()); + qCWarning(lcConfig, "%s", qUtf8Printable(err)); + emit saveFailed(err, m_screen); + return; + } + + auto json = toJsonObject(); + file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); + file.close(); + + // Update watches — save may have created directories + updateWatch(); + + emit saved(m_screen); + }); + + m_cooldownTimer->setSingleShot(true); + m_cooldownTimer->setInterval(2000); + connect(m_cooldownTimer, &QTimer::timeout, this, [this] { + m_recentlySaved = false; + }); + + m_reloadDebounce = new QTimer(this); + m_reloadDebounce->setSingleShot(true); + m_reloadDebounce->setInterval(50); + connect(m_reloadDebounce, &QTimer::timeout, this, &RootConfig::reload); + + // Auto-save when any property changes (debounced by the save timer) + connectAutoSave(this); + + connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RootConfig::onWatcherEvent); + connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onWatcherEvent); + + qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; + + updateWatch(); + + // Load immediately so values are available during construction. + // Defer signal emissions to next event loop tick so QML has time to connect. + auto result = reloadFromFile(); + QTimer::singleShot(0, this, [this, result] { + emitLoadSignals(result, false); + }); +} + +void RootConfig::connectAutoSave(ConfigObject* obj) { + connect(obj, &ConfigObject::propertiesChanged, this, [this] { + if (!m_loading) + saveToFile(); + }); + + // Recurse into sub-objects + const auto* meta = obj->metaObject(); + for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { + auto prop = meta->property(i); + auto value = prop.read(obj); + auto* subObj = value.value(); + if (subObj) + connectAutoSave(subObj); + } +} + +void RootConfig::updateWatch() { + auto targetDir = QFileInfo(m_filePath).absolutePath(); + + // Find the nearest existing directory, walking up toward the watch root + auto dir = targetDir; + while (!QFile::exists(dir) && dir != watchRoot() && !dir.isEmpty()) { + auto parent = QFileInfo(dir).absolutePath(); + if (parent == dir) + break; // reached filesystem root + dir = parent; + } + + // Update directory watch if it changed + if (dir != m_watchedDir) { + if (!m_watchedDir.isEmpty()) + m_watcher->removePath(m_watchedDir); + + m_watchedDir = dir; + + if (QFile::exists(dir)) + m_watcher->addPath(dir); + } + + // Watch the file itself if it exists (for in-place modifications) + if (QFile::exists(m_filePath)) { + if (!m_watcher->files().contains(m_filePath)) + m_watcher->addPath(m_filePath); + } +} + +void RootConfig::onWatcherEvent() { + // Re-evaluate what to watch — directories may have been created or deleted + updateWatch(); + + if (!m_recentlySaved) + m_reloadDebounce->start(); +} + +void RootConfig::saveToFile() { + if (!m_saveTimer) + return; + m_saveTimer->start(); + m_recentlySaved = true; + m_cooldownTimer->start(); +} + +std::optional RootConfig::reloadFromFile() { + QFile file(m_filePath); + + if (!file.exists()) { + qCDebug(lcConfig) << "File does not exist:" << m_filePath; + return std::nullopt; + } + + if (!file.open(QIODevice::ReadOnly)) { + auto err = QStringLiteral("Failed to open %1: %2").arg(m_filePath, file.errorString()); + qCDebug(lcConfig, "%s", qUtf8Printable(err)); + return err; + } + + QJsonParseError error{}; + auto doc = QJsonDocument::fromJson(file.readAll(), &error); + + if (error.error != QJsonParseError::NoError) { + if (m_retryTimer && m_parseRetries < 3) { + m_parseRetries++; + qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath), + qUtf8Printable(error.errorString()), m_parseRetries); + m_retryTimer->start(); + return std::nullopt; // pending retry — no signal + } + + qCWarning(lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); + m_parseRetries = 0; + return QStringLiteral("JSON parse error: %1").arg(error.errorString()); + } + + m_parseRetries = 0; + + qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; + + m_loading = true; + + clearLoadedKeys(); + + auto jsonObj = doc.object(); + loadFromJson(jsonObj); + + m_loading = false; + + // Collect unknown keys — caller is responsible for emitting signals + m_lastUnknownKeys = collectUnknownKeys(this, jsonObj); + + return QString(); // success +} + +void RootConfig::save() { + saveToFile(); +} + +void RootConfig::emitLoadSignals(const std::optional& result, bool emitLoaded) { + if (!result.has_value()) + return; + + for (const auto& key : std::as_const(m_lastUnknownKeys)) + emit unknownOption(key, m_screen); + m_lastUnknownKeys.clear(); + + if (result->isEmpty()) { + if (emitLoaded) + emit loaded(m_screen); + } else { + emit loadFailed(*result, m_screen); + } +} + +void RootConfig::reload() { + emitLoadSignals(reloadFromFile()); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp new file mode 100644 index 000000000..bf9b8e2c3 --- /dev/null +++ b/plugin/src/Caelestia/Config/rootconfig.hpp @@ -0,0 +1,59 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +// Intermediate base for singleton config roots (GlobalConfig, TokenConfig). +// Provides file-backed persistence, save/reload, and lifecycle signals. +class RootConfig : public ConfigObject { + Q_OBJECT + +public: + explicit RootConfig(QObject* parent = nullptr); + + void setupFileBackend(const QString& path, const QString& screen = {}); + void saveToFile(); + // Returns nullopt if retrying, empty string on success, error message on failure. + [[nodiscard]] std::optional reloadFromFile(); + + [[nodiscard]] bool recentlySaved() const; + + Q_INVOKABLE void save(); + Q_INVOKABLE void reload(); + +signals: + void loaded(const QString& screen); + void loadFailed(const QString& error, const QString& screen); + void saved(const QString& screen); + void saveFailed(const QString& error, const QString& screen); + void unknownOption(const QString& key, const QString& screen); + +private: + static QStringList collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json); + void emitLoadSignals(const std::optional& result, bool emitLoaded = true); + void updateWatch(); + void onWatcherEvent(); + + void connectAutoSave(ConfigObject* obj); + + QString m_filePath; + QString m_screen; + QString m_watchedDir; + bool m_recentlySaved = false; + bool m_loading = false; + + QFileSystemWatcher* m_watcher = nullptr; + QTimer* m_saveTimer = nullptr; + QTimer* m_cooldownTimer = nullptr; + QTimer* m_retryTimer = nullptr; + QTimer* m_reloadDebounce = nullptr; + int m_parseRetries = 0; + QStringList m_lastUnknownKeys; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp new file mode 100644 index 000000000..b97c69c1e --- /dev/null +++ b/plugin/src/Caelestia/Config/serviceconfig.hpp @@ -0,0 +1,43 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include + +namespace caelestia::config { + +using Qt::StringLiterals::operator""_s; + +class ServiceConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(QString, weatherLocation) + // Guess based on locale + CONFIG_GLOBAL_PROPERTY(bool, useFahrenheit, + QLocale().measurementSystem() == QLocale::ImperialUSSystem || + QLocale().measurementSystem() == QLocale::ImperialUKSystem) + // This is always false by default cause apparently even imperial system users don't use it for perf temps? + CONFIG_GLOBAL_PROPERTY(bool, useFahrenheitPerformance, false) + // Attempt to guess based on locale + CONFIG_GLOBAL_PROPERTY( + bool, useTwelveHourClock, QLocale().timeFormat(QLocale::ShortFormat).toLower().contains(u"a"_s)) + CONFIG_GLOBAL_PROPERTY(QString, gpuType) + CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 45) + CONFIG_GLOBAL_PROPERTY(qreal, audioIncrement, 0.1) + CONFIG_GLOBAL_PROPERTY(qreal, brightnessIncrement, 0.1) + CONFIG_GLOBAL_PROPERTY(qreal, maxVolume, 1.0) + CONFIG_GLOBAL_PROPERTY(bool, smartScheme, true) + CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, u"Spotify"_s) + CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases, + { vmap({ { u"from"_s, u"com.github.th_ch.youtube_music"_s }, { u"to"_s, u"YT Music"_s } }) }) + CONFIG_GLOBAL_PROPERTY(bool, showLyrics, false) + CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, u"Auto"_s) + +public: + explicit ServiceConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/sessionconfig.hpp b/plugin/src/Caelestia/Config/sessionconfig.hpp new file mode 100644 index 000000000..7e7548f1b --- /dev/null +++ b/plugin/src/Caelestia/Config/sessionconfig.hpp @@ -0,0 +1,57 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include + +namespace caelestia::config { + +using Qt::StringLiterals::operator""_s; + +class SessionIcons : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(QString, logout, u"logout"_s) + CONFIG_PROPERTY(QString, shutdown, u"power_settings_new"_s) + CONFIG_PROPERTY(QString, hibernate, u"downloading"_s) + CONFIG_PROPERTY(QString, reboot, u"cached"_s) + +public: + explicit SessionIcons(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SessionCommands : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(QStringList, logout, { u"loginctl"_s, u"terminate-user"_s, u""_s }) + CONFIG_PROPERTY(QStringList, shutdown, { u"systemctl"_s, u"poweroff"_s }) + CONFIG_PROPERTY(QStringList, hibernate, { u"systemctl"_s, u"hibernate"_s }) + CONFIG_PROPERTY(QStringList, reboot, { u"systemctl"_s, u"reboot"_s }) + +public: + explicit SessionCommands(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SessionConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, dragThreshold, 30) + CONFIG_PROPERTY(bool, vimKeybinds, false) + CONFIG_SUBOBJECT(SessionIcons, icons) + CONFIG_SUBOBJECT(SessionCommands, commands) + +public: + explicit SessionConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_icons(new SessionIcons(this)) + , m_commands(new SessionCommands(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/sidebarconfig.hpp b/plugin/src/Caelestia/Config/sidebarconfig.hpp new file mode 100644 index 000000000..4460872e4 --- /dev/null +++ b/plugin/src/Caelestia/Config/sidebarconfig.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +class SidebarConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, dragThreshold, 80) + +public: + explicit SidebarConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp new file mode 100644 index 000000000..6d171cb35 --- /dev/null +++ b/plugin/src/Caelestia/Config/tokens.cpp @@ -0,0 +1,54 @@ +#include "tokens.hpp" +#include "monitorconfigmanager.hpp" + +#include +#include + +namespace caelestia::config { + +namespace { + +QString configDir() { + return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); +} + +} // namespace + +TokenConfig::TokenConfig(QObject* parent) + : RootConfig(parent) + , m_appearance(new AppearanceTokens(this)) + , m_sizes(new SizeTokens(this)) { + setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); +} + +TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) + : RootConfig(parent) + , m_appearance(new AppearanceTokens(this)) + , m_sizes(new SizeTokens(this)) { + if (!filePath.isEmpty()) + setupFileBackend(filePath, screen); + if (fallback) + syncFromGlobal(fallback); +} + +TokenConfig* TokenConfig::instance() { + static TokenConfig instance; + return &instance; +} + +TokenConfig* TokenConfig::defaults() { + if (!m_defaults) + m_defaults = new TokenConfig(nullptr, QString(), QString(), this); + return m_defaults; +} + +TokenConfig* TokenConfig::forScreen(const QString& screen) { + return MonitorConfigManager::instance()->tokensForScreen(screen); +} + +TokenConfig* TokenConfig::create(QQmlEngine*, QJSEngine*) { + QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); + return instance(); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp new file mode 100644 index 000000000..28bac1f1a --- /dev/null +++ b/plugin/src/Caelestia/Config/tokens.hpp @@ -0,0 +1,351 @@ +#pragma once + +#include "rootconfig.hpp" + +#include +#include + +namespace caelestia::config { + +class AnimCurves : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(QList, emphasized) + CONFIG_GLOBAL_PROPERTY(QList, emphasizedAccel) + CONFIG_GLOBAL_PROPERTY(QList, emphasizedDecel) + CONFIG_GLOBAL_PROPERTY(QList, standard) + CONFIG_GLOBAL_PROPERTY(QList, standardAccel) + CONFIG_GLOBAL_PROPERTY(QList, standardDecel) + CONFIG_GLOBAL_PROPERTY(QList, expressiveFastSpatial) + CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultSpatial) + CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowSpatial) + CONFIG_GLOBAL_PROPERTY(QList, expressiveFastEffects) + CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultEffects) + CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowEffects) + +public: + explicit AnimCurves(QObject* parent = nullptr) + : ConfigObject(parent) + , m_emphasized({ 0.05, 0, 2.0 / 15.0, 0.06, 1.0 / 6.0, 0.4, 5.0 / 24.0, 0.82, 0.25, 1, 1, 1 }) + , m_emphasizedAccel({ 0.3, 0, 0.8, 0.15, 1, 1 }) + , m_emphasizedDecel({ 0.05, 0.7, 0.1, 1, 1, 1 }) + , m_standard({ 0.2, 0, 0, 1, 1, 1 }) + , m_standardAccel({ 0.3, 0, 1, 1, 1, 1 }) + , m_standardDecel({ 0, 0, 0, 1, 1, 1 }) + , m_expressiveFastSpatial({ 0.42, 1.67, 0.21, 0.9, 1, 1 }) + , m_expressiveDefaultSpatial({ 0.38, 1.21, 0.22, 1, 1, 1 }) + , m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) + , m_expressiveFastEffects({ 0.31, 0.94, 0.34, 1, 1, 1 }) + , m_expressiveDefaultEffects({ 0.34, 0.8, 0.34, 1, 1, 1 }) + , m_expressiveSlowEffects({ 0.34, 0.88, 0.34, 1, 1, 1 }) {} +}; + +class RoundingTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, extraSmall, 4) + CONFIG_PROPERTY(int, small, 12) + CONFIG_PROPERTY(int, normal, 17) + CONFIG_PROPERTY(int, large, 25) + CONFIG_PROPERTY(int, full, 1000) + +public: + explicit RoundingTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SpacingTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, small, 7) + CONFIG_PROPERTY(int, smaller, 10) + CONFIG_PROPERTY(int, normal, 12) + CONFIG_PROPERTY(int, larger, 15) + CONFIG_PROPERTY(int, large, 20) + +public: + explicit SpacingTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class PaddingTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, small, 5) + CONFIG_PROPERTY(int, smaller, 7) + CONFIG_PROPERTY(int, normal, 10) + CONFIG_PROPERTY(int, larger, 12) + CONFIG_PROPERTY(int, large, 15) + +public: + explicit PaddingTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class FontSizeTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, small, 11) + CONFIG_PROPERTY(int, smaller, 12) + CONFIG_PROPERTY(int, normal, 13) + CONFIG_PROPERTY(int, larger, 15) + CONFIG_PROPERTY(int, large, 18) + CONFIG_PROPERTY(int, extraLarge, 28) + +public: + explicit FontSizeTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class AnimDurationTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(int, small, 200) + CONFIG_GLOBAL_PROPERTY(int, normal, 400) + CONFIG_GLOBAL_PROPERTY(int, large, 600) + CONFIG_GLOBAL_PROPERTY(int, extraLarge, 1000) + CONFIG_GLOBAL_PROPERTY(int, expressiveFastSpatial, 350) + CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultSpatial, 500) + CONFIG_GLOBAL_PROPERTY(int, expressiveSlowSpatial, 650) + CONFIG_GLOBAL_PROPERTY(int, expressiveFastEffects, 150) + CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultEffects, 200) + CONFIG_GLOBAL_PROPERTY(int, expressiveSlowEffects, 300) + +public: + explicit AnimDurationTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class AppearanceTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_SUBOBJECT(AnimCurves, curves) + CONFIG_SUBOBJECT(RoundingTokens, rounding) + CONFIG_SUBOBJECT(SpacingTokens, spacing) + CONFIG_SUBOBJECT(PaddingTokens, padding) + CONFIG_SUBOBJECT(FontSizeTokens, fontSize) + CONFIG_SUBOBJECT(AnimDurationTokens, animDurations) + +public: + explicit AppearanceTokens(QObject* parent = nullptr) + : ConfigObject(parent) + , m_curves(new AnimCurves(this)) + , m_rounding(new RoundingTokens(this)) + , m_spacing(new SpacingTokens(this)) + , m_padding(new PaddingTokens(this)) + , m_fontSize(new FontSizeTokens(this)) + , m_animDurations(new AnimDurationTokens(this)) {} +}; + +class BarTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, innerWidth, 40) + CONFIG_PROPERTY(int, windowPreviewSize, 400) + CONFIG_PROPERTY(int, trayMenuWidth, 300) + CONFIG_PROPERTY(int, batteryWidth, 250) + CONFIG_PROPERTY(int, networkWidth, 320) + CONFIG_PROPERTY(int, kbLayoutWidth, 320) + +public: + explicit BarTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class DashboardTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, tabIndicatorHeight, 3) + CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) + CONFIG_PROPERTY(int, infoWidth, 200) + CONFIG_PROPERTY(int, infoIconSize, 25) + CONFIG_PROPERTY(int, dateTimeWidth, 110) + CONFIG_PROPERTY(int, mediaWidth, 200) + CONFIG_PROPERTY(int, mediaProgressSweep, 180) + CONFIG_PROPERTY(int, mediaProgressThickness, 8) + CONFIG_PROPERTY(int, resourceProgressThickness, 10) + CONFIG_PROPERTY(int, weatherWidth, 250) + CONFIG_PROPERTY(int, mediaCoverArtSize, 150) + CONFIG_PROPERTY(int, mediaVisualiserSize, 80) + CONFIG_PROPERTY(int, resourceSize, 200) + +public: + explicit DashboardTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class LauncherTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, itemWidth, 600) + CONFIG_PROPERTY(int, itemHeight, 57) + CONFIG_PROPERTY(int, wallpaperWidth, 280) + CONFIG_PROPERTY(int, wallpaperHeight, 200) + +public: + explicit LauncherTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class NotifsTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, width, 400) + CONFIG_GLOBAL_PROPERTY(int, image, 41) + CONFIG_PROPERTY(int, badge, 20) + +public: + explicit NotifsTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class OsdTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, sliderWidth, 30) + CONFIG_PROPERTY(int, sliderHeight, 150) + +public: + explicit OsdTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SessionTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, button, 80) + +public: + explicit SessionTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SidebarTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, width, 430) + +public: + explicit SidebarTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class UtilitiesTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(int, width, 430) + CONFIG_PROPERTY(int, toastWidth, 430) + +public: + explicit UtilitiesTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class LockTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, heightMult, 0.7) + CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) + CONFIG_PROPERTY(int, centerWidth, 600) + +public: + explicit LockTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class WInfoTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, heightMult, 0.7) + CONFIG_PROPERTY(qreal, detailsWidth, 500) + +public: + explicit WInfoTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class ControlCenterTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(qreal, heightMult, 0.7) + CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) + +public: + explicit ControlCenterTokens(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class SizeTokens : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_SUBOBJECT(BarTokens, bar) + CONFIG_SUBOBJECT(DashboardTokens, dashboard) + CONFIG_SUBOBJECT(LauncherTokens, launcher) + CONFIG_SUBOBJECT(NotifsTokens, notifs) + CONFIG_SUBOBJECT(OsdTokens, osd) + CONFIG_SUBOBJECT(SessionTokens, session) + CONFIG_SUBOBJECT(SidebarTokens, sidebar) + CONFIG_SUBOBJECT(UtilitiesTokens, utilities) + CONFIG_SUBOBJECT(LockTokens, lock) + CONFIG_SUBOBJECT(WInfoTokens, winfo) + CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter) + +public: + explicit SizeTokens(QObject* parent = nullptr) + : ConfigObject(parent) + , m_bar(new BarTokens(this)) + , m_dashboard(new DashboardTokens(this)) + , m_launcher(new LauncherTokens(this)) + , m_notifs(new NotifsTokens(this)) + , m_osd(new OsdTokens(this)) + , m_session(new SessionTokens(this)) + , m_sidebar(new SidebarTokens(this)) + , m_utilities(new UtilitiesTokens(this)) + , m_lock(new LockTokens(this)) + , m_winfo(new WInfoTokens(this)) + , m_controlCenter(new ControlCenterTokens(this)) {} +}; + +class TokenConfig : public RootConfig { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + + CONFIG_SUBOBJECT(AppearanceTokens, appearance) + CONFIG_SUBOBJECT(SizeTokens, sizes) + +public: + static TokenConfig* instance(); + [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); + [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); + static TokenConfig* create(QQmlEngine*, QJSEngine*); + +private: + friend class MonitorConfigManager; + explicit TokenConfig(QObject* parent = nullptr); + explicit TokenConfig( + TokenConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); + + TokenConfig* m_defaults = nullptr; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp new file mode 100644 index 000000000..16f863b8c --- /dev/null +++ b/plugin/src/Caelestia/Config/tokensattached.cpp @@ -0,0 +1,118 @@ +#include "tokensattached.hpp" +#include "anim.hpp" +#include "appearanceconfig.hpp" +#include "config.hpp" +#include "monitorconfigmanager.hpp" +#include "tokens.hpp" + +#include + +namespace caelestia::config { + +namespace { + +const AppearanceConfig* resolveAppearance(GlobalConfig* config, bool complete, const char* prop, QObject* parent) { + if (config) + return config->appearance(); + if ((complete || !qobject_cast(parent)) && parent) + qCWarning(lcConfig, "Tokens.%s accessed without a screen set on %s", prop, parent->metaObject()->className()); + return GlobalConfig::instance()->appearance(); +} + +} // namespace + +Tokens::Tokens(QObject* parent) + : QQuickAttachedPropertyPropagator(parent) + , m_anim(new AnimTokens(this)) { + bindAnim(); + initialize(); +} + +void Tokens::classBegin() {} + +void Tokens::componentComplete() { + m_complete = true; +} + +QString Tokens::screen() const { + return m_screen; +} + +void Tokens::inheritScreen(const QString& screen) { + if (screen == m_screen) + return; + + m_screen = screen; + + if (m_screen.isEmpty()) { + m_config = nullptr; + m_tokens = nullptr; + } else { + m_config = MonitorConfigManager::instance()->configForScreen(m_screen); + m_tokens = MonitorConfigManager::instance()->tokensForScreen(m_screen); + } + + propagateScreen(); + emit sourceChanged(); +} + +void Tokens::propagateScreen() { + const auto children = attachedChildren(); + for (auto* const child : children) { + auto* const tokens = qobject_cast(child); + if (tokens) + tokens->inheritScreen(m_screen); + } +} + +void Tokens::attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { + Q_UNUSED(oldParent); + auto* const tokens = qobject_cast(newParent); + if (tokens) + inheritScreen(tokens->screen()); +} + +void Tokens::bindAnim() { + m_anim->bindDurations(GlobalConfig::instance()->appearance()->anim()->durations()); + m_anim->bindCurves(TokenConfig::instance()->appearance()->curves()); +} + +#define TOKENS_ATTACHED_GETTER(Type, name) \ + const Type* Tokens::name() const { \ + auto* a = resolveAppearance(m_config, m_complete, #name, parent()); \ + return a ? a->name() : nullptr; \ + } + +TOKENS_ATTACHED_GETTER(AppearanceRounding, rounding) +TOKENS_ATTACHED_GETTER(AppearanceSpacing, spacing) +TOKENS_ATTACHED_GETTER(AppearancePadding, padding) +TOKENS_ATTACHED_GETTER(AppearanceFont, font) + +#undef TOKENS_ATTACHED_GETTER + +const AppearanceTransparency* Tokens::transparency() const { + return GlobalConfig::instance()->appearance()->transparency(); // Transparency is always global +} + +const SizeTokens* Tokens::sizes() const { + if (m_tokens) + return m_tokens->sizes(); + if ((m_complete || !qobject_cast(parent())) && parent()) + qCWarning(lcConfig, "Tokens.sizes accessed without a screen set on %s", parent()->metaObject()->className()); + return TokenConfig::instance()->sizes(); +} + +const AnimTokens* Tokens::anim() const { + return m_anim; +} + +TokenConfig* Tokens::forScreen(const QString& screen) { + return TokenConfig::forScreen(screen); +} + +Tokens* Tokens::qmlAttachedProperties(QObject* object) { + return new Tokens(object); +} + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp new file mode 100644 index 000000000..810b0c0de --- /dev/null +++ b/plugin/src/Caelestia/Config/tokensattached.hpp @@ -0,0 +1,78 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::config { + +class AnimTokens; +class AppearanceFont; +class AppearancePadding; +class AppearanceRounding; +class AppearanceSpacing; +class AppearanceTransparency; +class GlobalConfig; +class SizeTokens; +class TokenConfig; + +class Tokens : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { + Q_OBJECT + Q_INTERFACES(QQmlParserStatus) + QML_ELEMENT + QML_UNCREATABLE("") + QML_ATTACHED(Tokens) + Q_MOC_INCLUDE("anim.hpp") + Q_MOC_INCLUDE("appearanceconfig.hpp") + Q_MOC_INCLUDE("tokens.hpp") + + Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) + Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) + +public: + explicit Tokens(QObject* parent = nullptr); + + [[nodiscard]] QString screen() const; + void inheritScreen(const QString& screen); + + [[nodiscard]] const AppearanceRounding* rounding() const; + [[nodiscard]] const AppearanceSpacing* spacing() const; + [[nodiscard]] const AppearancePadding* padding() const; + [[nodiscard]] const AppearanceFont* font() const; + [[nodiscard]] const AppearanceTransparency* transparency() const; + + [[nodiscard]] const SizeTokens* sizes() const; + [[nodiscard]] const AnimTokens* anim() const; + + [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); + + static Tokens* qmlAttachedProperties(QObject* object); + +signals: + void sourceChanged(); + +protected: + void attachedParentChange( + QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; + +private: + void classBegin() override; + void componentComplete() override; + + void propagateScreen(); + void bindAnim(); + + bool m_complete = false; + QString m_screen; + GlobalConfig* m_config = nullptr; + TokenConfig* m_tokens = nullptr; + AnimTokens* m_anim = nullptr; +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/userpaths.hpp b/plugin/src/Caelestia/Config/userpaths.hpp new file mode 100644 index 000000000..da6973a78 --- /dev/null +++ b/plugin/src/Caelestia/Config/userpaths.hpp @@ -0,0 +1,30 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include +#include + +namespace caelestia::config { + +using Qt::StringLiterals::operator""_s; + +class UserPaths : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY( + QString, wallpaperDir, QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + u"/Wallpapers"_s) + CONFIG_GLOBAL_PROPERTY(QString, lyricsDir, QDir::homePath() + u"/Music/lyrics/"_s) + CONFIG_PROPERTY(QString, sessionGif, u"root:/assets/kurukuru.gif"_s) + CONFIG_PROPERTY(QString, mediaGif, u"root:/assets/bongocat.gif"_s) + CONFIG_PROPERTY(QString, noNotifsPic, u"root:/assets/dino.png"_s) + CONFIG_PROPERTY(QString, lockNoNotifsPic, u"root:/assets/dino.png"_s) + +public: + explicit UserPaths(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/utilitiesconfig.hpp b/plugin/src/Caelestia/Config/utilitiesconfig.hpp new file mode 100644 index 000000000..153f21d5c --- /dev/null +++ b/plugin/src/Caelestia/Config/utilitiesconfig.hpp @@ -0,0 +1,88 @@ +#pragma once + +#include "configobject.hpp" + +#include +#include + +namespace caelestia::config { + +using Qt::StringLiterals::operator""_s; + +class UtilitiesToasts : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(QString, fullscreen, u"off"_s) + CONFIG_GLOBAL_PROPERTY(bool, configLoaded, true) + CONFIG_GLOBAL_PROPERTY(bool, chargingChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, gameModeChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, dndChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, audioOutputChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, audioInputChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, capsLockChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, numLockChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, kbLayoutChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, kbLimit, true) + CONFIG_GLOBAL_PROPERTY(bool, vpnChanged, true) + CONFIG_GLOBAL_PROPERTY(bool, nowPlaying, false) + +public: + explicit UtilitiesToasts(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class UtilitiesVpn : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_GLOBAL_PROPERTY(bool, enabled, false) + CONFIG_GLOBAL_PROPERTY(QVariantList, provider) + +public: + explicit UtilitiesVpn(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class UtilitiesRecording : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(QString, videoMode, u"fullscreen"_s) + CONFIG_PROPERTY(bool, recordSystem, true) + CONFIG_PROPERTY(bool, recordMicrophone, false) + +public: + explicit UtilitiesRecording(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +class UtilitiesConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + + CONFIG_PROPERTY(bool, enabled, true) + CONFIG_PROPERTY(int, maxToasts, 4) + CONFIG_SUBOBJECT(UtilitiesToasts, toasts) + CONFIG_SUBOBJECT(UtilitiesVpn, vpn) + CONFIG_SUBOBJECT(UtilitiesRecording, recording) + CONFIG_PROPERTY(QVariantList, quickToggles, + { + vmap({ { u"id"_s, u"wifi"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"bluetooth"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"mic"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"settings"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"gameMode"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"dnd"_s }, { u"enabled"_s, true } }), + vmap({ { u"id"_s, u"vpn"_s }, { u"enabled"_s, false } }), + }) + +public: + explicit UtilitiesConfig(QObject* parent = nullptr) + : ConfigObject(parent) + , m_toasts(new UtilitiesToasts(this)) + , m_vpn(new UtilitiesVpn(this)) + , m_recording(new UtilitiesRecording(this)) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Config/winfoconfig.hpp b/plugin/src/Caelestia/Config/winfoconfig.hpp new file mode 100644 index 000000000..30d4f5e6e --- /dev/null +++ b/plugin/src/Caelestia/Config/winfoconfig.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include "configobject.hpp" + +namespace caelestia::config { + +// WInfoConfig has no serialized properties (serializer returns {}) +// All properties are in AdvancedConfig.winfo +class WInfoConfig : public ConfigObject { + Q_OBJECT + QML_ANONYMOUS + +public: + explicit WInfoConfig(QObject* parent = nullptr) + : ConfigObject(parent) {} +}; + +} // namespace caelestia::config diff --git a/plugin/src/Caelestia/Images/CMakeLists.txt b/plugin/src/Caelestia/Images/CMakeLists.txt new file mode 100644 index 000000000..d869667d8 --- /dev/null +++ b/plugin/src/Caelestia/Images/CMakeLists.txt @@ -0,0 +1,11 @@ +qml_module(caelestia-images + URI Caelestia.Images + SOURCES + cachingimageprovider.cpp + imagecacher.cpp + iutils.cpp + LIBRARIES + Qt::Gui + Qt::Quick + Qt::Concurrent +) diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp new file mode 100644 index 000000000..768e05dae --- /dev/null +++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp @@ -0,0 +1,120 @@ +#include "cachingimageprovider.hpp" + +#include "imagecacher.hpp" + +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg) + +namespace caelestia::images { + +namespace { + +class CachingImageResponse final : public QQuickImageResponse, public QRunnable { +public: + CachingImageResponse(const QString& id, const QSize& requestedSize, ImageCacher::FillMode fillMode) + : m_id(id) + , m_requestedSize(requestedSize) + , m_fillMode(fillMode) { + setAutoDelete(false); + } + + [[nodiscard]] QQuickTextureFactory* textureFactory() const override { + return QQuickTextureFactory::textureFactoryForImage(m_image); + } + + [[nodiscard]] QString errorString() const override { return m_error; } + + void run() override { + process(); + emit finished(); + } + +private: + void process() { + QString path = QString::fromUtf8(m_id.toUtf8().percentDecoded()); + if (!path.startsWith(QLatin1Char('/'))) + path.prepend(QLatin1Char('/')); + + if (!QFileInfo::exists(path)) { + m_error = QStringLiteral("Source file does not exist: ") + path; + qCWarning(lcCProv).noquote() << m_error; + return; + } + + QSize size = m_requestedSize; + const bool needsW = size.width() <= 0; + const bool needsH = size.height() <= 0; + + // If both dimensions are missing, return the original directly + if (needsW && needsH) { + qCDebug(lcCProv).noquote() << "Given source size is invalid, returning original:" << path; + m_image = QImage(path); + if (m_image.isNull()) { + m_error = QStringLiteral("Failed to decode source: ") + path; + qCWarning(lcCProv).noquote() << m_error; + } + return; + } + + // If one dimension is missing, derive it from the source aspect ratio + if (needsW || needsH) { + const QImageReader sourceReader(path); + const QSize sourceSize = sourceReader.size(); + if (!sourceSize.isValid() || sourceSize.isEmpty()) { + m_error = QStringLiteral("Could not determine source size for: ") + path; + qCWarning(lcCProv).noquote() << m_error; + return; + } + + if (needsW) + size.setWidth(qRound(size.height() * sourceSize.width() / static_cast(sourceSize.height()))); + else + size.setHeight(qRound(size.width() * sourceSize.height() / static_cast(sourceSize.width()))); + } + + // Try to use cached image + const auto cachePath = ImageCacher::cachePathFor(path, size, m_fillMode); + if (!cachePath.isEmpty()) { + QImageReader cacheReader(cachePath); + if (cacheReader.canRead()) { + m_image = cacheReader.read(); + if (!m_image.isNull()) + return; + } + } + + // Schedule cache job (this call will return the original image, but later ones will use cache) + ImageCacher::instance()->schedule(path, cachePath, size, m_fillMode); + + m_image = QImage(path); + if (m_image.isNull()) { + m_error = QStringLiteral("Failed to decode source: ") + path; + qCWarning(lcCProv).noquote() << m_error; + } + } + + QString m_id; + QSize m_requestedSize; + ImageCacher::FillMode m_fillMode; + QImage m_image; + QString m_error; +}; + +} // namespace + +CachingImageProvider::CachingImageProvider(FillMode fillMode) + : m_fillMode(fillMode) {} + +QQuickImageResponse* CachingImageProvider::requestImageResponse(const QString& id, const QSize& requestedSize) { + auto* const response = new CachingImageResponse(id, requestedSize, m_fillMode); + QThreadPool::globalInstance()->start(response); + return response; +} + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.hpp b/plugin/src/Caelestia/Images/cachingimageprovider.hpp new file mode 100644 index 000000000..97082c765 --- /dev/null +++ b/plugin/src/Caelestia/Images/cachingimageprovider.hpp @@ -0,0 +1,21 @@ +#pragma once + +#include "imagecacher.hpp" + +#include + +namespace caelestia::images { + +class CachingImageProvider : public QQuickAsyncImageProvider { +public: + using FillMode = ImageCacher::FillMode; + + explicit CachingImageProvider(FillMode fillMode); + + QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override; + +private: + FillMode m_fillMode; +}; + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/imagecacher.cpp b/plugin/src/Caelestia/Images/imagecacher.cpp new file mode 100644 index 000000000..dfef8f0b8 --- /dev/null +++ b/plugin/src/Caelestia/Images/imagecacher.cpp @@ -0,0 +1,159 @@ +#include "imagecacher.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +Q_LOGGING_CATEGORY(lcCacher, "caelestia.images.cacher", QtInfoMsg) + +namespace caelestia::images { + +namespace { + +QString sha256sum(const QString& path) { + QFile file(path); + if (!file.open(QIODevice::ReadOnly)) { + qCWarning(lcCacher).noquote() << "sha256sum: failed to open" << path; + return {}; + } + + QCryptographicHash hash(QCryptographicHash::Sha256); + hash.addData(&file); + file.close(); + + return hash.result().toHex(); +} + +QString fillSuffix(ImageCacher::FillMode fillMode) { + switch (fillMode) { + case ImageCacher::FillMode::Crop: + return QStringLiteral("crop"); + case ImageCacher::FillMode::Fit: + return QStringLiteral("fit"); + default: + return QStringLiteral("stretch"); + } +} + +} // namespace + +const QString& ImageCacher::cacheDir() { + static const QString s_dir = [] { + QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); + if (cache.isEmpty()) + cache = QDir::homePath() + QStringLiteral("/.cache"); + return cache + QStringLiteral("/caelestia/imagecache"); + }(); + return s_dir; +} + +QString ImageCacher::cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode) { + const QString sha = sha256sum(sourcePath); + if (sha.isEmpty()) + return {}; + + const QString filename = + QStringLiteral("%1@%2x%3-%4.png") + .arg(sha, QString::number(size.width()), QString::number(size.height()), fillSuffix(fillMode)); + + return cacheDir() + QLatin1Char('/') + filename; +} + +ImageCacher* ImageCacher::instance() { + static ImageCacher s_instance; + return &s_instance; +} + +ImageCacher::ImageCacher(QObject* parent) + : QObject(parent) {} + +void ImageCacher::schedule(const QString& sourcePath, const QSize& size, FillMode fillMode) { + schedule(sourcePath, cachePathFor(sourcePath, size, fillMode), size, fillMode); +} + +void ImageCacher::schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { + if (cachePath.isEmpty()) + return; + + { + QMutexLocker locker(&m_mutex); + if (m_inflight.contains(cachePath)) + return; + m_inflight.insert(cachePath); + } + + QThreadPool::globalInstance()->start([this, sourcePath, cachePath, size, fillMode]() { + runJob(sourcePath, cachePath, size, fillMode); + QMutexLocker locker(&m_mutex); + m_inflight.remove(cachePath); + }); +} + +void ImageCacher::runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { + if (QFile::exists(cachePath)) { + return; + } + + QImage image(sourcePath); + if (image.isNull()) { + qCWarning(lcCacher).noquote() << "Failed to decode source" << sourcePath; + return; + } + + Qt::AspectRatioMode scaleMode; + switch (fillMode) { + case FillMode::Crop: + scaleMode = Qt::KeepAspectRatioByExpanding; + break; + case FillMode::Fit: + scaleMode = Qt::KeepAspectRatio; + break; + case FillMode::Stretch: + scaleMode = Qt::IgnoreAspectRatio; + break; + } + + image.convertTo(QImage::Format_ARGB32); + image = image.scaled(size, scaleMode, Qt::SmoothTransformation); + + if (image.isNull()) { + qCWarning(lcCacher).noquote() << "Failed to scale" << sourcePath; + return; + } + + QImage canvas; + if (fillMode == FillMode::Stretch) { + canvas = image; + } else { + canvas = QImage(size, QImage::Format_ARGB32); + canvas.fill(Qt::transparent); + + QPainter painter(&canvas); + painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); + painter.end(); + } + + const QString parent = QFileInfo(cachePath).absolutePath(); + if (!QDir().mkpath(parent)) { + qCWarning(lcCacher).noquote() << "Failed to create cache dir" << parent; + return; + } + + QSaveFile saveFile(cachePath); + if (!saveFile.open(QIODevice::WriteOnly) || !canvas.save(&saveFile, "PNG") || !saveFile.commit()) { + qCWarning( + lcCacher, "Failed to save to %s: %s", qUtf8Printable(cachePath), qUtf8Printable(saveFile.errorString())); + return; + } + + qCDebug(lcCacher).noquote() << "Saved to" << cachePath; +} + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/imagecacher.hpp b/plugin/src/Caelestia/Images/imagecacher.hpp new file mode 100644 index 000000000..796084193 --- /dev/null +++ b/plugin/src/Caelestia/Images/imagecacher.hpp @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::images { + +class ImageCacher : public QObject { + Q_OBJECT + +public: + enum class FillMode { + Crop, + Fit, + Stretch, + }; + + static ImageCacher* instance(); + + static const QString& cacheDir(); + static QString cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode); + + void schedule(const QString& sourcePath, const QSize& size, FillMode fillMode); + void schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); + +private: + explicit ImageCacher(QObject* parent = nullptr); + + static void runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); + + QMutex m_mutex; + QSet m_inflight; +}; + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/iutils.cpp b/plugin/src/Caelestia/Images/iutils.cpp new file mode 100644 index 000000000..996f93dd1 --- /dev/null +++ b/plugin/src/Caelestia/Images/iutils.cpp @@ -0,0 +1,42 @@ +#include "iutils.hpp" + +#include "cachingimageprovider.hpp" + +namespace caelestia::images { + +IUtils* IUtils::create(QQmlEngine* engine, QJSEngine* jsEngine) { + Q_UNUSED(jsEngine); + + engine->addImageProvider(QStringLiteral("ccache"), new CachingImageProvider(CachingImageProvider::FillMode::Crop)); + engine->addImageProvider(QStringLiteral("fcache"), new CachingImageProvider(CachingImageProvider::FillMode::Fit)); + engine->addImageProvider( + QStringLiteral("scache"), new CachingImageProvider(CachingImageProvider::FillMode::Stretch)); + + return new IUtils(engine); +} + +QUrl IUtils::urlForPath(const QString& path, int fillMode) { + if (path.isEmpty()) + return QUrl(); + + QString prefix; + switch (fillMode) { + case 1: // Image.PreserveAspectFit + prefix = QStringLiteral("fcache"); + break; + case 2: // Image.PreserveAspectCrop + prefix = QStringLiteral("ccache"); + break; + default: // Image.Stretch or any other ones + prefix = QStringLiteral("scache"); + break; + } + + QUrl url; + url.setScheme(QStringLiteral("image")); + url.setHost(prefix); + url.setPath(path.startsWith(QLatin1Char('/')) ? path : QLatin1Char('/') + path); + return url; +} + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Images/iutils.hpp b/plugin/src/Caelestia/Images/iutils.hpp new file mode 100644 index 000000000..2187c8d02 --- /dev/null +++ b/plugin/src/Caelestia/Images/iutils.hpp @@ -0,0 +1,24 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::images { + +class IUtils : public QObject { + Q_OBJECT + QML_ELEMENT + QML_SINGLETON + +public: + static IUtils* create(QQmlEngine* engine, QJSEngine* jsEngine); + + Q_INVOKABLE static QUrl urlForPath(const QString& path, int fillMode); + +private: + explicit IUtils(QObject* parent = nullptr) + : QObject(parent) {}; +}; + +} // namespace caelestia::images diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt index bdc58dbf7..f4bbc5fdc 100644 --- a/plugin/src/Caelestia/Internal/CMakeLists.txt +++ b/plugin/src/Caelestia/Internal/CMakeLists.txt @@ -1,15 +1,17 @@ qml_module(caelestia-internal URI Caelestia.Internal SOURCES - cachingimagemanager.hpp cachingimagemanager.cpp - circularindicatormanager.hpp circularindicatormanager.cpp - hyprdevices.hpp hyprdevices.cpp - hyprextras.hpp hyprextras.cpp - logindmanager.hpp logindmanager.cpp + arcgauge.cpp + circularbuffer.cpp + circularindicatormanager.cpp + hyprdevices.cpp + hyprextras.cpp + logindmanager.cpp + sparklineitem.cpp + visualiserbars.cpp LIBRARIES Qt::Gui Qt::Quick - Qt::Concurrent Qt::Network Qt::DBus ) diff --git a/plugin/src/Caelestia/Internal/arcgauge.cpp b/plugin/src/Caelestia/Internal/arcgauge.cpp new file mode 100644 index 000000000..d534f5f73 --- /dev/null +++ b/plugin/src/Caelestia/Internal/arcgauge.cpp @@ -0,0 +1,119 @@ +#include "arcgauge.hpp" + +#include +#include +#include + +namespace caelestia::internal { + +ArcGauge::ArcGauge(QQuickItem* parent) + : QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void ArcGauge::paint(QPainter* painter) { + const qreal w = width(); + const qreal h = height(); + const qreal side = qMin(w, h); + const qreal radius = (side - m_lineWidth - 2.0) / 2.0; + const qreal cx = w / 2.0; + const qreal cy = h / 2.0; + + const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0); + + // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees) + const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0); + const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0); + + painter->setRenderHint(QPainter::Antialiasing, true); + + // Draw track arc + QPen trackPen(m_trackColor, m_lineWidth); + trackPen.setCapStyle(Qt::RoundCap); + painter->setPen(trackPen); + painter->setBrush(Qt::NoBrush); + painter->drawArc(arcRect, startAngle16, sweepAngle16); + + // Draw value arc + if (m_percentage > 0.0) { + const int valueSweep16 = qRound(static_cast(sweepAngle16) * m_percentage); + QPen valuePen(m_accentColor, m_lineWidth); + valuePen.setCapStyle(Qt::RoundCap); + painter->setPen(valuePen); + painter->drawArc(arcRect, startAngle16, valueSweep16); + } +} + +qreal ArcGauge::percentage() const { + return m_percentage; +} + +void ArcGauge::setPercentage(qreal percentage) { + if (qFuzzyCompare(m_percentage, percentage)) + return; + m_percentage = percentage; + emit percentageChanged(); + update(); +} + +QColor ArcGauge::accentColor() const { + return m_accentColor; +} + +void ArcGauge::setAccentColor(const QColor& color) { + if (m_accentColor == color) + return; + m_accentColor = color; + emit accentColorChanged(); + update(); +} + +QColor ArcGauge::trackColor() const { + return m_trackColor; +} + +void ArcGauge::setTrackColor(const QColor& color) { + if (m_trackColor == color) + return; + m_trackColor = color; + emit trackColorChanged(); + update(); +} + +qreal ArcGauge::startAngle() const { + return m_startAngle; +} + +void ArcGauge::setStartAngle(qreal angle) { + if (qFuzzyCompare(m_startAngle, angle)) + return; + m_startAngle = angle; + emit startAngleChanged(); + update(); +} + +qreal ArcGauge::sweepAngle() const { + return m_sweepAngle; +} + +void ArcGauge::setSweepAngle(qreal angle) { + if (qFuzzyCompare(m_sweepAngle, angle)) + return; + m_sweepAngle = angle; + emit sweepAngleChanged(); + update(); +} + +qreal ArcGauge::lineWidth() const { + return m_lineWidth; +} + +void ArcGauge::setLineWidth(qreal width) { + if (qFuzzyCompare(m_lineWidth, width)) + return; + m_lineWidth = width; + emit lineWidthChanged(); + update(); +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/arcgauge.hpp b/plugin/src/Caelestia/Internal/arcgauge.hpp new file mode 100644 index 000000000..4ccb1fd01 --- /dev/null +++ b/plugin/src/Caelestia/Internal/arcgauge.hpp @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include +#include + +namespace caelestia::internal { + +class ArcGauge : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged) + Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) + Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged) + Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged) + Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged) + Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) + +public: + explicit ArcGauge(QQuickItem* parent = nullptr); + + void paint(QPainter* painter) override; + + [[nodiscard]] qreal percentage() const; + void setPercentage(qreal percentage); + + [[nodiscard]] QColor accentColor() const; + void setAccentColor(const QColor& color); + + [[nodiscard]] QColor trackColor() const; + void setTrackColor(const QColor& color); + + [[nodiscard]] qreal startAngle() const; + void setStartAngle(qreal angle); + + [[nodiscard]] qreal sweepAngle() const; + void setSweepAngle(qreal angle); + + [[nodiscard]] qreal lineWidth() const; + void setLineWidth(qreal width); + +signals: + void percentageChanged(); + void accentColorChanged(); + void trackColorChanged(); + void startAngleChanged(); + void sweepAngleChanged(); + void lineWidthChanged(); + +private: + qreal m_percentage = 0.0; + QColor m_accentColor; + QColor m_trackColor; + qreal m_startAngle = 0.75 * M_PI; + qreal m_sweepAngle = 1.5 * M_PI; + qreal m_lineWidth = 10.0; +}; + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/circularbuffer.cpp b/plugin/src/Caelestia/Internal/circularbuffer.cpp new file mode 100644 index 000000000..9701e7fac --- /dev/null +++ b/plugin/src/Caelestia/Internal/circularbuffer.cpp @@ -0,0 +1,94 @@ +#include "circularbuffer.hpp" + +#include + +namespace caelestia::internal { + +CircularBuffer::CircularBuffer(QObject* parent) + : QObject(parent) {} + +int CircularBuffer::capacity() const { + return m_capacity; +} + +void CircularBuffer::setCapacity(int capacity) { + if (capacity < 0) + capacity = 0; + if (m_capacity == capacity) + return; + + const auto old = values(); + + m_capacity = capacity; + m_data.resize(capacity); + m_data.fill(0.0); + m_head = 0; + m_count = 0; + + // Re-push old values, keeping the most recent ones + const auto start = old.size() > capacity ? old.size() - capacity : 0; + for (auto i = start; i < old.size(); ++i) { + m_data[m_head] = old[i]; + m_head = (m_head + 1) % m_capacity; + m_count++; + } + + emit capacityChanged(); + emit countChanged(); + emit valuesChanged(); +} + +int CircularBuffer::count() const { + return m_count; +} + +QList CircularBuffer::values() const { + QList result; + result.reserve(m_count); + for (int i = 0; i < m_count; ++i) + result.append(at(i)); + return result; +} + +qreal CircularBuffer::maximum() const { + if (m_count == 0) + return 0.0; + + qreal maxVal = at(0); + for (int i = 1; i < m_count; ++i) + maxVal = std::max(maxVal, at(i)); + return maxVal; +} + +void CircularBuffer::push(qreal value) { + if (m_capacity <= 0) + return; + + m_data[m_head] = value; + m_head = (m_head + 1) % m_capacity; + if (m_count < m_capacity) { + m_count++; + emit countChanged(); + } + emit valuesChanged(); +} + +void CircularBuffer::clear() { + if (m_count == 0) + return; + + m_head = 0; + m_count = 0; + emit countChanged(); + emit valuesChanged(); +} + +qreal CircularBuffer::at(int index) const { + if (index < 0 || index >= m_count) + return 0.0; + + const int actualIndex = (m_head - m_count + index + m_capacity) % m_capacity; + return m_data[actualIndex]; +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/circularbuffer.hpp b/plugin/src/Caelestia/Internal/circularbuffer.hpp new file mode 100644 index 000000000..ab2dba56e --- /dev/null +++ b/plugin/src/Caelestia/Internal/circularbuffer.hpp @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include + +namespace caelestia::internal { + +class CircularBuffer : public QObject { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(int capacity READ capacity WRITE setCapacity NOTIFY capacityChanged) + Q_PROPERTY(int count READ count NOTIFY countChanged) + Q_PROPERTY(QList values READ values NOTIFY valuesChanged) + Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged) + +public: + explicit CircularBuffer(QObject* parent = nullptr); + + [[nodiscard]] int capacity() const; + void setCapacity(int capacity); + + [[nodiscard]] int count() const; + [[nodiscard]] QList values() const; + [[nodiscard]] qreal maximum() const; + + Q_INVOKABLE void push(qreal value); + Q_INVOKABLE void clear(); + Q_INVOKABLE [[nodiscard]] qreal at(int index) const; + +signals: + void capacityChanged(); + void countChanged(); + void valuesChanged(); + +private: + QVector m_data; + int m_head = 0; + int m_count = 0; + int m_capacity = 0; +}; + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp index 434b75620..212499792 100644 --- a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp +++ b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp @@ -153,8 +153,10 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) * SPIN_ROTATION_DEGREES; } + const auto oldRotation = m_rotation; m_rotation = constantRotation + spinRotation; - emit rotationChanged(); + if (!qFuzzyCompare(m_rotation + 1.0, oldRotation + 1.0)) + emit rotationChanged(); // Grow active indicator. qreal fraction = @@ -182,6 +184,8 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { void CircularIndicatorManager::updateAdvance(qreal progress) { using namespace advance; const auto playtime = progress * TOTAL_DURATION_IN_MS; + const auto oldStart = m_startFraction; + const auto oldEnd = m_endFraction; // Adds constant rotation to segment positions. m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET; @@ -204,8 +208,10 @@ void CircularIndicatorManager::updateAdvance(qreal progress) { m_startFraction /= 360.0; m_endFraction /= 360.0; - emit startFractionChanged(); - emit endFractionChanged(); + if (!qFuzzyCompare(m_startFraction + 1.0, oldStart + 1.0)) + emit startFractionChanged(); + if (!qFuzzyCompare(m_endFraction + 1.0, oldEnd + 1.0)) + emit endFractionChanged(); } } // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/hyprextras.cpp b/plugin/src/Caelestia/Internal/hyprextras.cpp index 5308524d9..c73b6e0ba 100644 --- a/plugin/src/Caelestia/Internal/hyprextras.cpp +++ b/plugin/src/Caelestia/Internal/hyprextras.cpp @@ -1,10 +1,14 @@ #include "hyprextras.hpp" +#include "hyprdevices.hpp" #include #include #include +#include #include +Q_LOGGING_CATEGORY(lcHypr, "caelestia.internal.hypr", QtInfoMsg) + namespace caelestia::internal::hypr { HyprExtras::HyprExtras(QObject* parent) @@ -16,8 +20,7 @@ HyprExtras::HyprExtras(QObject* parent) , m_devices(new HyprDevices(this)) { const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); if (his.isEmpty()) { - qWarning() - << "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; + qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; return; } @@ -26,8 +29,7 @@ HyprExtras::HyprExtras(QObject* parent) hyprDir = "/tmp/hypr/" + his; if (!QDir(hyprDir).exists()) { - qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " - "Hyprland socket."; + qCWarning(lcHypr) << "Hyprland socket directory does not exist. Unable to connect to Hyprland socket."; return; } } @@ -62,7 +64,7 @@ void HyprExtras::message(const QString& message) { makeRequest(message, [](bool success, const QByteArray& res) { if (!success) { - qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); + qCWarning(lcHypr) << "message: request error:" << QString::fromUtf8(res); } }); } @@ -74,7 +76,7 @@ void HyprExtras::batchMessage(const QStringList& messages) { makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { if (!success) { - qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); + qCWarning(lcHypr) << "batchMessage: request error:" << QString::fromUtf8(res); } }); } @@ -84,16 +86,18 @@ void HyprExtras::applyOptions(const QVariantHash& options) { return; } - QString request = "[[BATCH]]"; + QString request; + request.reserve(12 + options.size() * 40); + request += QLatin1String("[[BATCH]]"); for (auto it = options.constBegin(); it != options.constEnd(); ++it) { - request += QString("keyword %1 %2;").arg(it.key(), it.value().toString()); + request += QLatin1String("keyword ") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';'); } makeRequest(request, [this](bool success, const QByteArray& res) { if (success) { refreshOptions(); } else { - qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); + qCWarning(lcHypr) << "applyOptions: request error" << QString::fromUtf8(res); } }); } @@ -143,15 +147,15 @@ void HyprExtras::refreshDevices() { void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { if (!m_socketValid) { - qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; + qCWarning(lcHypr) << "socketError: unable to connect to Hyprland event socket:" << error; } else { - qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; + qCWarning(lcHypr) << "socketError: Hyprland event socket error:" << error; } } void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { if (state == QLocalSocket::UnconnectedState && m_socketValid) { - qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; + qCWarning(lcHypr) << "socketStateChanged: Hyprland event socket disconnected."; } m_socketValid = state == QLocalSocket::ConnectedState; @@ -204,7 +208,7 @@ HyprExtras::SocketPtr HyprExtras::makeRequest( }); QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { - qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; + qCWarning(lcHypr) << "makeRequest: error making request:" << err << "| request:" << request; callback(false, {}); socket->close(); }); diff --git a/plugin/src/Caelestia/Internal/hyprextras.hpp b/plugin/src/Caelestia/Internal/hyprextras.hpp index 14563c0a3..48eceea92 100644 --- a/plugin/src/Caelestia/Internal/hyprextras.hpp +++ b/plugin/src/Caelestia/Internal/hyprextras.hpp @@ -1,15 +1,19 @@ #pragma once -#include "hyprdevices.hpp" #include #include #include +#include +#include namespace caelestia::internal::hypr { +class HyprDevices; + class HyprExtras : public QObject { Q_OBJECT QML_ELEMENT + Q_MOC_INCLUDE("hyprdevices.hpp") Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) Q_PROPERTY(caelestia::internal::hypr::HyprDevices* devices READ devices CONSTANT) diff --git a/plugin/src/Caelestia/Internal/logindmanager.cpp b/plugin/src/Caelestia/Internal/logindmanager.cpp index 4194ee1e7..740005d9d 100644 --- a/plugin/src/Caelestia/Internal/logindmanager.cpp +++ b/plugin/src/Caelestia/Internal/logindmanager.cpp @@ -4,6 +4,9 @@ #include #include #include +#include + +Q_LOGGING_CATEGORY(lcLogindManager, "caelestia.internal.logindmanager", QtInfoMsg) namespace caelestia::internal { @@ -11,7 +14,7 @@ LogindManager::LogindManager(QObject* parent) : QObject(parent) { auto bus = QDBusConnection::systemBus(); if (!bus.isConnected()) { - qWarning() << "LogindManager::LogindManager: failed to connect to system bus:" << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to system bus:" << bus.lastError().message(); return; } @@ -19,14 +22,13 @@ LogindManager::LogindManager(QObject* parent) "PrepareForSleep", this, SLOT(handlePrepareForSleep(bool))); if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to PrepareForSleep signal:" - << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to PrepareForSleep signal:" << bus.lastError().message(); } QDBusInterface login1("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", bus); const QDBusReply reply = login1.call("GetSession", "auto"); if (!reply.isValid()) { - qWarning() << "LogindManager::LogindManager: failed to get session path"; + qCWarning(lcLogindManager) << "Failed to get session path"; return; } const auto sessionPath = reply.value().path(); @@ -35,14 +37,14 @@ LogindManager::LogindManager(QObject* parent) SLOT(handleLockRequested())); if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to Lock signal:" << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to Lock signal:" << bus.lastError().message(); } ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Unlock", this, SLOT(handleUnlockRequested())); if (!ok) { - qWarning() << "LogindManager::LogindManager: failed to connect to Unlock signal:" << bus.lastError().message(); + qCWarning(lcLogindManager) << "Failed to connect to Unlock signal:" << bus.lastError().message(); } } diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp new file mode 100644 index 000000000..4e6b07194 --- /dev/null +++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp @@ -0,0 +1,216 @@ +#include "sparklineitem.hpp" +#include "circularbuffer.hpp" + +#include +#include +#include + +namespace caelestia::internal { + +SparklineItem::SparklineItem(QQuickItem* parent) + : QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void SparklineItem::paint(QPainter* painter) { + const bool has1 = m_line1 && m_line1->count() >= 2; + const bool has2 = m_line2 && m_line2->count() >= 2; + if (!has1 && !has2) + return; + + painter->setRenderHint(QPainter::Antialiasing, true); + + // Draw line1 first (behind), then line2 (in front) + if (has1) + drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha); + if (has2) + drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha); +} + +void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { + if (m_historyLength < 2) + return; + + const qreal w = width(); + const qreal h = height(); + const int len = buffer->count(); + const qreal stepX = w / static_cast(m_historyLength - 1); + const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX; + + // Build line path + QPainterPath linePath; + linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h); + for (int i = 1; i < len; ++i) { + const qreal x = startX + i * stepX; + const qreal y = h - (buffer->at(i) / m_maxValue) * h; + linePath.lineTo(x, y); + } + + // Stroke the line + QPen pen(color, m_lineWidth); + pen.setCapStyle(Qt::RoundCap); + pen.setJoinStyle(Qt::RoundJoin); + painter->setPen(pen); + painter->setBrush(Qt::NoBrush); + painter->drawPath(linePath); + + // Fill under the line + QPainterPath fillPath = linePath; + fillPath.lineTo(startX + (len - 1) * stepX, h); + fillPath.lineTo(startX, h); + fillPath.closeSubpath(); + + QColor fillColor = color; + fillColor.setAlphaF(static_cast(fillAlpha)); + painter->setPen(Qt::NoPen); + painter->setBrush(fillColor); + painter->drawPath(fillPath); +} + +void SparklineItem::connectBuffer(CircularBuffer* buffer) { + if (!buffer) + return; + + connect(buffer, &CircularBuffer::valuesChanged, this, [this]() { + update(); + }); + connect(buffer, &QObject::destroyed, this, [this, buffer]() { + if (m_line1 == buffer) { + m_line1 = nullptr; + emit line1Changed(); + } + if (m_line2 == buffer) { + m_line2 = nullptr; + emit line2Changed(); + } + update(); + }); +} + +CircularBuffer* SparklineItem::line1() const { + return m_line1; +} + +void SparklineItem::setLine1(CircularBuffer* buffer) { + if (m_line1 == buffer) + return; + if (m_line1) + disconnect(m_line1, nullptr, this, nullptr); + m_line1 = buffer; + connectBuffer(buffer); + emit line1Changed(); + update(); +} + +CircularBuffer* SparklineItem::line2() const { + return m_line2; +} + +void SparklineItem::setLine2(CircularBuffer* buffer) { + if (m_line2 == buffer) + return; + if (m_line2) + disconnect(m_line2, nullptr, this, nullptr); + m_line2 = buffer; + connectBuffer(buffer); + emit line2Changed(); + update(); +} + +QColor SparklineItem::line1Color() const { + return m_line1Color; +} + +void SparklineItem::setLine1Color(const QColor& color) { + if (m_line1Color == color) + return; + m_line1Color = color; + emit line1ColorChanged(); + update(); +} + +QColor SparklineItem::line2Color() const { + return m_line2Color; +} + +void SparklineItem::setLine2Color(const QColor& color) { + if (m_line2Color == color) + return; + m_line2Color = color; + emit line2ColorChanged(); + update(); +} + +qreal SparklineItem::line1FillAlpha() const { + return m_line1FillAlpha; +} + +void SparklineItem::setLine1FillAlpha(qreal alpha) { + if (qFuzzyCompare(m_line1FillAlpha, alpha)) + return; + m_line1FillAlpha = alpha; + emit line1FillAlphaChanged(); + update(); +} + +qreal SparklineItem::line2FillAlpha() const { + return m_line2FillAlpha; +} + +void SparklineItem::setLine2FillAlpha(qreal alpha) { + if (qFuzzyCompare(m_line2FillAlpha, alpha)) + return; + m_line2FillAlpha = alpha; + emit line2FillAlphaChanged(); + update(); +} + +qreal SparklineItem::maxValue() const { + return m_maxValue; +} + +void SparklineItem::setMaxValue(qreal value) { + if (qFuzzyCompare(m_maxValue, value)) + return; + m_maxValue = value; + emit maxValueChanged(); + update(); +} + +qreal SparklineItem::slideProgress() const { + return m_slideProgress; +} + +void SparklineItem::setSlideProgress(qreal progress) { + if (qFuzzyCompare(m_slideProgress, progress)) + return; + m_slideProgress = progress; + emit slideProgressChanged(); + update(); +} + +int SparklineItem::historyLength() const { + return m_historyLength; +} + +void SparklineItem::setHistoryLength(int length) { + if (m_historyLength == length) + return; + m_historyLength = length; + emit historyLengthChanged(); + update(); +} + +qreal SparklineItem::lineWidth() const { + return m_lineWidth; +} + +void SparklineItem::setLineWidth(qreal width) { + if (qFuzzyCompare(m_lineWidth, width)) + return; + m_lineWidth = width; + emit lineWidthChanged(); + update(); +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/sparklineitem.hpp b/plugin/src/Caelestia/Internal/sparklineitem.hpp new file mode 100644 index 000000000..22632a906 --- /dev/null +++ b/plugin/src/Caelestia/Internal/sparklineitem.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include +#include +#include +#include + +namespace caelestia::internal { + +class CircularBuffer; + +class SparklineItem : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + Q_MOC_INCLUDE("circularbuffer.hpp") + + Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed) + Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed) + Q_PROPERTY(QColor line1Color READ line1Color WRITE setLine1Color NOTIFY line1ColorChanged) + Q_PROPERTY(QColor line2Color READ line2Color WRITE setLine2Color NOTIFY line2ColorChanged) + Q_PROPERTY(qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY line1FillAlphaChanged) + Q_PROPERTY(qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY line2FillAlphaChanged) + Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) + Q_PROPERTY(qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY slideProgressChanged) + Q_PROPERTY(int historyLength READ historyLength WRITE setHistoryLength NOTIFY historyLengthChanged) + Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) + +public: + explicit SparklineItem(QQuickItem* parent = nullptr); + + void paint(QPainter* painter) override; + + [[nodiscard]] CircularBuffer* line1() const; + void setLine1(CircularBuffer* buffer); + + [[nodiscard]] CircularBuffer* line2() const; + void setLine2(CircularBuffer* buffer); + + [[nodiscard]] QColor line1Color() const; + void setLine1Color(const QColor& color); + + [[nodiscard]] QColor line2Color() const; + void setLine2Color(const QColor& color); + + [[nodiscard]] qreal line1FillAlpha() const; + void setLine1FillAlpha(qreal alpha); + + [[nodiscard]] qreal line2FillAlpha() const; + void setLine2FillAlpha(qreal alpha); + + [[nodiscard]] qreal maxValue() const; + void setMaxValue(qreal value); + + [[nodiscard]] qreal slideProgress() const; + void setSlideProgress(qreal progress); + + [[nodiscard]] int historyLength() const; + void setHistoryLength(int length); + + [[nodiscard]] qreal lineWidth() const; + void setLineWidth(qreal width); + +signals: + void line1Changed(); + void line2Changed(); + void line1ColorChanged(); + void line2ColorChanged(); + void line1FillAlphaChanged(); + void line2FillAlphaChanged(); + void maxValueChanged(); + void slideProgressChanged(); + void historyLengthChanged(); + void lineWidthChanged(); + +private: + void drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha); + void connectBuffer(CircularBuffer* buffer); + + CircularBuffer* m_line1 = nullptr; + CircularBuffer* m_line2 = nullptr; + QColor m_line1Color; + QColor m_line2Color; + qreal m_line1FillAlpha = 0.15; + qreal m_line2FillAlpha = 0.2; + qreal m_maxValue = 1024.0; + qreal m_slideProgress = 0.0; + int m_historyLength = 30; + qreal m_lineWidth = 2.0; +}; + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/visualiserbars.cpp b/plugin/src/Caelestia/Internal/visualiserbars.cpp new file mode 100644 index 000000000..926468b07 --- /dev/null +++ b/plugin/src/Caelestia/Internal/visualiserbars.cpp @@ -0,0 +1,198 @@ +#include "visualiserbars.hpp" + +#include +#include +#include +#include +#include +#include + +namespace caelestia::internal { + +VisualiserBars::VisualiserBars(QQuickItem* parent) + : QQuickPaintedItem(parent) { + setAntialiasing(true); +} + +void VisualiserBars::advance(qreal dt) { + if (m_displayValues.isEmpty() || m_settled) + return; + + // dt is in seconds (from FrameAnimation.frameTime), convert to ms + const qreal dtMs = dt * 1000.0; + const qreal tau = m_animationDuration / 3.0; + const qreal alpha = 1.0 - std::exp(-dtMs / tau); + + bool allSettled = true; + + for (qsizetype i = 0; i < m_displayValues.size(); ++i) { + const double diff = m_targetValues[i] - m_displayValues[i]; + + if (std::abs(diff) > 0.001) { + m_displayValues[i] += diff * alpha; + allSettled = false; + } else { + m_displayValues[i] = m_targetValues[i]; + } + } + + update(); + + if (allSettled && !m_settled) { + m_settled = true; + emit settledChanged(); + } +} + +void VisualiserBars::paint(QPainter* painter) { + if (m_displayValues.isEmpty()) + return; + + painter->setRenderHint(QPainter::Antialiasing, true); + painter->setPen(Qt::NoPen); + + const qreal h = height(); + const qreal maxBarHeight = h * 0.4; + + QLinearGradient gradient(0, h - maxBarHeight, 0, h); + gradient.setColorAt(0, m_primaryColor); + gradient.setColorAt(1, m_secondaryColor); + painter->setBrush(gradient); + + drawSide(painter, false); + drawSide(painter, true); +} + +void VisualiserBars::drawSide(QPainter* painter, bool rightSide) { + const qreal w = width(); + const qreal h = height(); + const auto count = m_displayValues.size(); + + if (count == 0) + return; + + const qreal sideWidth = w * 0.4; + const qreal slotWidth = sideWidth / static_cast(count); + const qreal barWidth = slotWidth - m_spacing; + + if (barWidth <= 0) + return; + + const qreal sideOffset = rightSide ? w * 0.6 : 0; + const qreal maxBarHeight = h * 0.4; + + for (qsizetype i = 0; i < count; ++i) { + const qsizetype valueIndex = rightSide ? i : (count - i - 1); + const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0); + const qreal barHeight = value * maxBarHeight; + + if (barHeight <= 0) + continue; + + const qreal x = static_cast(i) * slotWidth + sideOffset; + const qreal y = h - barHeight; + const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight }); + + QPainterPath path; + path.moveTo(x, h); + path.lineTo(x, y + r); + + if (r > 0) { + path.arcTo(x, y, r * 2, r * 2, 180, -90); + path.lineTo(x + barWidth - r, y); + path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90); + } else { + path.lineTo(x, y); + path.lineTo(x + barWidth, y); + } + + path.lineTo(x + barWidth, h); + path.closeSubpath(); + + painter->drawPath(path); + } +} + +QVector VisualiserBars::values() const { + return m_targetValues; +} + +void VisualiserBars::setValues(const QVector& values) { + m_targetValues = values; + + if (m_displayValues.size() != values.size()) { + m_displayValues.resize(values.size(), 0.0); + } + + if (m_settled) { + m_settled = false; + emit settledChanged(); + } + + emit valuesChanged(); +} + +bool VisualiserBars::settled() const { + return m_settled; +} + +QColor VisualiserBars::primaryColor() const { + return m_primaryColor; +} + +void VisualiserBars::setPrimaryColor(const QColor& color) { + if (m_primaryColor == color) + return; + m_primaryColor = color; + emit primaryColorChanged(); + update(); +} + +QColor VisualiserBars::secondaryColor() const { + return m_secondaryColor; +} + +void VisualiserBars::setSecondaryColor(const QColor& color) { + if (m_secondaryColor == color) + return; + m_secondaryColor = color; + emit secondaryColorChanged(); + update(); +} + +qreal VisualiserBars::rounding() const { + return m_rounding; +} + +void VisualiserBars::setRounding(qreal rounding) { + if (qFuzzyCompare(m_rounding, rounding)) + return; + m_rounding = rounding; + emit roundingChanged(); + update(); +} + +qreal VisualiserBars::spacing() const { + return m_spacing; +} + +void VisualiserBars::setSpacing(qreal spacing) { + if (qFuzzyCompare(m_spacing, spacing)) + return; + m_spacing = spacing; + emit spacingChanged(); + update(); +} + +int VisualiserBars::animationDuration() const { + return m_animationDuration; +} + +void VisualiserBars::setAnimationDuration(int duration) { + if (m_animationDuration == duration) + return; + m_animationDuration = duration; + emit animationDurationChanged(); +} + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Internal/visualiserbars.hpp b/plugin/src/Caelestia/Internal/visualiserbars.hpp new file mode 100644 index 000000000..95c071243 --- /dev/null +++ b/plugin/src/Caelestia/Internal/visualiserbars.hpp @@ -0,0 +1,72 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace caelestia::internal { + +class VisualiserBars : public QQuickPaintedItem { + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(QVector values READ values WRITE setValues NOTIFY valuesChanged) + Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged) + Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged) + Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged) + Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) + Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged) + Q_PROPERTY(bool settled READ settled NOTIFY settledChanged) + +public: + explicit VisualiserBars(QQuickItem* parent = nullptr); + + void paint(QPainter* painter) override; + + Q_INVOKABLE void advance(qreal dt); + + [[nodiscard]] QVector values() const; + void setValues(const QVector& values); + + [[nodiscard]] QColor primaryColor() const; + void setPrimaryColor(const QColor& color); + + [[nodiscard]] QColor secondaryColor() const; + void setSecondaryColor(const QColor& color); + + [[nodiscard]] qreal rounding() const; + void setRounding(qreal rounding); + + [[nodiscard]] qreal spacing() const; + void setSpacing(qreal spacing); + + [[nodiscard]] int animationDuration() const; + void setAnimationDuration(int duration); + + [[nodiscard]] bool settled() const; + +signals: + void valuesChanged(); + void primaryColorChanged(); + void secondaryColorChanged(); + void roundingChanged(); + void spacingChanged(); + void animationDurationChanged(); + void settledChanged(); + +private: + void drawSide(QPainter* painter, bool rightSide); + + QVector m_targetValues; + QVector m_displayValues; + QColor m_primaryColor; + QColor m_secondaryColor; + qreal m_rounding = 0.0; + qreal m_spacing = 0.0; + int m_animationDuration = 200; + bool m_settled = true; +}; + +} // namespace caelestia::internal diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp index e387ecd00..267a43946 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.cpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp @@ -57,8 +57,8 @@ bool FileSystemEntry::isImage() const { QString FileSystemEntry::mimeType() const { if (!m_mimeTypeInitialised) { - const QMimeDatabase db; - m_mimeType = db.mimeTypeForFile(m_path).name(); + static const QMimeDatabase s_db; + m_mimeType = s_db.mimeTypeForFile(m_path).name(); m_mimeTypeInitialised = true; } return m_mimeType; @@ -219,7 +219,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { if (m_recursive && m_watchChanges) { const auto currentDir = m_dir; const bool showHidden = m_showHidden; - const auto future = QtConcurrent::run([showHidden, path]() { + auto future = QtConcurrent::run([showHidden, path]() { QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; if (showHidden) { filters |= QDir::Hidden; @@ -232,16 +232,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { } return dirs; }); - const auto watcher = new QFutureWatcher(this); - connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { - const auto paths = watcher->result(); + future.then(this, [currentDir, showHidden, this](const QStringList& paths) { if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { // Ignore if dir or showHidden has changed m_watcher.addPaths(paths); } - watcher->deleteLater(); }); - watcher->setFuture(future); } } @@ -295,7 +291,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { oldPaths << entry->path(); } - const auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { + auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; std::optional iter; @@ -353,7 +349,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { newPaths.insert(path); } - if (promise.isCanceled() || newPaths == oldPaths) { + if (promise.isCanceled()) { return; } @@ -365,23 +361,17 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { } m_futures.insert(dir, future); - const auto watcher = new QFutureWatcher, QSet>>(this); - - connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { - m_futures.remove(dir); - - if (!watcher->future().isResultReadyAt(0)) { - watcher->deleteLater(); - return; - } - - const auto result = watcher->result(); - applyChanges(result.first, result.second); - - watcher->deleteLater(); - }); - - watcher->setFuture(future); + future + .then(this, + [dir, this](QPair, QSet> result) { + m_futures.remove(dir); + if (!result.first.isEmpty() || !result.second.isEmpty()) { + applyChanges(result.first, result.second); + } + }) + .onCanceled(this, [dir, this]() { + m_futures.remove(dir); + }); } void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp index cf8eae822..c3315858a 100644 --- a/plugin/src/Caelestia/Models/filesystemmodel.hpp +++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp @@ -132,7 +132,7 @@ class FileSystemModel : public QAbstractListModel { bool m_recursive; bool m_watchChanges; bool m_showHidden; - bool m_sortReverse; + bool m_sortReverse = false; Filter m_filter; QStringList m_nameFilters; diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp index 15634059e..69309f756 100644 --- a/plugin/src/Caelestia/Services/audiocollector.cpp +++ b/plugin/src/Caelestia/Services/audiocollector.cpp @@ -3,13 +3,16 @@ #include "service.hpp" #include #include -#include +#include #include #include #include #include #include +Q_LOGGING_CATEGORY(lcAc, "caelestia.services.ac", QtInfoMsg) +Q_LOGGING_CATEGORY(lcAcWorker, "caelestia.services.ac.worker", QtInfoMsg) + namespace caelestia::services { PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) @@ -23,13 +26,19 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) m_loop = pw_main_loop_new(nullptr); if (!m_loop) { - qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop"; + qCWarning(lcAcWorker) << "init: failed to create PipeWire main loop"; pw_deinit(); return; } timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC }; m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this); + if (!m_timer) { + qCWarning(lcAcWorker) << "init: failed to create timer"; + pw_main_loop_destroy(m_loop); + pw_deinit(); + return; + } pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false); auto props = pw_properties_new( @@ -55,6 +64,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); pw_stream_events events{}; + events.version = PW_VERSION_STREAM_EVENTS; events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) { auto* self = static_cast(data); self->streamStateChanged(state); @@ -65,13 +75,19 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) }; m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this); + if (!m_stream) { + qCWarning(lcAcWorker) << "init: failed to create stream"; + pw_main_loop_destroy(m_loop); + pw_deinit(); + return; + } const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY, static_cast( PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), params, 1); if (success < 0) { - qWarning() << "PipeWireWorker::init: failed to connect stream"; + qCWarning(lcAcWorker) << "init: failed to connect stream"; pw_stream_destroy(m_stream); pw_main_loop_destroy(m_loop); pw_deinit(); @@ -221,7 +237,7 @@ AudioCollector::AudioCollector(QObject* parent) , m_writeBuffer(&m_buffer2) {} AudioCollector::~AudioCollector() { - stop(); + AudioCollector::stop(); } void AudioCollector::start() { diff --git a/plugin/src/Caelestia/Services/audioprovider.cpp b/plugin/src/Caelestia/Services/audioprovider.cpp index 1fac9eea8..d2916f45e 100644 --- a/plugin/src/Caelestia/Services/audioprovider.cpp +++ b/plugin/src/Caelestia/Services/audioprovider.cpp @@ -2,9 +2,12 @@ #include "audiocollector.hpp" #include "service.hpp" -#include +#include #include +Q_LOGGING_CATEGORY(lcAp, "caelestia.services.ap", QtInfoMsg) +Q_LOGGING_CATEGORY(lcApProcessor, "caelestia.services.ap.processor", QtInfoMsg) + namespace caelestia::services { AudioProcessor::AudioProcessor(QObject* parent) @@ -48,7 +51,7 @@ AudioProvider::~AudioProvider() { void AudioProvider::init() { if (!m_processor) { - qWarning() << "AudioProvider::init: attempted to init with no processor set"; + qCWarning(lcAp) << "init: attempted to init with no processor set"; return; } diff --git a/plugin/src/Caelestia/Services/audioprovider.hpp b/plugin/src/Caelestia/Services/audioprovider.hpp index 5bf9bb00d..4b85929f4 100644 --- a/plugin/src/Caelestia/Services/audioprovider.hpp +++ b/plugin/src/Caelestia/Services/audioprovider.hpp @@ -23,7 +23,7 @@ public slots: virtual void process() = 0; private: - QTimer* m_timer; + QTimer* m_timer = nullptr; }; class AudioProvider : public Service { diff --git a/plugin/src/Caelestia/Services/beattracker.cpp b/plugin/src/Caelestia/Services/beattracker.cpp index 93addc679..649705799 100644 --- a/plugin/src/Caelestia/Services/beattracker.cpp +++ b/plugin/src/Caelestia/Services/beattracker.cpp @@ -19,7 +19,9 @@ BeatProcessor::~BeatProcessor() { if (m_in) { del_fvec(m_in); } - del_fvec(m_out); + if (m_out) { + del_fvec(m_out); + } } void BeatProcessor::process() { diff --git a/plugin/src/Caelestia/Services/cavaprovider.cpp b/plugin/src/Caelestia/Services/cavaprovider.cpp index 7b6cc1f20..a57f4040a 100644 --- a/plugin/src/Caelestia/Services/cavaprovider.cpp +++ b/plugin/src/Caelestia/Services/cavaprovider.cpp @@ -4,7 +4,10 @@ #include "audioprovider.hpp" #include #include -#include +#include + +Q_LOGGING_CATEGORY(lcCava, "caelestia.services.cava", QtInfoMsg) +Q_LOGGING_CATEGORY(lcCavaProcessor, "caelestia.services.cava.processor", QtInfoMsg) namespace caelestia::services { @@ -57,7 +60,7 @@ void CavaProcessor::process() { void CavaProcessor::setBars(int bars) { if (bars < 0) { - qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0."; + qCWarning(lcCavaProcessor) << "setBars: bars must be greater than 0. Setting to 0."; bars = 0; } @@ -109,7 +112,7 @@ int CavaProvider::bars() const { void CavaProvider::setBars(int bars) { if (bars < 0) { - qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0."; + qCWarning(lcCava) << "setBars: bars must be greater than 0. Setting to 0."; bars = 0; } diff --git a/plugin/src/Caelestia/Services/service.cpp b/plugin/src/Caelestia/Services/service.cpp index bc2156717..4e1921e87 100644 --- a/plugin/src/Caelestia/Services/service.cpp +++ b/plugin/src/Caelestia/Services/service.cpp @@ -1,6 +1,5 @@ #include "service.hpp" -#include #include namespace caelestia::services { diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp index 6e37e16f3..8d80ced97 100644 --- a/plugin/src/Caelestia/appdb.cpp +++ b/plugin/src/Caelestia/appdb.cpp @@ -1,9 +1,12 @@ #include "appdb.hpp" +#include #include #include #include +Q_LOGGING_CATEGORY(lcAppDb, "caelestia.appdb", QtInfoMsg) + namespace caelestia { AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) @@ -11,7 +14,7 @@ AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) , m_entry(entry) , m_frequency(frequency) { const auto mo = m_entry->metaObject(); - const auto tmo = metaObject(); + const auto tmo = &AppEntry::staticMetaObject; for (const auto& prop : { "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) { @@ -162,6 +165,39 @@ void AppDb::setEntries(const QObjectList& entries) { m_timer->start(); } +QStringList AppDb::favouriteApps() const { + return m_favouriteApps; +} + +void AppDb::setFavouriteApps(const QStringList& favApps) { + if (m_favouriteApps == favApps) { + return; + } + + m_favouriteApps = favApps; + emit favouriteAppsChanged(); + m_favouriteAppsRegex.clear(); + m_favouriteAppsRegex.reserve(m_favouriteApps.size()); + for (const QString& item : std::as_const(m_favouriteApps)) { + const QRegularExpression re(regexifyString(item)); + if (re.isValid()) { + m_favouriteAppsRegex << re; + } else { + qCWarning(lcAppDb) << "setFavouriteApps: regular expression is not valid:" << re.pattern(); + } + } + + emit appsChanged(); +} + +QString AppDb::regexifyString(const QString& original) const { + if (original.startsWith('^') && original.endsWith('$')) + return original; + + const QString escaped = QRegularExpression::escape(original); + return QStringLiteral("^%1$").arg(escaped); +} + QQmlListProperty AppDb::apps() { return QQmlListProperty(this, &getSortedApps()); } @@ -179,28 +215,48 @@ void AppDb::incrementFrequency(const QString& id) { auto* app = m_apps.value(id); if (app) { const auto before = getSortedApps(); - app->incrementFrequency(); - - if (before != getSortedApps()) { + getSortedApps(); + if (before != m_sortedApps) { emit appsChanged(); } } else { - qWarning() << "AppDb::incrementFrequency: could not find app with id" << id; + qCWarning(lcAppDb) << "incrementFrequency: could not find app with id" << id; } } QList& AppDb::getSortedApps() const { m_sortedApps = m_apps.values(); - std::sort(m_sortedApps.begin(), m_sortedApps.end(), [](AppEntry* a, AppEntry* b) { - if (a->frequency() != b->frequency()) { + + // Pre-compute favourite status to avoid repeated regex matching during sort + QSet favSet; + favSet.reserve(m_sortedApps.size()); + for (const auto* app : std::as_const(m_sortedApps)) { + if (isFavourite(app)) + favSet.insert(app->id()); + } + + std::sort(m_sortedApps.begin(), m_sortedApps.end(), [&favSet](AppEntry* a, AppEntry* b) { + const bool aIsFav = favSet.contains(a->id()); + const bool bIsFav = favSet.contains(b->id()); + if (aIsFav != bIsFav) + return aIsFav; + if (a->frequency() != b->frequency()) return a->frequency() > b->frequency(); - } return a->name().localeAwareCompare(b->name()) < 0; }); return m_sortedApps; } +bool AppDb::isFavourite(const AppEntry* app) const { + for (const QRegularExpression& re : m_favouriteAppsRegex) { + if (re.match(app->id()).hasMatch()) { + return true; + } + } + return false; +} + quint32 AppDb::getFrequency(const QString& id) const { auto db = QSqlDatabase::database(m_uuid); QSqlQuery query(db); @@ -222,7 +278,8 @@ void AppDb::updateAppFrequencies() { app->setFrequency(getFrequency(app->id())); } - if (before != getSortedApps()) { + getSortedApps(); + if (before != m_sortedApps) { emit appsChanged(); } } @@ -249,11 +306,13 @@ void AppDb::updateApps() { newIds.insert(entry->property("id").toString()); } - for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) { - const auto& id = *it; - if (!newIds.contains(id)) { + for (auto it = m_apps.begin(); it != m_apps.end();) { + if (!newIds.contains(it.key())) { dirty = true; - m_apps.take(id)->deleteLater(); + it.value()->deleteLater(); + it = m_apps.erase(it); + } else { + ++it; } } diff --git a/plugin/src/Caelestia/appdb.hpp b/plugin/src/Caelestia/appdb.hpp index 5f9b96044..ce5f27062 100644 --- a/plugin/src/Caelestia/appdb.hpp +++ b/plugin/src/Caelestia/appdb.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace caelestia { @@ -66,6 +67,7 @@ class AppDb : public QObject { Q_PROPERTY(QString uuid READ uuid CONSTANT) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED) Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED) + Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED) Q_PROPERTY(QQmlListProperty apps READ apps NOTIFY appsChanged) public: @@ -79,6 +81,9 @@ class AppDb : public QObject { [[nodiscard]] QObjectList entries() const; void setEntries(const QObjectList& entries); + [[nodiscard]] QStringList favouriteApps() const; + void setFavouriteApps(const QStringList& favApps); + [[nodiscard]] QQmlListProperty apps(); Q_INVOKABLE void incrementFrequency(const QString& id); @@ -86,6 +91,7 @@ class AppDb : public QObject { signals: void pathChanged(); void entriesChanged(); + void favouriteAppsChanged(); void appsChanged(); private: @@ -94,10 +100,14 @@ class AppDb : public QObject { const QString m_uuid; QString m_path; QObjectList m_entries; + QStringList m_favouriteApps; // unedited string list from qml + QList m_favouriteAppsRegex; // pre-regexified m_favouriteApps list QHash m_apps; mutable QList m_sortedApps; + QString regexifyString(const QString& original) const; QList& getSortedApps() const; + bool isFavourite(const AppEntry* app) const; quint32 getFrequency(const QString& id) const; void updateAppFrequencies(); void updateApps(); diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp index 6e3bfa99c..28a0e1784 100644 --- a/plugin/src/Caelestia/cutils.cpp +++ b/plugin/src/Caelestia/cutils.cpp @@ -6,8 +6,11 @@ #include #include #include +#include #include +Q_LOGGING_CATEGORY(lcCUtils, "caelestia.cutils", QtInfoMsg) + namespace caelestia { void CUtils::saveItem(QQuickItem* target, const QUrl& path) { @@ -32,17 +35,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { if (!target) { - qWarning() << "CUtils::saveItem: a target is required"; + qCWarning(lcCUtils) << "saveItem: a target is required"; return; } if (!path.isLocalFile()) { - qWarning() << "CUtils::saveItem:" << path << "is not a local file"; + qCWarning(lcCUtils) << "saveItem:" << path << "is not a local file"; return; } if (!target->window()) { - qWarning() << "CUtils::saveItem: unable to save target" << target << "without a window"; + qCWarning(lcCUtils) << "saveItem: unable to save target" << target << "without a window"; return; } @@ -75,13 +78,20 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { if (watcher->result()) { if (onSaved.isCallable()) { - onSaved.call( - { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); + QJSValueList args = { QJSValue(path.toLocalFile()) }; + if (engine) { + args << engine->toScriptValue(QVariant::fromValue(path)); + } + onSaved.call(args); } } else { - qWarning() << "CUtils::saveItem: failed to save" << path; + qCWarning(lcCUtils) << "saveItem: failed to save" << path; if (onFailed.isCallable()) { - onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); + if (engine) { + onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); + } else { + onFailed.call(); + } } } watcher->deleteLater(); @@ -92,17 +102,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { if (!source.isLocalFile()) { - qWarning() << "CUtils::copyFile: source" << source << "is not a local file"; + qCWarning(lcCUtils) << "copyFile: source" << source << "is not a local file"; return false; } if (!target.isLocalFile()) { - qWarning() << "CUtils::copyFile: target" << target << "is not a local file"; + qCWarning(lcCUtils) << "copyFile: target" << target << "is not a local file"; return false; } if (overwrite && QFile::exists(target.toLocalFile())) { if (!QFile::remove(target.toLocalFile())) { - qWarning() << "CUtils::copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); + qCWarning(lcCUtils) << "copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); return false; } } @@ -112,7 +122,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co bool CUtils::deleteFile(const QUrl& path) const { if (!path.isLocalFile()) { - qWarning() << "CUtils::deleteFile: path" << path << "is not a local file"; + qCWarning(lcCUtils) << "deleteFile: path" << path << "is not a local file"; return false; } @@ -121,7 +131,7 @@ bool CUtils::deleteFile(const QUrl& path) const { QString CUtils::toLocalFile(const QUrl& url) const { if (!url.isLocalFile()) { - qWarning() << "CUtils::toLocalFile: given url is not a local file" << url; + qCWarning(lcCUtils) << "toLocalFile: given url is not a local file" << url; return QString(); } diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp index 880b0785c..7f3ba5261 100644 --- a/plugin/src/Caelestia/imageanalyser.cpp +++ b/plugin/src/Caelestia/imageanalyser.cpp @@ -4,8 +4,11 @@ #include #include #include +#include #include +Q_LOGGING_CATEGORY(lcImageAnalyser, "caelestia.imageanalyser", QtInfoMsg) + namespace caelestia { ImageAnalyser::ImageAnalyser(QObject* parent) @@ -134,6 +137,11 @@ void ImageAnalyser::update() { if (m_sourceItem) { const QSharedPointer grabResult = m_sourceItem->grabToImage(); + if (!grabResult) { + QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, + Qt::SingleShotConnection); + return; + } QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); }); @@ -147,7 +155,7 @@ void ImageAnalyser::update() { void ImageAnalyser::analyse(QPromise& promise, const QImage& image, int rescaleSize) { if (image.isNull()) { - qWarning() << "ImageAnalyser::analyse: image is null"; + qCWarning(lcImageAnalyser) << "analyse: image is null"; return; } @@ -191,14 +199,14 @@ void ImageAnalyser::analyse(QPromise& promise, const QImage& imag continue; } - const quint32 mr = static_cast(pixel[0] & 0xF8); + const quint32 mr = static_cast(pixel[2] & 0xF8); const quint32 mg = static_cast(pixel[1] & 0xF8); - const quint32 mb = static_cast(pixel[2] & 0xF8); + const quint32 mb = static_cast(pixel[0] & 0xF8); ++colours[(mr << 16) | (mg << 8) | mb]; - const qreal r = pixel[0] / 255.0; + const qreal r = pixel[2] / 255.0; const qreal g = pixel[1] / 255.0; - const qreal b = pixel[2] / 255.0; + const qreal b = pixel[0] / 255.0; totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); ++count; } diff --git a/plugin/src/Caelestia/imageanalyser.hpp b/plugin/src/Caelestia/imageanalyser.hpp index bbea2b32e..63fbf9691 100644 --- a/plugin/src/Caelestia/imageanalyser.hpp +++ b/plugin/src/Caelestia/imageanalyser.hpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace caelestia { @@ -48,7 +49,7 @@ class ImageAnalyser : public QObject { QFutureWatcher* const m_futureWatcher; QString m_source; - QQuickItem* m_sourceItem; + QPointer m_sourceItem; int m_rescaleSize; QColor m_dominantColour; diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp index 44e8d21e2..c72421793 100644 --- a/plugin/src/Caelestia/qalculator.cpp +++ b/plugin/src/Caelestia/qalculator.cpp @@ -1,13 +1,19 @@ #include "qalculator.hpp" #include +#include namespace caelestia { +QMutex Qalculator::s_calculatorMutex; + Qalculator::Qalculator(QObject* parent) : QObject(parent) { if (!CALCULATOR) { - new Calculator(); + // Calculator constructor sets the global `calculator` pointer (CALCULATOR macro), + // but we need to assign it to a var so compiler doesn't flag it as a leak + static const auto* const instance = new Calculator(); + Q_UNUSED(instance) CALCULATOR->loadExchangeRates(); CALCULATOR->loadGlobalDefinitions(); CALCULATOR->loadLocalDefinitions(); @@ -19,6 +25,8 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString(); } + QMutexLocker locker(&s_calculatorMutex); + EvaluationOptions eo; PrintOptions po; @@ -49,4 +57,92 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { return QString::fromStdString(result); } +void Qalculator::evalAsync(const QString& expr) { + const quint64 gen = ++m_generation; + + if (expr.isEmpty()) { + if (!m_result.isEmpty()) { + m_result.clear(); + emit resultChanged(); + } + if (!m_rawResult.isEmpty()) { + m_rawResult.clear(); + emit rawResultChanged(); + } + if (m_busy) { + m_busy = false; + emit busyChanged(); + } + return; + } + + if (!m_busy) { + m_busy = true; + emit busyChanged(); + } + + QtConcurrent::run([expr]() -> QPair { + QMutexLocker locker(&s_calculatorMutex); + + EvaluationOptions eo; + PrintOptions po; + + std::string parsed; + std::string result = CALCULATOR->calculateAndPrint( + CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); + + std::string error; + while (CALCULATOR->message()) { + if (!CALCULATOR->message()->message().empty()) { + if (CALCULATOR->message()->type() == MESSAGE_ERROR) { + error += "error: "; + } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { + error += "warning: "; + } + error += CALCULATOR->message()->message(); + } + CALCULATOR->nextMessage(); + } + + if (!error.empty()) { + const QString errorStr = QString::fromStdString(error); + return { errorStr, errorStr }; + } + + const QString rawStr = QString::fromStdString(result); + return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; + }).then(this, [this, gen](QPair result) { + if (gen != m_generation) { + return; + } + + const auto& [formatted, raw] = result; + + if (m_result != formatted) { + m_result = formatted; + emit resultChanged(); + } + if (m_rawResult != raw) { + m_rawResult = raw; + emit rawResultChanged(); + } + if (m_busy) { + m_busy = false; + emit busyChanged(); + } + }); +} + +QString Qalculator::result() const { + return m_result; +} + +QString Qalculator::rawResult() const { + return m_rawResult; +} + +bool Qalculator::busy() const { + return m_busy; +} + } // namespace caelestia diff --git a/plugin/src/Caelestia/qalculator.hpp b/plugin/src/Caelestia/qalculator.hpp index a07a8a2fc..b2f5517f7 100644 --- a/plugin/src/Caelestia/qalculator.hpp +++ b/plugin/src/Caelestia/qalculator.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -10,10 +11,32 @@ class Qalculator : public QObject { QML_ELEMENT QML_SINGLETON + Q_PROPERTY(QString result READ result NOTIFY resultChanged) + Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged) + Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) + public: explicit Qalculator(QObject* parent = nullptr); Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; + Q_INVOKABLE void evalAsync(const QString& expr); + + [[nodiscard]] QString result() const; + [[nodiscard]] QString rawResult() const; + [[nodiscard]] bool busy() const; + +signals: + void resultChanged(); + void rawResultChanged(); + void busyChanged(); + +private: + static QMutex s_calculatorMutex; + + QString m_result; + QString m_rawResult; + bool m_busy = false; + quint64 m_generation = 0; }; } // namespace caelestia diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp index 2ceddb351..862dea7ff 100644 --- a/plugin/src/Caelestia/requests.cpp +++ b/plugin/src/Caelestia/requests.cpp @@ -1,22 +1,41 @@ #include "requests.hpp" +#include +#include #include +#include #include #include +Q_LOGGING_CATEGORY(lcRequests, "caelestia.requests", QtInfoMsg) + namespace caelestia { Requests::Requests(QObject* parent) : QObject(parent) , m_manager(new QNetworkAccessManager(this)) {} -void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const { +void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { if (!onSuccess.isCallable()) { - qWarning() << "Requests::get: onSuccess is not callable"; + qCWarning(lcRequests) << "get: onSuccess is not callable"; return; } QNetworkRequest request(url); + request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); + request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); + request.setRawHeader("Cache-Control", "no-cache, no-store"); + request.setRawHeader("Pragma", "no-cache"); + request.setRawHeader("Connection", "close"); + + if (headers.isObject()) { + QJSValueIterator it(headers); + while (it.hasNext()) { + it.next(); + request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8()); + } + } + auto reply = m_manager->get(request); QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() { @@ -25,11 +44,15 @@ void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const } else if (onError.isCallable()) { onError.call({ reply->errorString() }); } else { - qWarning() << "Requests::get: request failed with error" << reply->errorString(); + qCWarning(lcRequests) << "get: request failed with error" << reply->errorString(); } reply->deleteLater(); }); } +void Requests::resetCookies() const { + m_manager->setCookieJar(new QNetworkCookieJar(m_manager)); +} + } // namespace caelestia diff --git a/plugin/src/Caelestia/requests.hpp b/plugin/src/Caelestia/requests.hpp index 1db2f4cf6..d07d7e8f0 100644 --- a/plugin/src/Caelestia/requests.hpp +++ b/plugin/src/Caelestia/requests.hpp @@ -14,7 +14,9 @@ class Requests : public QObject { public: explicit Requests(QObject* parent = nullptr); - Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const; + Q_INVOKABLE void get( + const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; + Q_INVOKABLE void resetCookies() const; private: QNetworkAccessManager* m_manager; diff --git a/plugin/src/Caelestia/toaster.cpp b/plugin/src/Caelestia/toaster.cpp index 978805de0..b51c77f8d 100644 --- a/plugin/src/Caelestia/toaster.cpp +++ b/plugin/src/Caelestia/toaster.cpp @@ -1,6 +1,5 @@ #include "toaster.hpp" -#include #include #include diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py new file mode 100755 index 000000000..cef46657c --- /dev/null +++ b/scripts/qml-lint-conventions.py @@ -0,0 +1,648 @@ +#!/usr/bin/env python3 +"""Checks QML files for Qt coding convention violations. + +https://doc.qt.io/qt-6/qml-codingconventions.html + +Required ordering within each QML object (with blank line between sections): + 1. id + 2. property declarations + 3. signal declarations + 4. JavaScript functions + 5. object properties (bindings) + 6. child objects + 7. component definitions +""" + +import re +import sys +from enum import IntEnum +from pathlib import Path + +RED = "\033[0;31m" +YELLOW = "\033[0;33m" +CYAN = "\033[0;36m" +GREEN = "\033[0;32m" +MAGENTA = "\033[0;35m" +BOLD = "\033[1m" +RESET = "\033[0m" + +REPO_ROOT = Path(__file__).resolve().parent.parent + + +class Section(IntEnum): + ID = 0 + PROPERTY = 1 + SIGNAL = 2 + FUNCTION = 3 + BINDING = 4 + CHILD = 5 + COMPONENT_DEF = 6 + + +SECTION_NAMES = { + Section.ID: "id", + Section.PROPERTY: "property declarations", + Section.SIGNAL: "signal declarations", + Section.FUNCTION: "functions", + Section.BINDING: "bindings", + Section.CHILD: "child objects", + Section.COMPONENT_DEF: "component definitions", +} + +RULE_COLOURS = { + "file-structure": RED, + "import-order": GREEN, + "section-order": YELLOW, + "missing-section-separator": CYAN, + "blank-after-open-brace": MAGENTA, + "blank-before-close-brace": MAGENTA, +} + +IMPORT_RE = re.compile(r"^import\s+(\S+)") + + +def import_group(module: str) -> tuple[int, int] | None: + """Return (group, depth) for a module import, or None to skip.""" + if module.startswith('"'): + return None + depth = module.count(".") + 1 + if module == "QtQuick" or module.startswith("QtQuick."): + return (1, depth) + if module.startswith("Qt"): + return (2, depth) + if module == "Quickshell" or module.startswith("Quickshell."): + return (3, depth) + if module == "M3Shapes": + return (4, depth) + if module == "Caelestia" or module.startswith("Caelestia."): + return (5, depth) + if module == "qs.components" or module.startswith("qs.components."): + return (6, depth) + if module == "qs.services": + return (7, depth) + if module == "qs.config": + return (8, depth) + if module == "qs.utils": + return (9, depth) + if module == "qs.modules" or module.startswith("qs.modules."): + return (10, depth) + return None + + +def parse_imports(lines: list[str]) -> tuple[int | None, int | None, list[str], list[tuple[str, int, int, str]]]: + """Parse the import block, returning (first_idx, last_idx, relative_imports, module_imports). + + module_imports entries are (line_text, group, depth, module_name). + """ + first_import = None + last_import = None + relative_imports: list[str] = [] + module_imports: list[tuple[str, int, int, str]] = [] + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped or stripped.startswith("//") or stripped.startswith("pragma "): + continue + m = IMPORT_RE.match(stripped) + if m: + if first_import is None: + first_import = i + last_import = i + module = m.group(1) + result = import_group(module) + if result is None: + relative_imports.append(line) + else: + group, depth = result + module_imports.append((line, group, depth, module)) + continue + break + + return first_import, last_import, relative_imports, module_imports + + +def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]: + """Check that module imports are in the required order.""" + violations = [] + _, _, _, module_imports = parse_imports(lines) + imports = [(i, *entry) for i, entry in enumerate(module_imports)] + + for j in range(1, len(imports)): + _, prev_line, prev_group, prev_depth, prev_mod = imports[j - 1] + _, curr_line, curr_group, curr_depth, curr_mod = imports[j] + + # Find actual line number for the current import + lineno = 0 + count = 0 + for li, line in enumerate(lines): + stripped = line.strip() + m = IMPORT_RE.match(stripped) + if m and import_group(m.group(1)) is not None: + if count == j: + lineno = li + 1 + break + count += 1 + + if curr_group < prev_group: + violations.append( + Violation( + rel, + lineno, + "import-order", + f"'{curr_mod}' should appear before '{prev_mod}'", + ) + ) + elif curr_group == prev_group and curr_depth < prev_depth: + violations.append( + Violation( + rel, + lineno, + "import-order", + f"'{curr_mod}' should appear before '{prev_mod}' (less nested first)", + ) + ) + + return violations + + +def fix_imports(lines: list[str]) -> list[str]: + """Sort imports and return the modified lines.""" + first, last, relative, module = parse_imports(lines) + if first is None: + return lines + + module.sort(key=lambda x: (x[1], x[2], x[3])) + sorted_imports = relative + [entry[0] for entry in module] + return lines[:first] + sorted_imports + lines[last + 1 :] + + +def check_file_structure(lines: list[str], rel: str) -> list[Violation]: + """Check file-level structure: pragmas, then imports, then content.""" + violations = [] + pragma_indices: list[int] = [] + import_indices: list[int] = [] + content_start: int | None = None + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("pragma "): + pragma_indices.append(i) + elif IMPORT_RE.match(stripped): + import_indices.append(i) + else: + content_start = i + break + + # Pragmas must come before imports + if pragma_indices and import_indices: + if pragma_indices[-1] > import_indices[0]: + violations.append( + Violation(rel, pragma_indices[-1] + 1, "file-structure", "pragmas should appear before imports") + ) + + # Separator between pragmas and imports + if pragma_indices and import_indices: + gap = import_indices[0] - pragma_indices[-1] - 1 + if gap == 0: + violations.append( + Violation( + rel, import_indices[0] + 1, "file-structure", "blank line expected between pragmas and imports" + ) + ) + elif gap > 1: + violations.append( + Violation( + rel, + pragma_indices[-1] + 3, + "file-structure", + "only one blank line expected between pragmas and imports", + ) + ) + + # No blank lines within imports + for j in range(1, len(import_indices)): + if import_indices[j] != import_indices[j - 1] + 1: + for gap_line in range(import_indices[j - 1] + 1, import_indices[j]): + if not lines[gap_line].strip(): + violations.append( + Violation(rel, gap_line + 1, "file-structure", "no blank lines expected within imports") + ) + + # Separator between imports/pragmas and content + last_header = import_indices[-1] if import_indices else (pragma_indices[-1] if pragma_indices else None) + if last_header is not None and content_start is not None: + gap = content_start - last_header - 1 + label = "imports" if import_indices else "pragmas" + if gap == 0: + violations.append( + Violation( + rel, content_start + 1, "file-structure", f"blank line expected between {label} and content" + ) + ) + elif gap > 1: + violations.append( + Violation( + rel, + last_header + 3, + "file-structure", + f"only one blank line expected between {label} and content", + ) + ) + + return violations + + +def fix_file_structure(lines: list[str]) -> list[str]: + """Ensure correct file structure: pragmas, blank, imports (no gaps), blank, content.""" + pragmas: list[str] = [] + imports: list[str] = [] + content_start: int | None = None + + for i, line in enumerate(lines): + stripped = line.strip() + if not stripped: + continue + if stripped.startswith("pragma "): + pragmas.append(line) + elif IMPORT_RE.match(stripped): + imports.append(line) + else: + content_start = i + break + + if content_start is None: + content_start = len(lines) + + # Skip any blank lines at the start of content + while content_start < len(lines) and not lines[content_start].strip(): + content_start += 1 + + result: list[str] = [] + has_content = content_start < len(lines) + if pragmas: + result.extend(pragmas) + if imports or has_content: + result.append("") + if imports: + result.extend(imports) + if has_content: + result.append("") + result.extend(lines[content_start:]) + return result + + +def fix_section_separators(lines: list[str]) -> list[str]: + """Insert blank lines between different sections and return modified lines.""" + insertions: list[int] = [] + scopes: dict[str, ScopeTracker] = {} + in_block_comment = False + func_skip_depth = 0 + prev_blank: dict[str, bool] = {} + + for i, line in enumerate(lines): + stripped = line.strip() + indent = get_indent(line) + + if in_block_comment: + if BLOCK_COMMENT_END.search(stripped): + in_block_comment = False + continue + if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): + in_block_comment = True + continue + + if not stripped: + for key in prev_blank: + prev_blank[key] = True + continue + + if COMMENT_LINE_RE.match(stripped): + continue + + if func_skip_depth > 0: + func_skip_depth += stripped.count("{") - stripped.count("}") + if func_skip_depth <= 0: + func_skip_depth = 0 + continue + + if stripped == "}": + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + continue + + section = classify_line(stripped) + if section is None: + continue + + if indent not in scopes: + scopes[indent] = ScopeTracker() + prev_blank[indent] = True + + tracker = scopes[indent] + had_blank = prev_blank.get(indent, True) + + if tracker.last_section is not None and section != tracker.last_section and not had_blank: + insertions.append(i) + + if tracker.last_section is None or section >= tracker.last_section: + tracker.last_section = section + tracker.last_section_line = i + 1 + + prev_blank[indent] = False + + brace_count = stripped.count("{") - stripped.count("}") + if brace_count > 0 and section == Section.FUNCTION: + func_skip_depth = brace_count + if brace_count > 0 and section == Section.BINDING: + colon_idx = stripped.index(":") + after_colon = stripped[colon_idx + 1 :].strip() + if not re.match(r"^[A-Z]", after_colon): + func_skip_depth = brace_count + if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + + result = list(lines) + for idx in reversed(insertions): + result.insert(idx, "") + return result + + +def fix_file(filepath: Path) -> bool: + """Fix auto-fixable violations. Returns True if file was modified.""" + try: + text = filepath.read_text() + except (OSError, UnicodeDecodeError): + return False + + lines = text.splitlines() + lines = fix_imports(lines) + lines = fix_file_structure(lines) + lines = fix_section_separators(lines) + new_text = "\n".join(lines) + if text.endswith("\n"): + new_text += "\n" + if new_text != text: + filepath.write_text(new_text) + return True + return False + + +# Regexes +PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s") +SIGNAL_RE = re.compile(r"^signal\s") +FUNCTION_RE = re.compile(r"^function\s") +ID_RE = re.compile(r"^id\s*:\s*[a-zA-Z_]\w*\s*$") +ENUM_RE = re.compile(r"^enum\s") +COMPONENT_DEF_RE = re.compile(r"^component\s+\w+\s*:") +COMMENT_LINE_RE = re.compile(r"^//") +BLOCK_COMMENT_START = re.compile(r"/\*") +BLOCK_COMMENT_END = re.compile(r"\*/") +BINDING_RE = re.compile(r"^[a-z][a-zA-Z0-9_.]*\s*:") +SIGNAL_HANDLER_RE = re.compile(r"^on[A-Z][a-zA-Z]*\s*:") +# Child object: starts with uppercase or is a known child-like pattern +CHILD_OBJECT_RE = re.compile(r"^[A-Z][a-zA-Z0-9_.]*\s*\{") +# Inline component: Component { ... } +INLINE_COMPONENT_RE = re.compile(r"^Component\s*\{") +# Behavior on {, NumberAnimation on {, etc. +BEHAVIOR_ON_RE = re.compile(r"^[A-Z]\w+\s+on\s+\w[\w.]*\s*\{") +# Attached signal handler: Component.onCompleted:, Drag.onDragStarted:, etc. +ATTACHED_HANDLER_RE = re.compile(r"^[A-Z]\w+\.on[A-Z]\w*\s*:") + + +class Violation: + def __init__(self, file: str, line: int, rule: str, msg: str): + self.file = file + self.line = line + self.rule = rule + self.msg = msg + + def __str__(self): + c = RULE_COLOURS.get(self.rule, "") + return f"{c}[{self.rule}]{RESET} {self.file}:{self.line}: {self.msg}" + + +class ScopeTracker: + """Tracks the current section and last-seen state for one indent level.""" + + def __init__(self): + self.last_section: Section | None = None + self.last_section_line: int = 0 + self.had_blank_before_current: bool = True # no separator needed at start + + +def get_indent(line: str) -> str: + return line[: len(line) - len(line.lstrip())] + + +def classify_line(stripped: str) -> Section | None: + """Classify a stripped QML line into a section category.""" + if ID_RE.match(stripped): + return Section.ID + if PROPERTY_DECL_RE.match(stripped): + return Section.PROPERTY + if SIGNAL_RE.match(stripped): + return Section.SIGNAL + if FUNCTION_RE.match(stripped): + return Section.FUNCTION + if ENUM_RE.match(stripped): + return Section.PROPERTY # enums go with declarations + if COMPONENT_DEF_RE.match(stripped): + return Section.COMPONENT_DEF + if BEHAVIOR_ON_RE.match(stripped): + return Section.CHILD + if CHILD_OBJECT_RE.match(stripped): + return Section.CHILD + if INLINE_COMPONENT_RE.match(stripped): + return Section.CHILD + if BINDING_RE.match(stripped) or SIGNAL_HANDLER_RE.match(stripped): + return Section.BINDING + if ATTACHED_HANDLER_RE.match(stripped): + return Section.BINDING + return None + + +def check_file(filepath: Path) -> list[Violation]: + violations = [] + rel = str(filepath.relative_to(REPO_ROOT)) + + try: + lines = filepath.read_text().splitlines() + except (OSError, UnicodeDecodeError): + return violations + + violations.extend(check_file_structure(lines, rel)) + violations.extend(check_imports(filepath, lines, rel)) + + scopes: dict[str, ScopeTracker] = {} # indent -> tracker + in_block_comment = False + func_skip_depth = 0 # brace depth for skipping function bodies only + prev_blank: dict[str, bool] = {} # indent -> was previous relevant line a blank? + + for i, line in enumerate(lines): + lineno = i + 1 + stripped = line.strip() + indent = get_indent(line) + + # Handle block comments + if in_block_comment: + if BLOCK_COMMENT_END.search(stripped): + in_block_comment = False + continue + if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): + in_block_comment = True + continue + + # Track blank lines per indent + if not stripped: + # Check: blank line right after opening brace of a QML object + if i > 0 and func_skip_depth == 0 and not in_block_comment and lines[i - 1].strip().endswith("{"): + violations.append( + Violation( + rel, + lineno, + "blank-after-open-brace", + "no blank line expected after opening brace", + ) + ) + for key in prev_blank: + prev_blank[key] = True + continue + + # Skip line comments + if COMMENT_LINE_RE.match(stripped): + continue + + # Skip inside function bodies (JS code, not QML structure) + if func_skip_depth > 0: + func_skip_depth += stripped.count("{") - stripped.count("}") + if func_skip_depth <= 0: + func_skip_depth = 0 + continue + + # Closing brace: pop all scopes deeper than this indent + # (the scope at this indent belongs to the parent object and must persist) + if stripped == "}": + # Check: blank line right before closing brace + if i > 0 and not lines[i - 1].strip(): + violations.append( + Violation( + rel, + lineno, + "blank-before-close-brace", + "no blank line expected before closing brace", + ) + ) + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + continue + + section = classify_line(stripped) + if section is None: + continue + + # Get or create scope tracker for this indent + if indent not in scopes: + scopes[indent] = ScopeTracker() + prev_blank[indent] = True # treat start of object as having separator + + tracker = scopes[indent] + had_blank = prev_blank.get(indent, True) + + # --- Check 1: Section ordering --- + if tracker.last_section is not None and section < tracker.last_section: + violations.append( + Violation( + rel, + lineno, + "section-order", + f"{SECTION_NAMES[section]} should appear before " + f"{SECTION_NAMES[tracker.last_section]} " + f"(seen at line {tracker.last_section_line})", + ) + ) + + # --- Check 2: Missing blank line between different sections --- + if tracker.last_section is not None and section != tracker.last_section and not had_blank: + violations.append( + Violation( + rel, + lineno, + "missing-section-separator", + f"blank line expected between {SECTION_NAMES[tracker.last_section]} and {SECTION_NAMES[section]}", + ) + ) + + # Update tracker + if tracker.last_section is None or section >= tracker.last_section: + tracker.last_section = section + tracker.last_section_line = lineno + + prev_blank[indent] = False + + # Skip function bodies (they contain JS, not QML structure) + brace_count = stripped.count("{") - stripped.count("}") + if brace_count > 0 and section == Section.FUNCTION: + func_skip_depth = brace_count + + # Skip JS blocks in bindings (signal handlers, attached handlers, + # and expression blocks like `color: { ... }`) + if brace_count > 0 and section == Section.BINDING: + colon_idx = stripped.index(":") + after_colon = stripped[colon_idx + 1 :].strip() + # If content after : doesn't start with an uppercase type name, + # it's a JS block (not an inline QML object like `contentItem: Rect {`) + if not re.match(r"^[A-Z]", after_colon): + func_skip_depth = brace_count + + # Child object/component opening resets deeper scopes + if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): + to_remove = [k for k in scopes if len(k) > len(indent)] + for k in to_remove: + del scopes[k] + prev_blank.pop(k, None) + + return violations + + +def main(): + fix_mode = "--fix" in sys.argv + qml_files = sorted(p for p in REPO_ROOT.rglob("*.qml") if "build" not in p.parts) + + if fix_mode: + fixed = sum(1 for f in qml_files if fix_file(f)) + print(f"{BOLD}Fixed {fixed} file(s).{RESET}\n") + + print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n") + + all_violations: list[Violation] = [] + for f in qml_files: + all_violations.extend(check_file(f)) + + for v in all_violations: + print(v) + + print() + if all_violations: + by_rule: dict[str, int] = {} + for v in all_violations: + by_rule[v.rule] = by_rule.get(v.rule, 0) + 1 + for rule, count in sorted(by_rule.items()): + print(f" {RULE_COLOURS.get(rule, '')}{rule}{RESET}: {count}") + print(f"\n{BOLD}Found {len(all_violations)} violation(s).{RESET}") + return 1 + else: + print(f"{BOLD}No violations found.{RESET}") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/services/Audio.qml b/services/Audio.qml index 908d1563c..6268b92ed 100644 --- a/services/Audio.qml +++ b/services/Audio.qml @@ -1,11 +1,12 @@ pragma Singleton -import qs.config -import Caelestia.Services -import Caelestia +import QtQuick import Quickshell +import Quickshell.Io import Quickshell.Services.Pipewire -import QtQuick +import Caelestia +import Caelestia.Config +import Caelestia.Services Singleton { id: root @@ -13,26 +14,9 @@ Singleton { property string previousSinkName: "" property string previousSourceName: "" - readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { - if (!node.isStream) { - if (node.isSink) - acc.sinks.push(node); - else if (node.audio) - acc.sources.push(node); - } else if (node.isStream && node.audio) { - // Application streams (output streams) - acc.streams.push(node); - } - return acc; - }, { - sources: [], - sinks: [], - streams: [] - }) - - readonly property list sinks: nodes.sinks - readonly property list sources: nodes.sources - readonly property list streams: nodes.streams + property list sinks: [] + property list sources: [] + property list streams: [] readonly property PwNode sink: Pipewire.defaultAudioSink readonly property PwNode source: Pipewire.defaultAudioSource @@ -49,31 +33,31 @@ Singleton { function setVolume(newVolume: real): void { if (sink?.ready && sink?.audio) { sink.audio.muted = false; - sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + sink.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); } } function incrementVolume(amount: real): void { - setVolume(volume + (amount || Config.services.audioIncrement)); + setVolume(volume + (amount || GlobalConfig.services.audioIncrement)); } function decrementVolume(amount: real): void { - setVolume(volume - (amount || Config.services.audioIncrement)); + setVolume(volume - (amount || GlobalConfig.services.audioIncrement)); } function setSourceVolume(newVolume: real): void { if (source?.ready && source?.audio) { source.audio.muted = false; - source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + source.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); } } function incrementSourceVolume(amount: real): void { - setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); + setSourceVolume(sourceVolume + (amount || GlobalConfig.services.audioIncrement)); } function decrementSourceVolume(amount: real): void { - setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); + setSourceVolume(sourceVolume - (amount || GlobalConfig.services.audioIncrement)); } function setAudioSink(newSink: PwNode): void { @@ -84,10 +68,19 @@ Singleton { Pipewire.preferredDefaultAudioSource = newSource; } + function cycleNextAudioOutput(): void { + if (sinks.length === 0) + return; + + const currentIndex = sinks.findIndex(s => s === sink); + const nextIndex = (currentIndex + 1) % sinks.length; + setAudioSink(sinks[nextIndex]); + } + function setStreamVolume(stream: PwNode, newVolume: real): void { if (stream?.ready && stream?.audio) { stream.audio.muted = false; - stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); + stream.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); } } @@ -109,7 +102,7 @@ Singleton { if (!stream) return qsTr("Unknown"); // Try application name first, then description, then name - return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); + return stream.properties["application.name"] || stream.description || stream.name || qsTr("Unknown Application"); } onSinkChanged: { @@ -118,7 +111,7 @@ Singleton { const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); - if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) + if (previousSinkName && previousSinkName !== newSinkName && GlobalConfig.utilities.toasts.audioOutputChanged) Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); previousSinkName = newSinkName; @@ -130,7 +123,7 @@ Singleton { const newSourceName = source.description || source.name || qsTr("Unknown Device"); - if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) + if (previousSourceName && previousSourceName !== newSourceName && GlobalConfig.utilities.toasts.audioInputChanged) Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); previousSourceName = newSourceName; @@ -141,6 +134,31 @@ Singleton { previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); } + Connections { + function onValuesChanged(): void { + const newSinks = []; + const newSources = []; + const newStreams = []; + + for (const node of Pipewire.nodes.values) { + if (!node.isStream) { + if (node.isSink) + newSinks.push(node); + else if (node.audio) + newSources.push(node); + } else if (node.audio) { + newStreams.push(node); + } + } + + root.sinks = newSinks; + root.sources = newSources; + root.streams = newStreams; + } + + target: Pipewire.nodes + } + PwObjectTracker { objects: [...root.sinks, ...root.sources, ...root.streams] } @@ -148,10 +166,18 @@ Singleton { CavaProvider { id: cava - bars: Config.services.visualiserBars + bars: GlobalConfig.services.visualiserBars } BeatTracker { id: beatTracker } + + IpcHandler { + function cycleOutput(): void { + root.cycleNextAudioOutput(); + } + + target: "audio" + } } diff --git a/services/Brightness.qml b/services/Brightness.qml index 12920eedd..ac34cbc1c 100644 --- a/services/Brightness.qml +++ b/services/Brightness.qml @@ -1,56 +1,62 @@ pragma Singleton pragma ComponentBehavior: Bound -import qs.config -import qs.components.misc +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia.Config +import qs.components.misc Singleton { id: root property list ddcMonitors: [] - readonly property list monitors: variants.instances + readonly property var ddcMonitorMap: { + const map = {}; + for (const m of ddcMonitors) + map[m.connector] = m; + return map; + } + readonly property list monitors: variants.instances // qmllint disable incompatible-type property bool appleDisplayPresent: false function getMonitorForScreen(screen: ShellScreen): var { - return monitors.find(m => m.modelData === screen); + return monitors.find(m => m.modelData === screen); // qmllint disable missing-property } function getMonitor(query: string): var { if (query === "active") { - return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); + return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property } if (query.startsWith("model:")) { const model = query.slice(6); - return monitors.find(m => m.modelData.model === model); + return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property } if (query.startsWith("serial:")) { const serial = query.slice(7); - return monitors.find(m => m.modelData.serialNumber === serial); + return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property } if (query.startsWith("id:")) { const id = parseInt(query.slice(3), 10); - return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); + return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property } - return monitors.find(m => m.modelData.name === query); + return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property } function increaseBrightness(): void { const monitor = getMonitor("active"); if (monitor) - monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); } function decreaseBrightness(): void { const monitor = getMonitor("active"); if (monitor) - monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); + monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); } onMonitorsChanged: { @@ -61,7 +67,7 @@ Singleton { Variants { id: variants - model: Quickshell.screens + model: Quickshell.screens // Don't respect excluded screens cause ipc Monitor {} } @@ -86,21 +92,23 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "brightnessUp" description: "Increase brightness" onPressed: root.increaseBrightness() } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "brightnessDown" description: "Decrease brightness" onPressed: root.decreaseBrightness() } IpcHandler { - target: "brightness" - function get(): real { return getFor("active"); } @@ -149,14 +157,17 @@ Singleton { return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; } + + target: "brightness" } component Monitor: QtObject { id: monitor required property ShellScreen modelData - readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name) - readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" + readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null + readonly property bool isDdc: ddcInfo !== null + readonly property string busNum: ddcInfo?.busNum ?? "" readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") property real brightness property real queuedBrightness: NaN diff --git a/services/Colours.qml b/services/Colours.qml index cd86c8fbf..922b51dbc 100644 --- a/services/Colours.qml +++ b/services/Colours.qml @@ -1,12 +1,13 @@ pragma Singleton pragma ComponentBehavior: Bound -import qs.config -import qs.utils -import Caelestia +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia +import Caelestia.Config +import qs.services +import qs.utils Singleton { id: root @@ -78,6 +79,21 @@ Singleton { Quickshell.execDetached(["caelestia", "scheme", "set", "--notify", "-m", mode]); } + function reloadHyprRules(): void { + const str = "keyword layerrule %1 %2, match:namespace caelestia-drawers"; + Hypr.extras.batchMessage([str.arg("blur").arg(transparency.enabled ? 1 : 0), str.arg("ignore_alpha").arg(transparency.base - 0.03)]); + } + + Component.onCompleted: debounceTimer.triggered() + + Connections { + function onConfigReloaded(): void { + root.reloadHyprRules(); + } + + target: Hypr + } + FileView { path: `${Paths.state}/scheme.json` watchChanges: true @@ -91,10 +107,20 @@ Singleton { source: Wallpapers.current } + Timer { + id: debounceTimer + + interval: 300 + onTriggered: root.reloadHyprRules() + } + component Transparency: QtObject { - readonly property bool enabled: Appearance.transparency.enabled - readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0) - readonly property real layers: Appearance.transparency.layers + readonly property bool enabled: Tokens.transparency.enabled + readonly property real base: Math.max(0, Math.min(1, Tokens.transparency.base - (root.light ? 0.1 : 0))) + readonly property real layers: Tokens.transparency.layers + + onEnabledChanged: debounceTimer.restart() + onBaseChanged: debounceTimer.restart() } component M3TPalette: QtObject { diff --git a/services/GameMode.qml b/services/GameMode.qml index 83770b79f..6dfc791c5 100644 --- a/services/GameMode.qml +++ b/services/GameMode.qml @@ -1,11 +1,11 @@ pragma Singleton -import qs.services -import qs.config -import Caelestia +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia +import Caelestia.Config +import qs.services Singleton { id: root @@ -28,11 +28,11 @@ Singleton { onEnabledChanged: { if (enabled) { setDynamicConfs(); - if (Config.utilities.toasts.gameModeChanged) + if (GlobalConfig.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad"); } else { Hypr.extras.message("reload"); - if (Config.utilities.toasts.gameModeChanged) + if (GlobalConfig.utilities.toasts.gameModeChanged) Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad"); } } @@ -40,23 +40,21 @@ Singleton { PersistentProperties { id: props - property bool enabled: Hypr.options["animations:enabled"] === 0 + property bool enabled: Hypr.options["animations:enabled"] === 0 // qmllint disable missing-property reloadableId: "gameMode" } Connections { - target: Hypr - function onConfigReloaded(): void { if (props.enabled) root.setDynamicConfs(); } + + target: Hypr } IpcHandler { - target: "gameMode" - function isEnabled(): bool { return props.enabled; } @@ -72,5 +70,7 @@ Singleton { function disable(): void { props.enabled = false; } + + target: "gameMode" } } diff --git a/services/Hypr.qml b/services/Hypr.qml index a654fdd34..181967e78 100644 --- a/services/Hypr.qml +++ b/services/Hypr.qml @@ -1,13 +1,13 @@ pragma Singleton -import qs.components.misc -import qs.config -import Caelestia -import Caelestia.Internal +import QtQuick import Quickshell import Quickshell.Hyprland import Quickshell.Io -import QtQuick +import Caelestia +import Caelestia.Config +import Caelestia.Internal +import qs.components.misc Singleton { id: root @@ -16,7 +16,10 @@ Singleton { readonly property var workspaces: Hyprland.workspaces readonly property var monitors: Hyprland.monitors - readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null + readonly property HyprlandToplevel activeToplevel: { + const t = Hyprland.activeToplevel; + return t?.workspace?.name.startsWith("special:") || Hyprland.focusedWorkspace?.toplevels.values.length > 0 ? t : null; + } readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor readonly property int activeWsId: focusedWorkspace?.id ?? 1 @@ -34,6 +37,7 @@ Singleton { readonly property alias devices: extras.devices property bool hadKeyboard + property string lastSpecialWorkspace: "" signal configReloaded @@ -41,6 +45,43 @@ Singleton { Hyprland.dispatch(request); } + function cycleSpecialWorkspace(direction: string): void { + const openSpecials = workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0); + + if (openSpecials.length === 0) + return; + + const activeSpecial = focusedMonitor.lastIpcObject.specialWorkspace.name ?? ""; + + if (!activeSpecial) { + if (lastSpecialWorkspace) { + const workspace = workspaces.values.find(w => w.name === lastSpecialWorkspace); + if (workspace && workspace.lastIpcObject.windows > 0) { + dispatch(`workspace ${lastSpecialWorkspace}`); + return; + } + } + dispatch(`workspace ${openSpecials[0].name}`); + return; + } + + const currentIndex = openSpecials.findIndex(w => w.name === activeSpecial); + let nextIndex = 0; + + if (currentIndex !== -1) { + if (direction === "next") + nextIndex = (currentIndex + 1) % openSpecials.length; + else + nextIndex = (currentIndex - 1 + openSpecials.length) % openSpecials.length; + } + + dispatch(`workspace ${openSpecials[nextIndex].name}`); + } + + function monitorNames(): list { + return monitors.values.map(e => e.name); + } + function monitorFor(screen: ShellScreen): HyprlandMonitor { return Hyprland.monitorFor(screen); } @@ -52,7 +93,7 @@ Singleton { Component.onCompleted: reloadDynamicConfs() onCapsLockChanged: { - if (!Config.utilities.toasts.capsLockChanged) + if (!GlobalConfig.utilities.toasts.capsLockChanged) return; if (capsLock) @@ -62,7 +103,7 @@ Singleton { } onNumLockChanged: { - if (!Config.utilities.toasts.numLockChanged) + if (!GlobalConfig.utilities.toasts.numLockChanged) return; if (numLock) @@ -72,15 +113,13 @@ Singleton { } onKbLayoutFullChanged: { - if (hadKeyboard && Config.utilities.toasts.kbLayoutChanged) + if (hadKeyboard && GlobalConfig.utilities.toasts.kbLayoutChanged) Toaster.toast(qsTr("Keyboard layout changed"), qsTr("Layout changed to: %1").arg(kbLayoutFull), "keyboard"); hadKeyboard = !!keyboard; } Connections { - target: Hyprland - function onRawEvent(event: HyprlandEvent): void { const n = event.name; if (n.endsWith("v2")) @@ -103,6 +142,20 @@ Singleton { Hyprland.refreshToplevels(); } } + + target: Hyprland + } + + Connections { + function onLastIpcObjectChanged(): void { + const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; + + if (specialName && specialName.startsWith("special:")) { + root.lastSpecialWorkspace = specialName; + } + } + + target: root.focusedMonitor } FileView { @@ -139,14 +192,24 @@ Singleton { } IpcHandler { - target: "hypr" - function refreshDevices(): void { extras.refreshDevices(); } + + function cycleSpecialWorkspace(direction: string): void { + root.cycleSpecialWorkspace(direction); + } + + function listSpecialWorkspaces(): string { + return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); + } + + target: "hypr" } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "refreshDevices" description: "Reload devices" onPressed: extras.refreshDevices() diff --git a/services/IdleInhibitor.qml b/services/IdleInhibitor.qml index 29409abc1..9f556b3a4 100644 --- a/services/IdleInhibitor.qml +++ b/services/IdleInhibitor.qml @@ -35,8 +35,6 @@ Singleton { } IpcHandler { - target: "idleInhibitor" - function isEnabled(): bool { return props.enabled; } @@ -52,5 +50,7 @@ Singleton { function disable(): void { props.enabled = false; } + + target: "idleInhibitor" } } diff --git a/services/LyricsService.qml b/services/LyricsService.qml new file mode 100644 index 000000000..26a75e9b5 --- /dev/null +++ b/services/LyricsService.qml @@ -0,0 +1,403 @@ +pragma Singleton + +import "../utils/scripts/lrcparser.js" as Lrc +import QtQuick +import Quickshell +import Quickshell.Io +import Caelestia +import Caelestia.Config +import qs.utils + +Singleton { + id: root + + property var player: Players.active + property int currentIndex: -1 + property bool loading: false + property bool isManualSeeking: false + property bool lyricsVisible: GlobalConfig.services.showLyrics + property string backend: "Local" + property string preferredBackend: GlobalConfig.services.lyricsBackend + property real currentSongId: 0 + property string loadedLocalFile: "" + property real offset + property int currentRequestId: 0 + property var lyricsMap: ({}) + + readonly property string lyricsDir: Paths.absolutePath(GlobalConfig.paths.lyricsDir) + readonly property string lyricsMapFile: lyricsDir + "/lyrics_map.json" + readonly property alias model: lyricsModel + readonly property alias candidatesModel: fetchedCandidatesModel + readonly property var _netEaseHeaders: ({ + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", + "Referer": "https://music.163.com/" + }) + + function getMetadata() { + if (!player || !player.metadata) + return null; + let artist = player.metadata["xesam:artist"]; + const title = player.metadata["xesam:title"]; + if (Array.isArray(artist)) + artist = artist.join(", "); + return { + artist: artist || "Unknown", + title: title || "Unknown" + }; + } + + function _metaKey(meta) { + return `${meta.artist} - ${meta.title}`; + } + + function savePrefs() { + let meta = getMetadata(); + if (!meta) + return; + let key = _metaKey(meta); + let existing = root.lyricsMap[key] ?? {}; + root.lyricsMap[key] = { + offset: root.offset, + backend: root.backend, + neteaseId: existing.neteaseId ?? null + }; + // reassign to notify QML bindings of the map change + root.lyricsMap = root.lyricsMap; + saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; + saveLyricsMap.running = true; + } + + function toggleVisibility() { + GlobalConfig.services.showLyrics = !GlobalConfig.services.showLyrics; + } + + function loadLyrics() { + loadDebounce.restart(); + } + + function _doLoadLyrics() { + const meta = getMetadata(); + if (!meta) { + lyricsModel.clear(); + root.currentIndex = -1; + return; + } + + loading = true; + lyricsModel.clear(); + currentIndex = -1; + root.currentSongId = 0; + + root.currentRequestId++; + let requestId = root.currentRequestId; + + let key = _metaKey(meta); + let saved = root.lyricsMap[key]; + root.offset = saved?.offset ?? 0.0; + + if (root.preferredBackend === "NetEase") { + root.backend = "NetEase"; + fetchNetEase(meta.title, meta.artist, requestId); + return; + } + + if (root.preferredBackend === "Local") { + root.backend = "Local"; + let cleanDir = lyricsDir.replace(/\/$/, ""); + let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; + + // Search for files matching "Artist - Title.lrc" pattern + const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); + const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); + const escapedTitle = titleStr.replace(/'/g, "'\\''"); + const escapedArtist = artistStr.replace(/'/g, "'\\''"); + findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; + findLyricsInSubdirs.requestId = requestId; + findLyricsInSubdirs.running = true; + + lrcFile.path = ""; + lrcFile.path = flatPath; + return; + } + + // Auto mode: try local first + root.backend = "Local"; + let cleanDir = lyricsDir.replace(/\/$/, ""); + let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; + + const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); + const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); + const escapedTitle = titleStr.replace(/'/g, "'\\''"); + const escapedArtist = artistStr.replace(/'/g, "'\\''"); + findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; + findLyricsInSubdirs.requestId = requestId; + findLyricsInSubdirs.running = true; + + lrcFile.path = ""; + lrcFile.path = flatPath; + fetchNetEaseCandidates(meta.title, meta.artist, requestId); + } + + function updateModel(parsedArray) { + root.currentIndex = -1; + lyricsModel.clear(); + for (let line of parsedArray) { + lyricsModel.append({ + time: line.time, + lyricLine: line.text + }); + } + } + + function fallbackToOnline() { + let meta = getMetadata(); + if (!meta) + return; + fetchNetEase(meta.title, meta.artist, root.currentRequestId); + } + + // NetEase + + // searches NetEase and populates the candidates model. returns the result array via the onResults callback + function _searchNetEase(title, artist, reqId, onResults) { + Requests.resetCookies(); + const query = encodeURIComponent(`${title} ${artist}`); + const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; + + Requests.get(url, text => { + if (reqId !== root.currentRequestId) + return; + const res = JSON.parse(text); + const songs = res.result?.songs || []; + + fetchedCandidatesModel.clear(); + for (let s of songs) { + fetchedCandidatesModel.append({ + id: s.id, + title: s.name || "Unknown Title", + artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" + }); + } + + onResults(songs); + }, err => {}, root._netEaseHeaders); + } + + // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. + function fetchNetEaseCandidates(title, artist, reqId) { + _searchNetEase(title, artist, reqId, _songs => {}); + } + + // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. + function fetchNetEase(title, artist, reqId) { + _searchNetEase(title, artist, reqId, songs => { + const bestMatch = songs.find(s => { + const inputArtist = String(artist || "").toLowerCase(); + const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); + return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); + }); + + if (!bestMatch) { + return; // No reliable lyrics found + } + + let key = `${artist} - ${title}`; + root.lyricsMap[key] = { + offset: root.lyricsMap[key]?.offset ?? 0.0, + backend: "NetEase", + neteaseId: bestMatch.id + }; + root.currentSongId = bestMatch.id; + savePrefs(); + fetchNetEaseLyrics(bestMatch.id, reqId); + }); + } + + function fetchNetEaseLyrics(id, reqId) { + const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; + Requests.get(url, text => { + if (reqId !== root.currentRequestId) + return; + const res = JSON.parse(text); + if (res.lrc?.lyric) { + updateModel(Lrc.parseLrc(res.lrc.lyric)); + loading = false; + } + }); + } + + function selectCandidate(songId) { + let meta = getMetadata(); + if (!meta) + return; + root.backend = "NetEase"; + root.currentSongId = songId; + let key = _metaKey(meta); + root.lyricsMap[key] = { + offset: root.lyricsMap[key]?.offset ?? 0.0, + neteaseId: songId + }; + savePrefs(); + fetchNetEaseLyrics(songId, currentRequestId); + } + + function updatePosition() { + if (isManualSeeking || loading || !player || lyricsModel.count === 0) + return; + + let pos = player.position - root.offset; + let newIdx = -1; + for (let i = lyricsModel.count - 1; i >= 0; i--) { + if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor + newIdx = i; + break; + } + } + + if (newIdx !== currentIndex) { + root.currentIndex = newIdx; + } + } + + function jumpTo(index, time) { + root.isManualSeeking = true; + root.currentIndex = index; + + if (player) { + player.position = time + root.offset + 0.01; // compensate for rounding + } + + seekTimer.restart(); + } + + onPreferredBackendChanged: { + if (GlobalConfig.services.lyricsBackend !== preferredBackend) { + GlobalConfig.services.lyricsBackend = preferredBackend; + } + } + + ListModel { + id: lyricsModel + } + + ListModel { + id: fetchedCandidatesModel + } + + Timer { + id: seekTimer + + interval: 500 + onTriggered: root.isManualSeeking = false + } + + // If no local lyrics were loaded within the interval, fall back to NetEase + Timer { + id: fallbackTimer + + interval: 200 + onTriggered: { + if (lyricsModel.count === 0) { + root.backend = "NetEase"; + fallbackToOnline(); + } + } + } + + Timer { + id: loadDebounce + + interval: 50 + onTriggered: root._doLoadLyrics() + } + + FileView { + id: lyricsMapFileView + + path: root.lyricsMapFile + printErrors: false + onLoaded: { + try { + root.lyricsMap = JSON.parse(text()); + } catch (e) { + root.lyricsMap = {}; + } + } + } + + FileView { + id: lrcFile + + printErrors: false + onLoaded: { + fallbackTimer.stop(); + let parsed = Lrc.parseLrc(text()); + if (parsed.length > 0) { + root.backend = "Local"; + root.loadedLocalFile = path; + updateModel(parsed); + loading = false; + } else if (root.preferredBackend === "Local") { + // Local mode only - fail immediately + root.backend = "NetEase"; + fallbackToOnline(); + } + // In Auto mode, let the Process onExited handle fallback + } + } + + Connections { + function onActiveChanged() { + root.player = Players.active; + loadLyrics(); + } + + target: Players + } + + Connections { + function onMetadataChanged() { + loadLyrics(); + } + + target: root.player + ignoreUnknownSignals: true + } + + Process { + id: saveLyricsMap + + command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] + } + + Process { + id: findLyricsInSubdirs + + property int requestId: -1 + property bool foundFile: false + + stdout: SplitParser { + onRead: data => { + if (findLyricsInSubdirs.requestId === root.currentRequestId) { + const foundPath = data.trim(); + if (foundPath && foundPath.length > 0) { + findLyricsInSubdirs.foundFile = true; + fallbackTimer.stop(); + root.loadedLocalFile = foundPath; + lrcFile.path = ""; + lrcFile.path = foundPath; + } + } + } + } + + onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters + if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") { + if (lyricsModel.count === 0) { + fallbackTimer.restart(); + } + } + foundFile = false; + } + } +} diff --git a/services/Monitors.qml b/services/Monitors.qml index e64d14e5f..c880ada5e 100644 --- a/services/Monitors.qml +++ b/services/Monitors.qml @@ -29,29 +29,37 @@ Singleton { identifyTimer.stop(); } - // Safely iterate UntypedObjectModel — .find() doesn't work on it + function sourceMonitors(): var { + if ((Hyprctl.monitors?.length ?? 0) > 0) + return Hyprctl.monitors; + return Hypr.monitors.values ?? []; + } + + // Safely iterate monitor data — .find() doesn't work on UntypedObjectModel function findMonitorByName(name: string): var { - for (let i = 0; i < Hypr.monitors.length; i++) { - if (Hypr.monitors[i].name === name) - return Hypr.monitors[i]; + const monitors = sourceMonitors(); + for (let i = 0; i < monitors.length; i++) { + if (monitors[i].name === name) + return monitors[i]; } return null; } function findMonitorById(id: int): var { - for (let i = 0; i < Hypr.monitors.length; i++) { - if (Hypr.monitors[i].id === id) - return Hypr.monitors[i]; + const monitors = sourceMonitors(); + for (let i = 0; i < monitors.length; i++) { + if (monitors[i].id === id) + return monitors[i]; } return null; } // Build the monitor string Hyprland expects: // NAME,WIDTHxHEIGHT@RATE,XxY,SCALE[,transform,N] - function monitorStr(mon: var, overrideScale: real, overrideTransform: int): string { - const scale = overrideScale >= 0 ? overrideScale : (mon.scale || 1); + function monitorStr(mon: var, overrideScale: real, overrideTransform: int, overrideRefreshRate: real): string { + const scale = overrideScale >= 0 ? overrideScale : (mon.scale || 1); const transform = overrideTransform >= 0 ? overrideTransform : (mon.transform || 0); - const rr = (mon.refreshRate || 60).toFixed(3); + const rr = (overrideRefreshRate > 0 ? overrideRefreshRate : (mon.refreshRate || 60)).toFixed(3); let s = `${mon.name},${mon.width}x${mon.height}@${rr},${mon.x}x${mon.y},${scale}`; if (transform !== 0) s += `,transform,${transform}`; @@ -62,6 +70,7 @@ Singleton { // "keyword" is a config command, not a dispatcher action. function sendKeyword(monStr: string): void { Hypr.extras.batchMessage([`keyword monitor ${monStr}`]); + Hyprctl.update(); } function arrange(monitorName: string, pos: string, relativeToId: int): void { @@ -82,7 +91,7 @@ Singleton { else if (pos === "top") y -= movingH; else if (pos === "bottom") y += targetH; - sendKeyword(monitorStr(moving, moving.scale || 1, moving.transform || 0) + sendKeyword(monitorStr(moving, moving.scale || 1, moving.transform || 0, moving.refreshRate || 60) .replace(`${moving.x}x${moving.y}`, `${Math.round(x)}x${Math.round(y)}`)); } @@ -95,13 +104,19 @@ Singleton { else if (angle === 180) transform = 2; else if (angle === 270) transform = 3; - sendKeyword(monitorStr(mon, mon.scale || 1, transform)); + sendKeyword(monitorStr(mon, mon.scale || 1, transform, mon.refreshRate || 60)); } function setScale(monitorName: string, scale: real): void { const mon = findMonitorByName(monitorName); if (!mon) return; const s = Math.max(0.5, Math.min(3.0, scale)); - sendKeyword(monitorStr(mon, s, mon.transform || 0)); + sendKeyword(monitorStr(mon, s, mon.transform || 0, mon.refreshRate || 60)); + } + + function setRefreshRate(monitorName: string, refreshRate: real): void { + const mon = findMonitorByName(monitorName); + if (!mon) return; + sendKeyword(monitorStr(mon, mon.scale || 1, mon.transform || 0, Math.max(1, refreshRate))); } } diff --git a/services/Network.qml b/services/Network.qml index f3dfc3ea1..4e0b809ca 100644 --- a/services/Network.qml +++ b/services/Network.qml @@ -1,44 +1,28 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Io -import QtQuick import qs.services Singleton { id: root - Component.onCompleted: { - // Trigger ethernet device detection after initialization - Qt.callLater(() => { - getEthernetDevices(); - }); - // Load saved connections on startup - Nmcli.loadSavedConnections(() => { - root.savedConnections = Nmcli.savedConnections; - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - }); - // Get initial WiFi status - Nmcli.getWifiStatus(enabled => { - root.wifiEnabled = enabled; - }); - // Sync networks from Nmcli on startup - Qt.callLater(() => { - syncNetworksFromNmcli(); - }, 100); - } - readonly property list networks: [] readonly property AccessPoint active: networks.find(n => n.active) ?? null property bool wifiEnabled: true readonly property bool scanning: Nmcli.scanning - property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null property int ethernetDeviceCount: 0 property bool ethernetProcessRunning: false property var ethernetDeviceDetails: null property var wirelessDeviceDetails: null + property var pendingConnection: null + property list savedConnections: [] + property list savedConnectionSsids: [] + + signal connectionFailed(string ssid) function enableWifi(enabled: bool): void { Nmcli.enableWifi(enabled, result => { @@ -66,9 +50,6 @@ Singleton { Nmcli.rescanWifi(); } - property var pendingConnection: null - signal connectionFailed(string ssid) - function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { // Set up pending connection tracking if callback provided if (callback) { @@ -159,20 +140,6 @@ Singleton { }); } - property list savedConnections: [] - property list savedConnectionSsids: [] - - // Sync saved connections from Nmcli when they're updated - Connections { - target: Nmcli - function onSavedConnectionsChanged() { - root.savedConnections = Nmcli.savedConnections; - } - function onSavedConnectionSsidsChanged() { - root.savedConnectionSsids = Nmcli.savedConnectionSsids; - } - } - function syncNetworksFromNmcli(): void { const rNetworks = root.networks; const nNetworks = Nmcli.networks; @@ -217,22 +184,6 @@ Singleton { } } - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } - - Component { - id: apComp - AccessPoint {} - } - function hasSavedProfile(ssid: string): bool { // Use Nmcli's hasSavedProfile which has the same logic return Nmcli.hasSavedProfile(ssid); @@ -309,16 +260,73 @@ Singleton { return octets.join("."); } + Component.onCompleted: { + // Trigger ethernet device detection after initialization + Qt.callLater(() => { + getEthernetDevices(); + }); + // Load saved connections on startup + Nmcli.loadSavedConnections(() => { + root.savedConnections = Nmcli.savedConnections; + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + }); + // Get initial WiFi status + Nmcli.getWifiStatus(enabled => { + root.wifiEnabled = enabled; + }); + // Sync networks from Nmcli on startup + Qt.callLater(() => { + syncNetworksFromNmcli(); + }, 100); + } + + // Sync saved connections from Nmcli when they're updated + Connections { + function onSavedConnectionsChanged() { + root.savedConnections = Nmcli.savedConnections; + } + + function onSavedConnectionSsidsChanged() { + root.savedConnectionSsids = Nmcli.savedConnectionSsids; + } + + target: Nmcli + } + + Timer { + id: monitorDebounce + + interval: 200 + onTriggered: { + Nmcli.getNetworks(() => { + syncNetworksFromNmcli(); + }); + getEthernetDevices(); + } + } + Process { running: true command: ["nmcli", "m"] stdout: SplitParser { - onRead: { - Nmcli.getNetworks(() => { - syncNetworksFromNmcli(); - }); - getEthernetDevices(); - } + onRead: monitorDebounce.start() } } + + Component { + id: apComp + + AccessPoint {} + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } } diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml new file mode 100644 index 000000000..6c4dc8d25 --- /dev/null +++ b/services/NetworkUsage.qml @@ -0,0 +1,229 @@ +pragma Singleton + +import QtQuick +import Quickshell +import Quickshell.Io +import Caelestia.Config +import Caelestia.Internal + +Singleton { + id: root + + property int refCount: 0 + + // Current speeds in bytes per second + readonly property real downloadSpeed: _downloadSpeed + readonly property real uploadSpeed: _uploadSpeed + + // Total bytes transferred since tracking started + readonly property real downloadTotal: _downloadTotal + readonly property real uploadTotal: _uploadTotal + + // History buffers for sparkline + readonly property CircularBuffer downloadBuffer: _downloadBuffer + readonly property CircularBuffer uploadBuffer: _uploadBuffer + readonly property int historyLength: 30 + + // Private properties + property real _downloadSpeed: 0 + property real _uploadSpeed: 0 + property real _downloadTotal: 0 + property real _uploadTotal: 0 + + // Previous readings for calculating speed + property real _prevRxBytes: 0 + property real _prevTxBytes: 0 + property real _prevTimestamp: 0 + + // Initial readings for calculating totals + property real _initialRxBytes: 0 + property real _initialTxBytes: 0 + property bool _initialized: false + + function formatBytes(bytes: real): var { + // Handle negative or invalid values + if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { + return { + value: 0, + unit: "B/s" + }; + } + + if (bytes < 1024) { + return { + value: bytes, + unit: "B/s" + }; + } else if (bytes < 1024 * 1024) { + return { + value: bytes / 1024, + unit: "KB/s" + }; + } else if (bytes < 1024 * 1024 * 1024) { + return { + value: bytes / (1024 * 1024), + unit: "MB/s" + }; + } else { + return { + value: bytes / (1024 * 1024 * 1024), + unit: "GB/s" + }; + } + } + + function formatBytesTotal(bytes: real): var { + // Handle negative or invalid values + if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { + return { + value: 0, + unit: "B" + }; + } + + if (bytes < 1024) { + return { + value: bytes, + unit: "B" + }; + } else if (bytes < 1024 * 1024) { + return { + value: bytes / 1024, + unit: "KB" + }; + } else if (bytes < 1024 * 1024 * 1024) { + return { + value: bytes / (1024 * 1024), + unit: "MB" + }; + } else { + return { + value: bytes / (1024 * 1024 * 1024), + unit: "GB" + }; + } + } + + function parseNetDev(content: string): var { + const lines = content.split("\n"); + let totalRx = 0; + let totalTx = 0; + + for (let i = 2; i < lines.length; i++) { + const line = lines[i].trim(); + if (!line) + continue; + + const parts = line.split(/\s+/); + if (parts.length < 10) + continue; + + const iface = parts[0].replace(":", ""); + // Skip loopback interface + if (iface === "lo") + continue; + + const rxBytes = parseFloat(parts[1]) || 0; + const txBytes = parseFloat(parts[9]) || 0; + + totalRx += rxBytes; + totalTx += txBytes; + } + + return { + rx: totalRx, + tx: totalTx + }; + } + + CircularBuffer { + id: _downloadBuffer + + capacity: root.historyLength + 1 + } + + CircularBuffer { + id: _uploadBuffer + + capacity: root.historyLength + 1 + } + + FileView { + id: netDevFile + + path: "/proc/net/dev" + } + + Timer { + interval: GlobalConfig.dashboard.resourceUpdateInterval + running: root.refCount > 0 + repeat: true + triggeredOnStart: true + + onTriggered: { + netDevFile.reload(); + const content = netDevFile.text(); + if (!content) + return; + + const data = root.parseNetDev(content); + const now = Date.now(); + + if (!root._initialized) { + root._initialRxBytes = data.rx; + root._initialTxBytes = data.tx; + root._prevRxBytes = data.rx; + root._prevTxBytes = data.tx; + root._prevTimestamp = now; + root._initialized = true; + return; + } + + const timeDelta = (now - root._prevTimestamp) / 1000; // seconds + if (timeDelta > 0) { + // Calculate byte deltas + let rxDelta = data.rx - root._prevRxBytes; + let txDelta = data.tx - root._prevTxBytes; + + // Handle counter overflow (when counters wrap around from max to 0) + // This happens when counters exceed 32-bit or 64-bit limits + if (rxDelta < 0) { + // Counter wrapped around - assume 64-bit counter + rxDelta += Math.pow(2, 64); + } + if (txDelta < 0) { + txDelta += Math.pow(2, 64); + } + + // Calculate speeds + root._downloadSpeed = rxDelta / timeDelta; + root._uploadSpeed = txDelta / timeDelta; + + if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) + _downloadBuffer.push(root._downloadSpeed); + + if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) + _uploadBuffer.push(root._uploadSpeed); + } + + // Calculate totals with overflow handling + let downTotal = data.rx - root._initialRxBytes; + let upTotal = data.tx - root._initialTxBytes; + + // Handle counter overflow for totals + if (downTotal < 0) { + downTotal += Math.pow(2, 64); + } + if (upTotal < 0) { + upTotal += Math.pow(2, 64); + } + + root._downloadTotal = downTotal; + root._uploadTotal = upTotal; + + root._prevRxBytes = data.rx; + root._prevTxBytes = data.tx; + root._prevTimestamp = now; + } + } +} diff --git a/services/Nmcli.qml b/services/Nmcli.qml index 36bd3e6df..ea5d7eb31 100644 --- a/services/Nmcli.qml +++ b/services/Nmcli.qml @@ -1,9 +1,9 @@ pragma Singleton pragma ComponentBehavior: Bound +import QtQuick import Quickshell import Quickshell.Io -import QtQuick Singleton { id: root @@ -24,14 +24,15 @@ Singleton { property var wifiConnectionQueue: [] property int currentSsidQueryIndex: 0 property var pendingConnection: null - signal connectionFailed(string ssid) property var wirelessDeviceDetails: null property var ethernetDeviceDetails: null property list ethernetDevices: [] readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - property list activeProcesses: [] + readonly property alias connectionCheckTimer: connectionCheckTimer + readonly property alias immediateCheckTimer: immediateCheckTimer + // Constants readonly property string deviceTypeWifi: "wifi" readonly property string deviceTypeEthernet: "ethernet" @@ -55,6 +56,8 @@ Singleton { readonly property string connectionParamPassword: "password" readonly property string connectionParamBssid: "802-11-wireless.bssid" + signal connectionFailed(string ssid) + function detectPasswordRequired(error: string): bool { if (!error || error.length === 0) { return false; @@ -165,7 +168,7 @@ Singleton { function executeCommand(args: list, callback: var): void { const proc = commandProc.createObject(root); - proc.command = ["nmcli", ...args]; + proc.cmdArgs = ["nmcli", ...args]; proc.callback = callback; activeProcesses.push(proc); @@ -178,7 +181,7 @@ Singleton { }); Qt.callLater(() => { - proc.exec(proc.command); + proc.exec(proc.cmdArgs); }); } @@ -388,7 +391,7 @@ Singleton { } if (!result.success && root.pendingConnection && retries < maxRetries) { - console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); + console.warn(lc, "Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); Qt.callLater(() => { connectWireless(ssid, password, bssid, callback, retries + 1); }, 1000); @@ -414,7 +417,7 @@ Singleton { loadSavedConnections(() => {}); activateConnection(ssid, callback); } else { - console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); + console.warn(lc, "Connection profile creation failed, trying fallback..."); let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; executeCommand(fallbackCmd, fallbackResult => { if (callback) @@ -751,17 +754,25 @@ Singleton { const networks = deduplicateNetworks(allNetworks); const rNetworks = root.networks; - const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); - for (const network of destroyed) { - const index = rNetworks.indexOf(network); - if (index >= 0) { - rNetworks.splice(index, 1); - network.destroy(); + const newMap = new Map(); + for (const n of networks) + newMap.set(`${n.frequency}:${n.ssid}:${n.bssid}`, n); + + for (let i = rNetworks.length - 1; i >= 0; i--) { + const rn = rNetworks[i]; + const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; + if (!newMap.has(key)) { + rNetworks.splice(i, 1); + rn.destroy(); } } - for (const network of networks) { - const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); + const existingMap = new Map(); + for (const rn of rNetworks) + existingMap.set(`${rn.frequency}:${rn.ssid}:${rn.bssid}`, rn); + + for (const [key, network] of newMap) { + const match = existingMap.get(key); if (match) { match.lastIpcObject = network; } else { @@ -829,7 +840,7 @@ Singleton { return false; } - if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { + if (!isConnectionCommand(proc.cmdArgs) || !root.pendingConnection || !root.pendingConnection.callback) { return false; } @@ -861,247 +872,6 @@ Singleton { return false; } - component CommandProcess: Process { - id: proc - - property var callback: null - property list command: [] - property bool callbackCalled: false - property int exitCode: 0 - - signal processFinished - - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - - stdout: StdioCollector { - id: stdoutCollector - } - - stderr: StdioCollector { - id: stderrCollector - - onStreamFinished: { - const error = text.trim(); - if (error && error.length > 0) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - root.handlePasswordRequired(proc, error, output, -1); - } - } - } - - onExited: code => { - exitCode = code; - - Qt.callLater(() => { - if (callbackCalled) { - processFinished(); - return; - } - - if (proc.callback) { - const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; - const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; - const success = exitCode === 0; - const cmdIsConnection = isConnectionCommand(proc.command); - - if (root.handlePasswordRequired(proc, error, output, exitCode)) { - processFinished(); - return; - } - - const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); - - if (!success && cmdIsConnection && root.pendingConnection) { - const failedSsid = root.pendingConnection.ssid; - root.connectionFailed(failedSsid); - } - - callbackCalled = true; - callback({ - success: success, - output: output, - error: error, - exitCode: proc.exitCode, - needsPassword: needsPassword || false - }); - processFinished(); - } else { - processFinished(); - } - }); - } - } - - Component { - id: commandProc - - CommandProcess {} - } - - component AccessPoint: QtObject { - required property var lastIpcObject - readonly property string ssid: lastIpcObject.ssid - readonly property string bssid: lastIpcObject.bssid - readonly property int strength: lastIpcObject.strength - readonly property int frequency: lastIpcObject.frequency - readonly property bool active: lastIpcObject.active - readonly property string security: lastIpcObject.security - readonly property bool isSecure: security.length > 0 - } - - Component { - id: apComp - - AccessPoint {} - } - - Timer { - id: connectionCheckTimer - - interval: 4000 - onTriggered: { - if (root.pendingConnection) { - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (!connected && root.pendingConnection.callback) { - let foundPasswordError = false; - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection) { - const pending = root.pendingConnection; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - foundPasswordError = true; - break; - } - } - } - } - } - - if (!foundPasswordError) { - const pending = root.pendingConnection; - const failedSsid = pending.ssid; - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - root.connectionFailed(failedSsid); - pending.callback({ - success: false, - output: "", - error: "Connection timeout", - exitCode: -1, - needsPassword: false - }); - } - } else if (connected) { - root.pendingConnection = null; - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - } - - Timer { - id: immediateCheckTimer - - property int checkCount: 0 - - interval: 500 - repeat: true - triggeredOnStart: false - - onTriggered: { - if (root.pendingConnection) { - checkCount++; - const connected = root.active && root.active.ssid === root.pendingConnection.ssid; - - if (connected) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - if (root.pendingConnection.callback) { - root.pendingConnection.callback({ - success: true, - output: "Connected", - error: "", - exitCode: 0 - }); - } - root.pendingConnection = null; - } else { - for (let i = 0; i < root.activeProcesses.length; i++) { - const proc = root.activeProcesses[i]; - if (proc && proc.stderr && proc.stderr.text) { - const error = proc.stderr.text.trim(); - if (error && error.length > 0) { - if (root.isConnectionCommand(proc.command)) { - const needsPassword = root.detectPasswordRequired(error); - - if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { - connectionCheckTimer.stop(); - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - const pending = root.pendingConnection; - root.pendingConnection = null; - proc.callbackCalled = true; - const result = { - success: false, - output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", - error: error, - exitCode: -1, - needsPassword: true - }; - if (pending.callback) { - pending.callback(result); - } - if (proc.callback && proc.callback !== pending.callback) { - proc.callback(result); - } - return; - } - } - } - } - } - - if (checkCount >= 6) { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } else { - immediateCheckTimer.stop(); - immediateCheckTimer.checkCount = 0; - } - } - } - function checkPendingConnection(): void { if (root.pendingConnection) { Qt.callLater(() => { @@ -1253,39 +1023,9 @@ Singleton { return details; } - Process { - id: rescanProc - - command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] - onExited: root.getNetworks() - } - - Process { - id: monitorProc - - running: true - command: ["nmcli", "monitor"] - environment: ({ - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: SplitParser { - onRead: root.refreshOnConnectionChange() - } - onExited: monitorRestartTimer.start() - } - - Timer { - id: monitorRestartTimer - interval: 2000 - onTriggered: { - monitorProc.running = true; - } - } - - function refreshOnConnectionChange(): void { - getNetworks(networks => { - const newActive = root.active; + function refreshOnConnectionChange(): void { + getNetworks(networks => { + const newActive = root.active; if (newActive && newActive.active) { Qt.callLater(() => { @@ -1349,4 +1089,283 @@ Singleton { } }, 2000); } + + Component { + id: commandProc + + CommandProcess {} + } + + Component { + id: apComp + + AccessPoint {} + } + + Timer { + id: connectionCheckTimer + + interval: 4000 + onTriggered: { + if (root.pendingConnection) { + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (!connected && root.pendingConnection.callback) { + let foundPasswordError = false; + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.cmdArgs)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection) { + const pending = root.pendingConnection; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + foundPasswordError = true; + break; + } + } + } + } + } + + if (!foundPasswordError) { + const pending = root.pendingConnection; + const failedSsid = pending.ssid; + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + root.connectionFailed(failedSsid); + pending.callback({ + success: false, + output: "", + error: "Connection timeout", + exitCode: -1, + needsPassword: false + }); + } + } else if (connected) { + root.pendingConnection = null; + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + } + + Timer { + id: immediateCheckTimer + + property int checkCount: 0 + + interval: 500 + repeat: true + triggeredOnStart: false + + onTriggered: { + if (root.pendingConnection) { + checkCount++; + const connected = root.active && root.active.ssid === root.pendingConnection.ssid; + + if (connected) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + if (root.pendingConnection.callback) { + root.pendingConnection.callback({ + success: true, + output: "Connected", + error: "", + exitCode: 0 + }); + } + root.pendingConnection = null; + } else { + for (let i = 0; i < root.activeProcesses.length; i++) { + const proc = root.activeProcesses[i]; + if (proc && proc.stderr && proc.stderr.text) { + const error = proc.stderr.text.trim(); + if (error && error.length > 0) { + if (root.isConnectionCommand(proc.cmdArgs)) { + const needsPassword = root.detectPasswordRequired(error); + + if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { + connectionCheckTimer.stop(); + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + const pending = root.pendingConnection; + root.pendingConnection = null; + proc.callbackCalled = true; + const result = { + success: false, + output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", + error: error, + exitCode: -1, + needsPassword: true + }; + if (pending.callback) { + pending.callback(result); + } + if (proc.callback && proc.callback !== pending.callback) { + proc.callback(result); + } + return; + } + } + } + } + } + + if (checkCount >= 6) { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } else { + immediateCheckTimer.stop(); + immediateCheckTimer.checkCount = 0; + } + } + } + + Process { + id: rescanProc + + command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] + onExited: root.getNetworks() // qmllint disable signal-handler-parameters + } + + Process { + id: monitorProc + + running: true + command: ["nmcli", "monitor"] + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + stdout: SplitParser { + onRead: root.refreshOnConnectionChange() + } + onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters + } + + Timer { + id: monitorRestartTimer + + interval: 2000 + onTriggered: { + monitorProc.running = true; + } + } + + LoggingCategory { + id: lc + + name: "caelestia.qml.services.nmcli" + defaultLogLevel: LoggingCategory.Info + } + + component CommandProcess: Process { + id: proc + + property var callback: null + property list cmdArgs: [] + property bool callbackCalled: false + property int exitCode: 0 + + signal processFinished + + environment: ({ + LANG: "C.UTF-8", + LC_ALL: "C.UTF-8" + }) + + stdout: StdioCollector { + id: stdoutCollector + } + + stderr: StdioCollector { + id: stderrCollector + + onStreamFinished: { + const error = text.trim(); + if (error && error.length > 0) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + root.handlePasswordRequired(proc, error, output, -1); + } + } + } + + onExited: code => { // qmllint disable signal-handler-parameters + exitCode = code; + + Qt.callLater(() => { + if (callbackCalled) { + processFinished(); + return; + } + + if (proc.callback) { + const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; + const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; + const success = exitCode === 0; + const cmdIsConnection = isConnectionCommand(proc.cmdArgs); + + if (root.handlePasswordRequired(proc, error, output, exitCode)) { + processFinished(); + return; + } + + const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); + + if (!success && cmdIsConnection && root.pendingConnection) { + const failedSsid = root.pendingConnection.ssid; + root.connectionFailed(failedSsid); + } + + callbackCalled = true; + callback({ + success: success, + output: output, + error: error, + exitCode: proc.exitCode, + needsPassword: needsPassword || false + }); + processFinished(); + } else { + processFinished(); + } + }); + } + } + + component AccessPoint: QtObject { + required property var lastIpcObject + readonly property string ssid: lastIpcObject.ssid + readonly property string bssid: lastIpcObject.bssid + readonly property int strength: lastIpcObject.strength + readonly property int frequency: lastIpcObject.frequency + readonly property bool active: lastIpcObject.active + readonly property string security: lastIpcObject.security + readonly property bool isSecure: security.length > 0 + } } diff --git a/services/NotifData.qml b/services/NotifData.qml new file mode 100644 index 000000000..3c6ae23b6 --- /dev/null +++ b/services/NotifData.qml @@ -0,0 +1,243 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Services.Notifications +import Caelestia +import Caelestia.Config +import qs.services +import qs.utils + +QtObject { + id: notif + + property bool popup + property bool closed + property var locks: new Set() + + property date time: new Date() + property string timeStr: qsTr("now") + + readonly property Timer timeStrTimer: Timer { + running: !notif.closed + repeat: true + interval: 5000 + onTriggered: notif.updateTimeStr() + } + + property Notification notification + property string id + property string summary + property string body + property string appIcon + property string appName + property string image + property var hints // Hints are not persisted across restarts + property real expireTimeout: GlobalConfig.notifs.defaultExpireTimeout + property int urgency: NotificationUrgency.Normal + property bool resident + property bool hasActionIcons + property list actions + + readonly property bool hasFullscreen: { + const monitor = Hypr.focusedMonitor; + const specialName = monitor?.lastIpcObject.specialWorkspace?.name; + if (specialName) { + const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); + return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + + readonly property Timer timer: Timer { + running: true + interval: notif.expireTimeout > 0 ? notif.expireTimeout : notif.hasFullscreen ? GlobalConfig.notifs.fullscreenExpireTimeout : GlobalConfig.notifs.defaultExpireTimeout + onTriggered: { + // Always expire if the active workspace has a fullscreen window + if (GlobalConfig.notifs.expire || notif.hasFullscreen) + notif.popup = false; + } + } + + readonly property LazyLoader dummyImageLoader: LazyLoader { + active: false + + // qmllint disable uncreatable-type + PanelWindow { + // qmllint enable uncreatable-type + implicitWidth: TokenConfig.sizes.notifs.image + implicitHeight: TokenConfig.sizes.notifs.image + color: "transparent" + mask: Region {} + + Image { + function tryCache(): void { + if (status !== Image.Ready || width != TokenConfig.sizes.notifs.image || height != TokenConfig.sizes.notifs.image) + return; + + const cacheKey = notif.appName + notif.summary + notif.id + notif.image; + let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; + for (let i = 0; i < cacheKey.length; i++) { + ch = cacheKey.charCodeAt(i); + h1 = Math.imul(h1 ^ ch, 2654435761); + h2 = Math.imul(h2 ^ ch, 1597334677); + } + h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); + h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); + h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); + h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); + const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); + + const cache = `${Paths.notifimagecache}/${hash}.png`; + CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { + notif.image = cache; + notif.dummyImageLoader.active = false; + }); + } + + anchors.fill: parent + source: Qt.resolvedUrl(notif.image) + fillMode: Image.PreserveAspectCrop + cache: false + asynchronous: true + opacity: 0 + + onStatusChanged: tryCache() + onWidthChanged: tryCache() + onHeightChanged: tryCache() + } + } + } + + readonly property Connections conn: Connections { + function onClosed(): void { + notif.close(); + } + + function onSummaryChanged(): void { + notif.summary = notif.notification.summary; + } + + function onBodyChanged(): void { + notif.body = notif.notification.body; + } + + function onAppIconChanged(): void { + notif.appIcon = notif.notification.appIcon; + } + + function onAppNameChanged(): void { + notif.appName = notif.notification.appName; + } + + function onImageChanged(): void { + notif.image = notif.notification.image; + notif.maybeTriggerDummyImageLoader(); + } + + function onExpireTimeoutChanged(): void { + notif.expireTimeout = notif.notification.expireTimeout; + } + + function onUrgencyChanged(): void { + notif.urgency = notif.notification.urgency; + } + + function onResidentChanged(): void { + notif.resident = notif.notification.resident; + } + + function onHasActionIconsChanged(): void { + notif.hasActionIcons = notif.notification.hasActionIcons; + } + + function onActionsChanged(): void { + // qmllint disable unresolved-type + notif.actions = notif.notification.actions.map(a => ({ + // qmllint enable unresolved-type + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } + + function onHintsChanged(): void { + notif.hints = notif.notification.hints; + } + + target: notif.notification + } + + function updateTimeStr(): void { + const diff = Date.now() - time.getTime(); + const m = Math.floor(diff / 60000); + + if (m < 1) { + timeStr = qsTr("now"); + timeStrTimer.interval = 5000; + } else { + const h = Math.floor(m / 60); + const d = Math.floor(h / 24); + + if (d > 0) { + timeStr = `${d}d`; + timeStrTimer.interval = 3600000; + } else if (h > 0) { + timeStr = `${h}h`; + timeStrTimer.interval = 300000; + } else { + timeStr = `${m}m`; + timeStrTimer.interval = m < 10 ? 30000 : 60000; + } + } + } + + function maybeTriggerDummyImageLoader(): void { + if (image && !image.startsWith("image://icon/") && !image.startsWith(Paths.notifimagecache)) + dummyImageLoader.active = true; + } + + function lock(item: Item): void { + locks.add(item); + } + + function unlock(item: Item): void { + locks.delete(item); + if (closed) + close(); + } + + function close(): void { + closed = true; + if (locks.size === 0 && Notifs.list.includes(this)) { + Notifs.list = Notifs.list.filter(n => n !== this); + notification?.dismiss(); + destroy(); + } + } + + Component.onCompleted: { + if (!notification) + return; + + id = notification.id; + summary = notification.summary; + body = notification.body; + appIcon = notification.appIcon; + appName = notification.appName; + image = notification.image; + maybeTriggerDummyImageLoader(); + expireTimeout = notification.expireTimeout; + hints = notification.hints; + urgency = notification.urgency; + resident = notification.resident; + hasActionIcons = notification.hasActionIcons; + // qmllint disable unresolved-type + actions = notification.actions.map(a => ({ + // qmllint enable unresolved-type + identifier: a.identifier, + text: a.text, + invoke: () => a.invoke() + })); + } +} diff --git a/services/Notifs.qml b/services/Notifs.qml index 2ebc32db1..d539d0a28 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -1,27 +1,44 @@ pragma Singleton pragma ComponentBehavior: Bound -import qs.components.misc -import qs.config -import qs.utils -import Caelestia +import QtQuick import Quickshell import Quickshell.Io import Quickshell.Services.Notifications -import QtQuick +import Caelestia +import Caelestia.Config +import qs.components.misc +import qs.services +import qs.utils Singleton { id: root - property list list: [] - readonly property list notClosed: list.filter(n => !n.closed) - readonly property list popups: list.filter(n => n.popup) + property list list: [] + readonly property list notClosed: list.filter(n => !n.closed) + readonly property list popups: list.filter(n => n.popup) property alias dnd: props.dnd property bool loaded + function hasFullscreen(): bool { + for (const monitor of Hypr.monitors.values) { + if (monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1)) + return true; + } + return false; + } + + function shouldShowPopup(): bool { + if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) + return false; + if (GlobalConfig.notifs.fullscreen === "off" && hasFullscreen()) + return false; + return true; + } + onDndChanged: { - if (!Config.utilities.toasts.dndChanged) + if (!GlobalConfig.utilities.toasts.dndChanged) return; if (dnd) @@ -78,7 +95,7 @@ Singleton { notif.tracked = true; const comp = notifComp.createObject(root, { - popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), + popup: root.shouldShowPopup(), notification: notif }); root.list = [comp, ...root.list]; @@ -88,6 +105,7 @@ Singleton { FileView { id: storage + printErrors: false path: `${Paths.state}/notifs.json` onLoaded: { const data = JSON.parse(text()); @@ -99,12 +117,14 @@ Singleton { onLoadFailed: err => { if (err === FileViewError.FileNotFound) { root.loaded = true; - setText("[]"); + Qt.callLater(() => setText("[]")); } } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "clearNotifs" description: "Clear all notifications" onPressed: { @@ -114,8 +134,6 @@ Singleton { } IpcHandler { - target: "notifs" - function clear(): void { for (const notif of root.list.slice()) notif.close(); @@ -136,203 +154,13 @@ Singleton { function disableDnd(): void { props.dnd = false; } - } - - component Notif: QtObject { - id: notif - - property bool popup - property bool closed - property var locks: new Set() - - property date time: new Date() - readonly property string timeStr: { - const diff = Time.date.getTime() - time.getTime(); - const m = Math.floor(diff / 60000); - - if (m < 1) - return qsTr("now"); - - const h = Math.floor(m / 60); - const d = Math.floor(h / 24); - - if (d > 0) - return `${d}d`; - if (h > 0) - return `${h}h`; - return `${m}m`; - } - - property Notification notification - property string id - property string summary - property string body - property string appIcon - property string appName - property string image - property real expireTimeout: Config.notifs.defaultExpireTimeout - property int urgency: NotificationUrgency.Normal - property bool resident - property bool hasActionIcons - property list actions - - readonly property Timer timer: Timer { - running: true - interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout - onTriggered: { - if (Config.notifs.expire) - notif.popup = false; - } - } - - readonly property LazyLoader dummyImageLoader: LazyLoader { - active: false - - PanelWindow { - implicitWidth: Config.notifs.sizes.image - implicitHeight: Config.notifs.sizes.image - color: "transparent" - mask: Region {} - - Image { - function tryCache(): void { - if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) - return; - - const cacheKey = notif.appName + notif.summary + notif.id; - let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; - for (let i = 0; i < cacheKey.length; i++) { - ch = cacheKey.charCodeAt(i); - h1 = Math.imul(h1 ^ ch, 2654435761); - h2 = Math.imul(h2 ^ ch, 1597334677); - } - h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); - h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); - h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); - h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); - const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); - - const cache = `${Paths.notifimagecache}/${hash}.png`; - CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { - notif.image = cache; - notif.dummyImageLoader.active = false; - }); - } - - anchors.fill: parent - source: Qt.resolvedUrl(notif.image) - fillMode: Image.PreserveAspectCrop - cache: false - asynchronous: true - opacity: 0 - - onStatusChanged: tryCache() - onWidthChanged: tryCache() - onHeightChanged: tryCache() - } - } - } - readonly property Connections conn: Connections { - target: notif.notification - - function onClosed(): void { - notif.close(); - } - - function onSummaryChanged(): void { - notif.summary = notif.notification.summary; - } - - function onBodyChanged(): void { - notif.body = notif.notification.body; - } - - function onAppIconChanged(): void { - notif.appIcon = notif.notification.appIcon; - } - - function onAppNameChanged(): void { - notif.appName = notif.notification.appName; - } - - function onImageChanged(): void { - notif.image = notif.notification.image; - if (notif.notification?.image) - notif.dummyImageLoader.active = true; - } - - function onExpireTimeoutChanged(): void { - notif.expireTimeout = notif.notification.expireTimeout; - } - - function onUrgencyChanged(): void { - notif.urgency = notif.notification.urgency; - } - - function onResidentChanged(): void { - notif.resident = notif.notification.resident; - } - - function onHasActionIconsChanged(): void { - notif.hasActionIcons = notif.notification.hasActionIcons; - } - - function onActionsChanged(): void { - notif.actions = notif.notification.actions.map(a => ({ - identifier: a.identifier, - text: a.text, - invoke: () => a.invoke() - })); - } - } - - function lock(item: Item): void { - locks.add(item); - } - - function unlock(item: Item): void { - locks.delete(item); - if (closed) - close(); - } - - function close(): void { - closed = true; - if (locks.size === 0 && root.list.includes(this)) { - root.list = root.list.filter(n => n !== this); - notification?.dismiss(); - destroy(); - } - } - - Component.onCompleted: { - if (!notification) - return; - - id = notification.id; - summary = notification.summary; - body = notification.body; - appIcon = notification.appIcon; - appName = notification.appName; - image = notification.image; - if (notification?.image) - dummyImageLoader.active = true; - expireTimeout = notification.expireTimeout; - urgency = notification.urgency; - resident = notification.resident; - hasActionIcons = notification.hasActionIcons; - actions = notification.actions.map(a => ({ - identifier: a.identifier, - text: a.text, - invoke: () => a.invoke() - })); - } + target: "notifs" } Component { id: notifComp - Notif {} + NotifData {} } } diff --git a/services/Players.qml b/services/Players.qml index 1191696ae..41507839d 100644 --- a/services/Players.qml +++ b/services/Players.qml @@ -1,36 +1,51 @@ pragma Singleton -import qs.components.misc -import qs.config +import QtQml import Quickshell import Quickshell.Io import Quickshell.Services.Mpris -import QtQml import Caelestia +import Caelestia.Config +import qs.components.misc Singleton { id: root readonly property list list: Mpris.players.values - readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null + readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === GlobalConfig.services.defaultPlayer) ?? list[0] ?? null property alias manualActive: props.manualActive function getIdentity(player: MprisPlayer): string { - const alias = Config.services.playerAliases.find(a => a.from === player.identity); + const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity); return alias?.to ?? player.identity; } - Connections { - target: active + function getArtUrl(player: MprisPlayer): string { + if (!player) + return ""; + if (player.trackArtUrl) + return player.trackArtUrl; + + const url = player.metadata["xesam:url"] ?? ""; + if (url.startsWith("https://www.youtube.com/watch")) { + // Fallback for youtube + const id = url.match(/[?&]v=([\w-]{11})/)?.[1]; + return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : ""; + } + return ""; + } + Connections { function onPostTrackChanged() { - if (!Config.utilities.toasts.nowPlaying) { + if (!GlobalConfig.utilities.toasts.nowPlaying) { return; } - if (active.trackArtist != "" && active.trackTitle != "") { - Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note"); + if (root.active.trackArtist != "" && root.active.trackTitle != "") { + Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note"); } } + + target: root.active } PersistentProperties { @@ -41,7 +56,9 @@ Singleton { reloadableId: "players" } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaToggle" description: "Toggle media playback" onPressed: { @@ -51,7 +68,9 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaPrev" description: "Previous track" onPressed: { @@ -61,7 +80,9 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaNext" description: "Next track" onPressed: { @@ -71,15 +92,15 @@ Singleton { } } + // qmllint disable unresolved-type CustomShortcut { + // qmllint enable unresolved-type name: "mediaStop" description: "Stop media playback" onPressed: root.active?.stop() } IpcHandler { - target: "mpris" - function getActive(prop: string): string { const active = root.active; return active ? active[prop] ?? "Invalid property" : "No active player"; @@ -122,5 +143,7 @@ Singleton { function stop(): void { root.active?.stop(); } + + target: "mpris" } } diff --git a/services/Recorder.qml b/services/Recorder.qml index be83629ca..253bac17c 100644 --- a/services/Recorder.qml +++ b/services/Recorder.qml @@ -1,54 +1,66 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Io -import QtQuick Singleton { id: root readonly property alias running: props.running + readonly property alias starting: props.starting readonly property alias paused: props.paused readonly property alias elapsed: props.elapsed + readonly property alias videoMode: props.videoMode + readonly property alias audioMode: props.audioMode + property int startChecks: 0 + readonly property int maxStartChecks: 30 signal errorOccurred(string errorMsg) signal recordingStarted() signal recordingStopped() function start(videoMode: string, audioMode: string): bool { - if (props.running) { + if (props.running || props.starting) { console.warn("Recording already running"); errorOccurred("Recording already in progress"); return false; } + const requestedVideoMode = videoMode || "fullscreen"; + const requestedAudioMode = audioMode || "none"; + // Build command array - const args = ["caelestia", "record", "--mode", videoMode]; + const args = ["caelestia", "record", "--mode", requestedVideoMode]; - if (audioMode) { - args.push("--audio", audioMode); + if (requestedAudioMode) { + args.push("--audio", requestedAudioMode); } console.log("Executing:", args.join(" ")); try { Quickshell.execDetached(args); - props.running = true; + props.starting = true; + props.running = false; props.paused = false; props.elapsed = 0; + props.videoMode = requestedVideoMode; + props.audioMode = requestedAudioMode; + root.startChecks = 0; verifyTimer.restart(); - recordingStarted(); return true; } catch (error) { console.error("Failed to start recording:", error); errorOccurred("Failed to execute recording command: " + error); + props.starting = false; props.running = false; return false; } } function stop(): void { - if (!props.running) { + if (!props.running && !props.starting) { console.warn("No recording to stop"); return; } @@ -57,12 +69,21 @@ Singleton { try { Quickshell.execDetached(["caelestia", "record", "--stop"]); + if (props.starting) { + props.starting = false; + props.running = false; + props.paused = false; + props.elapsed = 0; + recordingStopped(); + return; + } // Don't immediately set running to false - wait for process to confirm stopVerifyTimer.restart(); } catch (error) { console.error("Failed to stop recording:", error); errorOccurred("Failed to stop recording: " + error); // Force state reset on error + props.starting = false; props.running = false; props.paused = false; props.elapsed = 0; @@ -71,7 +92,7 @@ Singleton { } function togglePause(): void { - if (!props.running) { + if (!props.running || props.starting) { console.warn("No recording to pause"); return; } @@ -96,8 +117,11 @@ Singleton { id: props property bool running: false + property bool starting: false property bool paused: false property real elapsed: 0 + property string videoMode: "fullscreen" + property string audioMode: "none" reloadableId: "recorder" } @@ -116,6 +140,7 @@ Singleton { // Detect unexpected stop if (wasRunning && !isRunning) { console.warn("Recording process stopped unexpectedly"); + props.starting = false; props.running = false; props.paused = false; props.elapsed = 0; @@ -132,7 +157,7 @@ Singleton { // Verification timer after start Timer { id: verifyTimer - interval: 1500 + interval: 1000 repeat: false onTriggered: { console.log("Verifying recording started"); @@ -161,9 +186,26 @@ Singleton { onExited: code => { const isRunning = code === 0; - if (!isRunning && props.running) { + if (isRunning && props.starting) { + console.log("Recording verified running"); + props.starting = false; + props.running = true; + props.paused = false; + recordingStarted(); + statusCheckTimer.restart(); + return; + } + + if (!isRunning && props.starting) { + root.startChecks++; + if (root.startChecks < root.maxStartChecks) { + verifyTimer.restart(); + return; + } + console.error("Recording process failed to start"); - errorOccurred("Recording process failed to start"); + errorOccurred("Recording did not start"); + props.starting = false; props.running = false; props.paused = false; props.elapsed = 0; @@ -186,6 +228,7 @@ Singleton { if (!isRunning) { console.log("Recording stopped successfully"); + props.starting = false; props.running = false; props.paused = false; props.elapsed = 0; @@ -240,10 +283,12 @@ Singleton { onExited: code => { if (code === 0) { console.log("Found existing recording process"); + props.starting = false; props.running = true; statusCheckTimer.restart(); } else { console.log("No existing recording process"); + props.starting = false; props.running = false; props.paused = false; props.elapsed = 0; diff --git a/services/Screens.qml b/services/Screens.qml new file mode 100644 index 000000000..ac26d27d8 --- /dev/null +++ b/services/Screens.qml @@ -0,0 +1,14 @@ +pragma Singleton + +import Quickshell +import Caelestia.Config + +Singleton { + id: root + + readonly property list screens: Quickshell.screens.filter(s => GlobalConfig.forScreen(s.name).enabled) + + function isExcluded(screen: ShellScreen): bool { + return !GlobalConfig.forScreen(screen.name).enabled; + } +} diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml index bd02da362..15dda6111 100644 --- a/services/SystemUsage.qml +++ b/services/SystemUsage.qml @@ -1,31 +1,57 @@ pragma Singleton -import qs.config +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia.Config Singleton { id: root + // CPU properties + property string cpuName: "" property real cpuPerc property real cpuTemp - readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType + + // GPU properties + readonly property string gpuType: GlobalConfig.services.gpuType.toUpperCase() || autoGpuType property string autoGpuType: "NONE" + property string gpuName: "" property real gpuPerc property real gpuTemp + + // Memory properties property real memUsed property real memTotal readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 - property real storageUsed - property real storageTotal - property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 + + // Storage properties (aggregated) + readonly property real storagePerc: { + let totalUsed = 0; + let totalSize = 0; + for (const disk of disks) { + totalUsed += disk.used; + totalSize += disk.total; + } + return totalSize > 0 ? totalUsed / totalSize : 0; + } + + // Individual disks: Array of { mount, used, total, free, perc } + property var disks: [] property real lastCpuIdle property real lastCpuTotal property int refCount + function cleanCpuName(name: string): string { + return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim(); + } + + function cleanGpuName(name: string): string { + return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim(); + } + function formatKib(kib: real): var { const mib = 1024; const gib = 1024 ** 2; @@ -54,7 +80,7 @@ Singleton { Timer { running: root.refCount > 0 - interval: 3000 + interval: GlobalConfig.dashboard.resourceUpdateInterval repeat: true triggeredOnStart: true onTriggered: { @@ -66,6 +92,18 @@ Singleton { } } + // One-time CPU info detection (name) + FileView { + id: cpuinfoInit + + path: "/proc/cpuinfo" + onLoaded: { + const nameMatch = text().match(/model name\s*:\s*(.+)/); + if (nameMatch) + root.cpuName = root.cleanCpuName(nameMatch[1]); + } + } + FileView { id: stat @@ -101,41 +139,111 @@ Singleton { Process { id: storage - command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] + // Get physical disks with aggregated usage from their partitions + // -J triggers JSON output. -b triggers bytes. + command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] + stdout: StdioCollector { onStreamFinished: { - const deviceMap = new Map(); + const data = JSON.parse(text); + const diskList = []; + const seenDevices = new Set(); + + // Helper to recursively sum usage from children (partitions, crypt, lvm) + const aggregateUsage = dev => { + let used = 0; + let size = 0; + let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/")); + + if (!seenDevices.has(dev.name)) { + // lsblk returns null for empty/unformatted partitions, which parses to 0 here + used = parseInt(dev.fsused) || 0; + size = parseInt(dev.fssize) || 0; + seenDevices.add(dev.name); + } - for (const line of text.trim().split("\n")) { - if (line.trim() === "") - continue; - - const parts = line.trim().split(/\s+/); - if (parts.length >= 3) { - const device = parts[0]; - const used = parseInt(parts[1], 10) || 0; - const avail = parseInt(parts[2], 10) || 0; - - // Only keep the entry with the largest total space for each device - if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { - deviceMap.set(device, { - used: used, - avail: avail - }); + if (dev.children) { + for (const child of dev.children) { + const stats = aggregateUsage(child); + used += stats.used; + size += stats.size; + if (stats.isRoot) + isRoot = true; } } + return { + used, + size, + isRoot + }; + }; + + for (const dev of data.blockdevices) { + // Only process physical disks at the top level + if (dev.type === "disk" && !dev.name.startsWith("zram")) { + const stats = aggregateUsage(dev); + + if (stats.size === 0) { + continue; + } + + const total = stats.size; + const used = stats.used; + + diskList.push({ + mount: dev.name, + used: used / 1024 // KiB + , + total: total / 1024 // KiB + , + free: (total - used) / 1024, + perc: total > 0 ? used / total : 0, + hasRoot: stats.isRoot + }); + } } - let totalUsed = 0; - let totalAvail = 0; + // Sort by putting the disk with root first, then sort the rest alphabetically + root.disks = diskList.sort((a, b) => { + if (a.hasRoot && !b.hasRoot) + return -1; + if (!a.hasRoot && b.hasRoot) + return 1; + return a.mount.localeCompare(b.mount); + }); + } + } + } + + // GPU name detection (one-time) + Process { + id: gpuNameDetect - for (const [device, stats] of deviceMap) { - totalUsed += stats.used; - totalAvail += stats.avail; - } + running: true + command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"] + stdout: StdioCollector { + onStreamFinished: { + const output = text.trim(); + if (!output) + return; - root.storageUsed = totalUsed; - root.storageTotal = totalUsed + totalAvail; + // Check if it's from nvidia-smi (clean GPU name) + if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { + root.gpuName = root.cleanGpuName(output); + } else if (output.toLowerCase().includes("rx")) { + root.gpuName = root.cleanGpuName(output); + } else { + // Parse lspci output: extract name from brackets or after colon + // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0) + const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/); + if (bracketMatch) { + root.gpuName = root.cleanGpuName(bracketMatch[1]); + } else { + const colonMatch = output.match(/:\s*(.+)/); + if (colonMatch) + root.gpuName = root.cleanGpuName(colonMatch[1]); + } + } } } } @@ -143,7 +251,7 @@ Singleton { Process { id: gpuTypeCheck - running: !Config.services.gpuType + running: !GlobalConfig.services.gpuType command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] stdout: StdioCollector { onStreamFinished: root.autoGpuType = text.trim() diff --git a/services/Time.qml b/services/Time.qml index a07d9ef8e..0db520d09 100644 --- a/services/Time.qml +++ b/services/Time.qml @@ -1,8 +1,8 @@ pragma Singleton -import qs.config -import Quickshell import QtQuick +import Quickshell +import Caelestia.Config Singleton { property alias enabled: clock.enabled @@ -11,7 +11,7 @@ Singleton { readonly property int minutes: clock.minutes readonly property int seconds: clock.seconds - readonly property string timeStr: format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") + readonly property string timeStr: format(GlobalConfig.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") readonly property list timeComponents: timeStr.split(":") readonly property string hourStr: timeComponents[0] ?? "" readonly property string minuteStr: timeComponents[1] ?? "" @@ -23,6 +23,7 @@ Singleton { SystemClock { id: clock + precision: SystemClock.Seconds } } diff --git a/services/VPN.qml b/services/VPN.qml index 2d08631a1..61e91d0b2 100644 --- a/services/VPN.qml +++ b/services/VPN.qml @@ -1,20 +1,26 @@ pragma Singleton +import QtQuick import Quickshell import Quickshell.Io -import QtQuick -import qs.config import Caelestia +import Caelestia.Config Singleton { id: root property bool connected: false + property var status: ({ + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }) readonly property bool connecting: connectProc.running || disconnectProc.running - readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) + readonly property bool enabled: GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) readonly property var providerInput: { - const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); + const enabledProvider = GlobalConfig.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); return enabledProvider || "wireguard"; } readonly property bool isCustomProvider: typeof providerInput === "object" @@ -53,7 +59,7 @@ Singleton { displayName: "Warp" }, "netbird": { - connectCmd: ["netbird", "up"], + connectCmd: ["netbird", "up", "--no-browser"], disconnectCmd: ["netbird", "down"], interface: "wt0", displayName: "NetBird" @@ -75,6 +81,10 @@ Singleton { } function connect(): void { + if (status.state === "needs-auth" && status.authUrl) { + emitStatusToast(status); + return; + } if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { connectProc.exec(root.currentConfig.connectCmd); } @@ -87,11 +97,7 @@ Singleton { } function toggle(): void { - if (connected) { - disconnect(); - } else { - connect(); - } + connected ? disconnect() : connect(); } function checkStatus(): void { @@ -100,18 +106,223 @@ Singleton { } } - onConnectedChanged: { - if (!Config.utilities.toasts.vpnChanged) + function getStatusCommand(): var { + switch (providerName) { + case "tailscale": + return ["tailscale", "status", "--json"]; + case "netbird": + return ["netbird", "status", "--json"]; + case "warp": + return ["warp-cli", "status"]; + case "wireguard": + return ["ip", "link", "show"]; + default: + return ["ip", "link", "show"]; + } + } + + function parseTailscaleStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + + // Handle empty or whitespace-only output + if (!output || output.trim().length === 0) { + return status; + } + + // Check for common non-JSON states first + if (output.includes("Logged out") || output.includes("Stopped") || output.includes("not running") || output.includes("Tailscale is not running")) { + status.state = "disconnected"; + return status; + } + + // Try to parse as JSON + try { + const data = JSON.parse(output); + const backendState = data.BackendState || ""; + + if (backendState === "Running") { + status.connected = true; + status.state = "connected"; + } else if (backendState === "Starting") { + status.state = "connecting"; + } else if (backendState === "NeedsLogin" || backendState === "NeedsMachineAuth") { + status.state = "needs-auth"; + status.reason = backendState === "NeedsLogin" ? "Login required" : "Machine authorization required"; + status.authUrl = data.AuthURL || ""; + } + } catch (e) { + // JSON parsing failed - treat as disconnected unless it looks like an error + if (output.includes("error") || output.includes("Error") || output.includes("failed")) { + status.state = "disconnected"; + status.reason = "Tailscale may not be running"; + } else { + status.state = "disconnected"; + } + } + return status; + } + + function parseNetBirdStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + try { + const data = JSON.parse(output); + const mgmtConnected = data.management?.connected; + const signalConnected = data.signal?.connected; + + if (mgmtConnected && signalConnected) { + status.connected = true; + status.state = "connected"; + } else if (data.management?.error) { + const error = data.management.error; + if (error.includes("auth") || error.includes("login")) { + status.state = "needs-auth"; + status.reason = "Authentication required"; + } else { + status.reason = error; + } + } + } catch (e) { + status.state = "error"; + status.reason = "Failed to parse status"; + } + return status; + } + + function parseWarpStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + + if (output.includes("Connected")) { + status.connected = true; + status.state = "connected"; + } else if (output.includes("Connecting")) { + status.state = "connecting"; + } else if (output.includes("Unable") || output.includes("Registration Missing") || output.includes("registration") || output.includes("register")) { + status.state = "needs-auth"; + status.reason = "WARP registration required"; + } else if (!output.includes("Disconnected")) { + status.state = "error"; + status.reason = "Unknown WARP status"; + } + return status; + } + + function parseWireGuardStatus(output: string): var { + const status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + const iface = root.currentConfig?.interface || ""; + + if (iface && output.includes(iface + ":")) { + status.connected = true; + status.state = "connected"; + } + return status; + } + + function parseStatusOutput(output: string): var { + switch (providerName) { + case "tailscale": + return parseTailscaleStatus(output); + case "netbird": + return parseNetBirdStatus(output); + case "warp": + return parseWarpStatus(output); + case "wireguard": + default: + return parseWireGuardStatus(output); + } + } + + function extractAuthUrl(text: string): string { + const urlMatch = text.match(/(https?:\/\/[^\s]+)/); + return urlMatch ? urlMatch[1] : ""; + } + + function createAuthStatus(authUrl: string): var { + return { + connected: false, + state: "needs-auth", + reason: "Authentication required", + authUrl: authUrl + }; + } + + function updateStatus(newStatus: var): void { + const oldState = status.state; + if (newStatus.state === "needs-auth" && !newStatus.authUrl && status.authUrl) { + newStatus.authUrl = status.authUrl; + } + status = newStatus; + root.connected = newStatus.connected; + + if (oldState !== newStatus.state) { + emitStatusToast(newStatus); + } + } + + function emitStatusToast(statusObj: var): void { + if (!GlobalConfig.utilities.toasts.vpnChanged) return; const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; - if (connected) { + + switch (statusObj.state) { + case "connected": Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); - } else { - Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); + break; + case "disconnected": + if (status.connected) { + Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); + } + break; + case "needs-auth": + const authMsg = statusObj.reason || "Authentication required"; + Toaster.toast(qsTr("VPN authentication required"), qsTr("%1: %2").arg(displayName).arg(authMsg), "vpn_lock"); + break; + case "error": + if (status.state === "connected" || status.state === "connecting" || status.state === "needs-auth") { + const errMsg = statusObj.reason || "Unknown error"; + Toaster.toast(qsTr("VPN error"), qsTr("%1: %2").arg(displayName).arg(errMsg), "error"); + } + break; + } + } + + onStatusChanged: { + if (providerName === "warp" && status.state === "needs-auth" && status.reason.includes("registration")) { + warpRegisterProc.exec(["warp-cli", "registration", "new"]); } } + onProviderNameChanged: { + status = { + connected: false, + state: "disconnected", + reason: "", + authUrl: "" + }; + root.connected = false; + statusCheckTimer.start(); + } + Component.onCompleted: root.enabled && statusCheckTimer.start() Process { @@ -127,15 +338,47 @@ Singleton { Process { id: statusProc - command: ["ip", "link", "show"] + command: root.getStatusCommand() + // qmllint disable incompatible-type environment: ({ + // qmllint enable incompatible-type LANG: "C.UTF-8", LC_ALL: "C.UTF-8" }) stdout: StdioCollector { onStreamFinished: { - const iface = root.currentConfig ? root.currentConfig.interface : ""; - root.connected = iface && text.includes(iface + ":"); + const newStatus = root.parseStatusOutput(text); + root.updateStatus(newStatus); + } + } + stderr: StdioCollector { + onStreamFinished: { + if (text.trim().length > 0) { + if (text.includes("doesn't appear to be running") || text.includes("failed to connect to local tailscaled") || text.includes("daemon is not running") || text.includes("not running") && (text.includes("netbird") || text.includes("warp"))) { + let cmd = "sudo systemctl start "; + switch (root.providerName) { + case "tailscale": + cmd += "tailscaled"; + break; + case "netbird": + cmd += "netbird"; + break; + case "warp": + cmd += "warp-svc"; + break; + default: + cmd += root.providerName + "d"; + break; + } + const errorStatus = { + connected: false, + state: "disconnected", + reason: `Service not running (run: ${cmd})`, + authUrl: "" + }; + root.updateStatus(errorStatus); + } + } } } } @@ -143,12 +386,59 @@ Singleton { Process { id: connectProc - onExited: statusCheckTimer.start() + onExited: exitCode => { // qmllint disable signal-handler-parameters + if (exitCode !== 0) { + return; + } + + if (root.providerName === "tailscale") { + Qt.callLater(() => { + if (root.status.state !== "needs-auth") { + statusCheckTimer.start(); + } + }); + } else if (root.status.state !== "needs-auth") { + statusCheckTimer.start(); + } + } + stdout: SplitParser { + onRead: data => { + const authUrl = root.extractAuthUrl(data); + if (authUrl) { + root.updateStatus(root.createAuthStatus(authUrl)); + } + } + } stderr: StdioCollector { onStreamFinished: { const error = text.trim(); - if (error && !error.includes("[#]") && !error.includes("already exists")) { - console.warn("VPN connection error:", error); + + if (error.includes("Access denied") || error.includes("checkprefs access denied")) { + const errorStatus = { + connected: false, + state: "disconnected", + reason: "Permission denied. Run in terminal: sudo tailscale set --operator=$USER", + authUrl: "" + }; + root.updateStatus(errorStatus); + return; + } + + if (error.includes("Unknown device type") || error.includes("Protocol not supported")) { + const errorStatus = { + connected: false, + state: "disconnected", + reason: "WireGuard module not loaded. Run: sudo modprobe wireguard", + authUrl: "" + }; + root.updateStatus(errorStatus); + return; + } + + const authUrl = root.extractAuthUrl(error); + + if (authUrl) { + root.updateStatus(root.createAuthStatus(authUrl)); } else if (error.includes("already exists")) { root.connected = true; } @@ -159,21 +449,38 @@ Singleton { Process { id: disconnectProc - onExited: statusCheckTimer.start() + onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters stderr: StdioCollector { onStreamFinished: { const error = text.trim(); if (error && !error.includes("[#]")) { - console.warn("VPN disconnection error:", error); + console.warn(lc, "Disconnection error:", error); } } } } + Process { + id: warpRegisterProc + + onExited: exitCode => { // qmllint disable signal-handler-parameters + if (exitCode === 0) { + statusCheckTimer.start(); + } + } + } + Timer { id: statusCheckTimer interval: 500 onTriggered: root.checkStatus() } + + LoggingCategory { + id: lc + + name: "caelestia.qml.services.vpn" + defaultLogLevel: LoggingCategory.Info + } } diff --git a/services/Visibilities.qml b/services/Visibilities.qml index 5ddde0c95..391870502 100644 --- a/services/Visibilities.qml +++ b/services/Visibilities.qml @@ -1,16 +1,18 @@ pragma Singleton import Quickshell +import qs.components +import qs.services Singleton { property var screens: new Map() property var bars: new Map() - function load(screen: ShellScreen, visibilities: var): void { + function load(screen: ShellScreen, visibilities: DrawerVisibilities): void { screens.set(Hypr.monitorFor(screen), visibilities); } - function getForActive(): PersistentProperties { + function getForActive(): DrawerVisibilities { return screens.get(Hypr.focusedMonitor); } } diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml index cb96bc565..15daf5c16 100644 --- a/services/Wallpapers.qml +++ b/services/Wallpapers.qml @@ -1,17 +1,18 @@ pragma Singleton -import qs.config -import qs.utils -import Caelestia.Models +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia.Config +import Caelestia.Models +import qs.services +import qs.utils Searcher { id: root readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt` - readonly property list smartArg: Config.services.smartScheme ? [] : ["--no-smart"] + readonly property list smartArg: GlobalConfig.services.smartScheme ? [] : ["--no-smart"] property bool showPreview: false readonly property string current: showPreview ? previewPath : actualCurrent @@ -40,14 +41,12 @@ Searcher { list: wallpapers.entries key: "relativePath" - useFuzzy: Config.launcher.useFuzzy.wallpapers + useFuzzy: GlobalConfig.launcher.useFuzzy.wallpapers extraOpts: useFuzzy ? ({}) : ({ forward: false }) IpcHandler { - target: "wallpaper" - function get(): string { return root.actualCurrent; } @@ -59,6 +58,8 @@ Searcher { function list(): string { return root.list.map(w => w.path).join("\n"); } + + target: "wallpaper" } FileView { diff --git a/services/Weather.qml b/services/Weather.qml index a3095423b..9c30bf7a8 100644 --- a/services/Weather.qml +++ b/services/Weather.qml @@ -1,10 +1,10 @@ pragma Singleton -import qs.config -import qs.utils -import Caelestia -import Quickshell import QtQuick +import Quickshell +import Caelestia +import Caelestia.Config +import qs.utils Singleton { id: root @@ -17,17 +17,17 @@ Singleton { readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" readonly property string description: cc?.weatherDesc ?? qsTr("No weather") - readonly property string temp: Config.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` - readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` + readonly property string temp: GlobalConfig.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` + readonly property string feelsLike: GlobalConfig.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` readonly property int humidity: cc?.humidity ?? 0 readonly property real windSpeed: cc?.windSpeed ?? 0 - readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" - readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" + readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" + readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" readonly property var cachedCities: new Map() function reload(): void { - const configLocation = Config.services.weatherLocation; + const configLocation = GlobalConfig.services.weatherLocation; if (configLocation) { if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) { @@ -54,18 +54,35 @@ Singleton { return; } - const [lat, lon] = coords.split(","); - const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; - Requests.get(url, text => { + const [lat, lon] = coords.split(",").map(s => s.trim()); + + const fallbackToBigDataCloud = () => { + const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`; + Requests.get(fallbackUrl, text => { + const geo = JSON.parse(text); + const geoCity = geo.city || geo.locality; + if (geoCity) { + city = geoCity; + cachedCities.set(coords, geoCity); + } else { + city = "Unknown City"; + } + }); + }; + + const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; + Requests.get(nominatimUrl, text => { const geo = JSON.parse(text).features?.[0]?.properties.geocoding; if (geo) { const geoCity = geo.type === "city" ? geo.name : geo.city; - city = geoCity; - cachedCities.set(coords, geoCity); - } else { - city = "Unknown City"; + if (geoCity) { + city = geoCity; + cachedCities.set(coords, geoCity); + return; + } } - }); + fallbackToBigDataCloud(); + }, fallbackToBigDataCloud); } function fetchCoordsFromCity(cityName: string): void { @@ -104,14 +121,14 @@ Singleton { humidity: json.current.relative_humidity_2m, windSpeed: json.current.wind_speed_10m, isDay: json.current.is_day, - sunrise: json.daily.sunrise[0], - sunset: json.daily.sunset[0] + sunrise: json.daily.sunrise[0].replace("T", " "), + sunset: json.daily.sunset[0].replace("T", " ") }; const forecastList = []; for (let i = 0; i < json.daily.time.length; i++) forecastList.push({ - date: json.daily.time[i], + date: json.daily.time[i].replace(/-/g, "/"), maxTempC: Math.round(json.daily.temperature_2m_max[i]), maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), minTempC: Math.round(json.daily.temperature_2m_min[i]), @@ -124,7 +141,8 @@ Singleton { const hourlyList = []; const now = new Date(); for (let i = 0; i < json.hourly.time.length; i++) { - const time = new Date(json.hourly.time[i]); + const time = new Date(json.hourly.time[i].replace("T", " ")); + if (time < now) continue; @@ -149,7 +167,7 @@ Singleton { if (!loc || loc.indexOf(",") === -1) return ""; - const [lat, lon] = loc.split(","); + const [lat, lon] = loc.split(",").map(s => s.trim()); const baseUrl = "https://api.open-meteo.com/v1/forecast"; const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; @@ -192,7 +210,14 @@ Singleton { onLocChanged: fetchWeatherData() - // Refresh current location hourly + Connections { + function onWeatherLocationChanged(): void { + root.reload(); + } + + target: GlobalConfig.services + } + Timer { interval: 3600000 // 1 hour running: true diff --git a/shell.qml b/shell.qml index e93b5c524..7cb423541 100644 --- a/shell.qml +++ b/shell.qml @@ -1,6 +1,8 @@ -//@ pragma Env QS_NO_RELOAD_POPUP=1 -//@ pragma Env QSG_RENDER_LOOP=threaded -//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 +//@ pragma Env QS_CRASHREPORT_URL=https://github.com/caelestia-dots/shell/issues/new?template=crash.yml +//@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1 +//@ pragma DefaultEnv QS_DROP_EXPENSIVE_FONTS=1 +//@ pragma DefaultEnv QSG_RENDER_LOOP=threaded +//@ pragma DefaultEnv QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 import "modules" import "modules/drawers" @@ -10,6 +12,8 @@ import "modules/lock" import Quickshell ShellRoot { + settings.watchFiles: true + Background {} Drawers {} AreaPicker {} @@ -17,6 +21,7 @@ ShellRoot { id: lock } + ConfigToasts {} Shortcuts {} BatteryMonitor {} IdleMonitors { diff --git a/utils/Icons.qml b/utils/Icons.qml index c06cbf809..c864d5530 100644 --- a/utils/Icons.qml +++ b/utils/Icons.qml @@ -1,9 +1,9 @@ pragma Singleton -import qs.config +import QtQuick import Quickshell import Quickshell.Services.Notifications -import QtQuick +import Caelestia.Config Singleton { id: root @@ -79,6 +79,26 @@ Singleton { Office: "content_paste" }) + // Checks if a name matches an icon config. Icon configs can have the following keys: + // - name: The exact name of the icon + // - regex: A regex to match against the name (takes priority over name) + // - flags: The regex flags (only used if regex is set) + // - icon: The icon to use + function matchIconConfig(name: string, iconConfig: var): bool { + if (!iconConfig.icon) + return false; + + if (iconConfig.regex) { + const re = new RegExp(iconConfig.regex, iconConfig.flags ?? ""); + if (re.test(name)) + return true; + } else if (iconConfig.name === name) { + return true; + } + + return false; + } + function getAppIcon(name: string, fallback: string): string { const icon = DesktopEntries.heuristicLookup(name)?.icon; if (fallback !== "undefined") @@ -87,6 +107,10 @@ Singleton { } function getAppCategoryIcon(name: string, fallback: string): string { + for (const iconConfig of GlobalConfig.bar.workspaces.windowIcons) + if (matchIconConfig(name, iconConfig)) + return iconConfig.icon; + const categories = DesktopEntries.heuristicLookup(name)?.categories; if (categories) @@ -188,11 +212,9 @@ Singleton { function getSpecialWsIcon(name: string): string { name = name.toLowerCase().slice("special:".length); - for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) { - if (iconConfig.name === name) { + for (const iconConfig of GlobalConfig.bar.workspaces.specialWorkspaceIcons) + if (matchIconConfig(name, iconConfig)) return iconConfig.icon; - } - } if (name === "special") return "star"; @@ -208,7 +230,7 @@ Singleton { } function getTrayIcon(id: string, icon: string): string { - for (const sub of Config.bar.tray.iconSubs) + for (const sub of GlobalConfig.bar.tray.iconSubs) if (sub.id === id) return sub.image ? Qt.resolvedUrl(sub.image) : Quickshell.iconPath(sub.icon); @@ -218,4 +240,24 @@ Singleton { } return icon; } + + function getBatteryIcon(charge: int): string { + if (charge > 0 && charge < 5) + return "battery_0_bar"; + if (charge >= 5 && charge < 20) + return "battery_1_bar"; + if (charge >= 20 && charge < 35) + return "battery_2_bar"; + if (charge >= 35 && charge < 50) + return "battery_3_bar"; + if (charge >= 50 && charge < 65) + return "battery_4_bar"; + if (charge >= 65 && charge < 80) + return "battery_5_bar"; + if (charge >= 80 && charge < 95) + return "battery_6_bar"; + if (charge >= 95) + return "battery_full"; + return "battery_alert"; + } } diff --git a/utils/NetworkConnection.qml b/utils/NetworkConnection.qml index e55b87bc4..8331813d9 100644 --- a/utils/NetworkConnection.qml +++ b/utils/NetworkConnection.qml @@ -1,7 +1,7 @@ pragma Singleton -import qs.services import QtQuick +import qs.services /** * NetworkConnection diff --git a/utils/Paths.qml b/utils/Paths.qml index bc89770ab..97f6448a5 100644 --- a/utils/Paths.qml +++ b/utils/Paths.qml @@ -1,8 +1,9 @@ pragma Singleton -import qs.config -import Caelestia +import QtQuick import Quickshell +import Caelestia +import Caelestia.Config Singleton { id: root @@ -18,7 +19,7 @@ Singleton { readonly property string imagecache: `${cache}/imagecache` readonly property string notifimagecache: `${imagecache}/notifs` - readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(Config.paths.wallpaperDir) + readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(GlobalConfig.paths.wallpaperDir) readonly property string recsdir: Quickshell.env("CAELESTIA_RECORDINGS_DIR") || `${videos}/Recordings` readonly property string libdir: Quickshell.env("CAELESTIA_LIB_DIR") || "/usr/lib/caelestia" diff --git a/utils/Searcher.qml b/utils/Searcher.qml index 053b73bba..102c9e766 100644 --- a/utils/Searcher.qml +++ b/utils/Searcher.qml @@ -1,8 +1,7 @@ -import Quickshell - import "scripts/fzf.js" as Fzf import "scripts/fuzzysort.js" as Fuzzy import QtQuick +import Quickshell Singleton { required property list list diff --git a/utils/Strings.qml b/utils/Strings.qml new file mode 100644 index 000000000..a91a0c082 --- /dev/null +++ b/utils/Strings.qml @@ -0,0 +1,26 @@ +pragma Singleton + +import Quickshell + +Singleton { + property var _regexCache: ({}) + + function testRegexList(filterList: list, target: string): bool { + const regexChecker = /^\^.*\$$/; + for (const filter of filterList) { + if (regexChecker.test(filter)) { + let re = _regexCache[filter]; + if (!re) { + re = new RegExp(filter); + _regexCache[filter] = re; + } + if (re.test(target)) + return true; + } else { + if (filter === target) + return true; + } + } + return false; + } +} diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml index 19aa4a7a7..c715b8d28 100644 --- a/utils/SysInfo.qml +++ b/utils/SysInfo.qml @@ -1,10 +1,10 @@ pragma Singleton -import qs.config -import qs.utils +import QtQuick import Quickshell import Quickshell.Io -import QtQuick +import Caelestia.Config +import qs.utils Singleton { id: root @@ -36,11 +36,11 @@ Singleton { root.osIdLike = fd("ID_LIKE").split(" "); const logo = Quickshell.iconPath(fd("LOGO"), true); - if (Config.general.logo === "caelestia") { + if (GlobalConfig.general.logo === "caelestia") { root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`); root.isDefaultLogo = true; - } else if (Config.general.logo) { - root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); + } else if (GlobalConfig.general.logo) { + root.osLogo = Quickshell.iconPath(GlobalConfig.general.logo, true) || "file://" + Paths.absolutePath(GlobalConfig.general.logo); root.isDefaultLogo = false; } else if (logo) { root.osLogo = logo; @@ -50,11 +50,11 @@ Singleton { } Connections { - target: Config.general - function onLogoChanged(): void { osRelease.reload(); } + + target: GlobalConfig.general } Timer { diff --git a/utils/scripts/lrcparser.js b/utils/scripts/lrcparser.js new file mode 100644 index 000000000..847779eda --- /dev/null +++ b/utils/scripts/lrcparser.js @@ -0,0 +1,62 @@ +function parseLrc(text) { + if (!text) return []; + let lines = text.split("\n"); + let result = []; + + let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; + + // Blacklist for credits/metadata often found in NetEase lyrics + const creditKeywords = [ + "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" + ]; + + for (let line of lines) { + + timeRegex.lastIndex = 0; + let matches = []; + let match; + + while ((match = timeRegex.exec(line)) !== null) { + matches.push(match); + } + + if (matches.length === 0) continue; + + let lyric = line.replace(timeRegex, "").trim(); + + let min = parseInt(matches[0][1]); + let sec = parseFloat(matches[0][2]); + let totalTime = min * 60 + sec; + + // Only filter credits if they appear in the first 20 seconds + if (totalTime < 20) { + let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); + if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { + continue; + } + } + + for (let match of matches) { + let min = parseInt(match[1]); + let sec = parseFloat(match[2]); + + result.push({ + time: min * 60 + sec, + text: lyric + }); + } + } + + result.sort((a, b) => a.time - b.time); + return result; +} + +function getCurrentLine(lyrics, position) { + const epsilon = 0.1; // 100ms tolerance + for (let i = lyrics.length - 1; i >= 0; i--) { + if ((position + epsilon) >= lyrics[i].time) { + return i; + } + } + return -1; +} From 778cdd5056b4f55b7cd27a3e235bcbccdd285a0c Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Sun, 24 May 2026 14:25:19 +0300 Subject: [PATCH 407/409] Some clean up --- all_changes.patch | 51777 ------------------------------------------- clean-snapshot.tar | 0 2 files changed, 51777 deletions(-) delete mode 100644 all_changes.patch delete mode 100644 clean-snapshot.tar diff --git a/all_changes.patch b/all_changes.patch deleted file mode 100644 index 2d737a143..000000000 --- a/all_changes.patch +++ /dev/null @@ -1,51777 +0,0 @@ -diff --git a/.envrc b/.envrc -index c90b500c..5a259560 100644 ---- a/.envrc -+++ b/.envrc -@@ -3,10 +3,11 @@ if has nix; then - fi - - shopt -s globstar --watch_file assets/cpp/**/*.cpp --watch_file assets/cpp/**/*.hpp - watch_file plugin/**/*.cpp - watch_file plugin/**/*.hpp -+watch_file plugin/**/*.qml -+watch_file plugin/**/*.vert -+watch_file plugin/**/*.frag - watch_file **/CMakeLists.txt - - cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=RelWithDebInfo -DCMAKE_CXX_COMPILER=clazy -DCMAKE_EXPORT_COMPILE_COMMANDS=ON -DDISTRIBUTOR=direnv -diff --git a/.github/ISSUE_TEMPLATE/crash.yml b/.github/ISSUE_TEMPLATE/crash.yml -new file mode 100644 -index 00000000..e37198c9 ---- /dev/null -+++ b/.github/ISSUE_TEMPLATE/crash.yml -@@ -0,0 +1,57 @@ -+name: Crash report -+description: Report a crash -+labels: ["bug", "crash"] -+type: "Bug" -+title: "[CRASH] " -+body: -+ - type: textarea -+ attributes: -+ label: What caused the crash -+ description: | -+ Any information likely to help debug the crash. What were you doing when the crash occurred, -+ what changes did you make, can you get it to happen again? -+ -+ - type: upload -+ attributes: -+ label: Report file -+ description: Attach `report.txt` here. -+ validations: -+ required: true -+ -+ - type: upload -+ attributes: -+ label: Log file -+ description: | -+ Attach `log.qslog.log` here. If it is too big to upload, compress it. -+ -+ You can preview the log if you'd like using `qs log -r '*=true'`. -+ validations: -+ required: true -+ -+ - type: textarea -+ attributes: -+ label: "Version info" -+ description: Run `caelestia -v` and paste the result below. -+ value: | -+
Version info -+ -+ ``` -+ -+ ``` -+ -+
-+ validations: -+ required: true -+ -+ - type: checkboxes -+ attributes: -+ label: Reminder -+ options: -+ - label: I've successfully updated to the latest versions following the [updating guide](https://github.com/caelestia-dots/caelestia?tab=readme-ov-file#updating). -+ required: false -+ - label: I've successfully updated the system packages to the latest. -+ required: false -+ - label: I agree that it's usually impossible for others to help me without my logs. -+ required: true -+ - label: I've ticked the checkboxes without reading their contents -+ required: false -diff --git a/.github/workflows/check-format.yml b/.github/workflows/check-format.yml -new file mode 100644 -index 00000000..ea5fe9ef ---- /dev/null -+++ b/.github/workflows/check-format.yml -@@ -0,0 +1,38 @@ -+name: Check formatting -+ -+on: -+ push: -+ branches: -+ - main -+ pull_request: -+ -+jobs: -+ check-qml: -+ runs-on: ubuntu-latest -+ container: -+ image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest -+ -+ steps: -+ - uses: actions/checkout@v6 -+ -+ - name: Check QML format -+ shell: fish {0} -+ run: | -+ for file in (string match -v 'build/*' **.qml) -+ /usr/lib/qt6/bin/qmlformat $file | diff -u $file - || exit 1 -+ end -+ python3 scripts/qml-lint-conventions.py -+ -+ check-cpp: -+ runs-on: ubuntu-latest -+ container: -+ image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest -+ -+ steps: -+ - uses: actions/checkout@v6 -+ -+ - name: Check C++ format -+ shell: fish {0} -+ run: | -+ find plugin extras -name '*.cpp' -o -name '*.hpp' \ -+ | xargs clang-format --dry-run --Werror -diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml -new file mode 100644 -index 00000000..7962963c ---- /dev/null -+++ b/.github/workflows/lint.yml -@@ -0,0 +1,43 @@ -+name: Lint code -+ -+on: -+ push: -+ branches: -+ - main -+ pull_request: -+ -+jobs: -+ lint: -+ runs-on: ubuntu-latest -+ -+ container: -+ image: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest -+ -+ steps: -+ - uses: actions/checkout@v6 -+ -+ - name: Build -+ run: | -+ # Set version and git rev for CI build so we don't call git -+ cmake -B build -G Ninja -DCMAKE_CXX_COMPILER=clazy -DCMAKE_CXX_FLAGS=-Werror -DVERSION= -DGIT_REVISION= -+ cmake --build build -+ -+ - name: Lint QML -+ shell: fish {0} -+ run: | -+ # Generate tooling -+ touch .qmlls.ini -+ QT_QPA_PLATFORM=offscreen QML2_IMPORT_PATH="$PWD/build/qml:$QML2_IMPORT_PATH" timeout 2 qs -p . -+ -+ # Construct linter args -+ set -l build_dir (grep -oP "(?<=buildDir=\")(.*)(?=\")" .qmlls.ini) -+ set -l import_paths (grep -oP "(?<=importPaths=\")(.*)(?=\")" .qmlls.ini | string split :) -+ set -l args -I $build_dir -+ for path in $import_paths -+ set -a args -I $path -+ end -+ set -l qml_files (string match -vr '(build|modules/controlcenter)/.*' **.qml) -+ -+ # Lint -+ set -l lint_out (/usr/lib/qt6/bin/qmllint --import disable $args $qml_files 2>&1 | tee /dev/stderr) -+ test -z "$lint_out" || exit 1 -diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml -index ac437723..29e704a9 100644 ---- a/.github/workflows/release.yml -+++ b/.github/workflows/release.yml -@@ -3,7 +3,7 @@ name: Create release - on: - push: - tags: -- - 'v*' -+ - "v*" - - jobs: - build-and-release: -@@ -13,7 +13,7 @@ jobs: - contents: write - - steps: -- - uses: actions/checkout@v4 -+ - uses: actions/checkout@v6 - - - name: Create packages - run: | -@@ -35,3 +35,8 @@ jobs: - caelestia-shell-${{ github.ref_name }}.tar.gz - caelestia-shell-latest.tar.gz - generate_release_notes: true -+ -+ - name: Update stable branch -+ run: | -+ git branch -f stable "$GITHUB_SHA" -+ git push origin stable --force -diff --git a/.github/workflows/update-flake-inputs.yml b/.github/workflows/update-flake-inputs.yml -index 1a8bd071..7f24c797 100644 ---- a/.github/workflows/update-flake-inputs.yml -+++ b/.github/workflows/update-flake-inputs.yml -@@ -3,27 +3,33 @@ name: Update flake inputs - on: - workflow_dispatch: - schedule: -- - cron: '0 0 * * 0' -+ - cron: "0 0 * * 0" - - jobs: - update-flake: - runs-on: ubuntu-latest - -- permissions: -- contents: write -- - steps: -- - uses: actions/checkout@v4 -+ - name: Generate app token -+ id: app-token -+ uses: actions/create-github-app-token@v3 -+ with: -+ client-id: ${{ secrets.CLIENT_ID }} -+ private-key: ${{ secrets.APP_PRIVATE_KEY }} -+ -+ - uses: actions/checkout@v6 -+ with: -+ token: ${{ steps.app-token.outputs.token }} - - - name: Install Nix -- uses: nixbuild/nix-quick-install-action@v31 -+ uses: nixbuild/nix-quick-install-action@v34 - with: - nix_conf: | - keep-env-derivations = true - keep-outputs = true - - - name: Restore and save Nix store -- uses: nix-community/cache-nix-action@v6 -+ uses: nix-community/cache-nix-action@v7 - with: - # restore and save a cache using this key - primary-key: nix-${{ hashFiles('**/*.nix', '**/flake.lock') }} -@@ -78,8 +84,9 @@ jobs: - - - name: Commit and push changes - if: steps.check.outputs.modified == 'true' -- uses: EndBug/add-and-commit@v9 -+ uses: EndBug/add-and-commit@v10 - with: -+ github_token: ${{ steps.app-token.outputs.token }} - add: flake.lock - default_author: github_actions - message: "[CI] chore: update flake" -diff --git a/.github/workflows/update-image.yml b/.github/workflows/update-image.yml -new file mode 100644 -index 00000000..74fe5435 ---- /dev/null -+++ b/.github/workflows/update-image.yml -@@ -0,0 +1,46 @@ -+name: Update Docker CI image -+ -+on: -+ workflow_dispatch: -+ schedule: -+ - cron: "0 0 * * 0" -+ -+permissions: -+ packages: write -+ -+jobs: -+ build-image: -+ runs-on: ubuntu-latest -+ -+ steps: -+ - uses: docker/login-action@v4 -+ with: -+ registry: ghcr.io -+ username: ${{ github.actor }} -+ password: ${{ secrets.GITHUB_TOKEN }} -+ -+ - name: Write Dockerfile -+ run: | -+ cat > /tmp/Dockerfile <> /etc/sudoers && \ -+ sudo -u builder git clone https://aur.archlinux.org/yay-bin.git /home/builder/yay-bin && \ -+ cd /home/builder/yay-bin && \ -+ sudo -u builder makepkg -si --noconfirm && \ -+ sudo -u builder yay -S --noconfirm quickshell-git libcava && \ -+ sudo -u builder yay -Yc --noconfirm && \ -+ pacman -Rns --noconfirm yay-bin && \ -+ sed -i '/builder ALL=(ALL) NOPASSWD:ALL/d' /etc/sudoers && \ -+ userdel -r builder && \ -+ pacman -Scc --noconfirm -+ EOF -+ -+ - name: Build and push -+ uses: docker/build-push-action@v7 -+ with: -+ context: . -+ file: /tmp/Dockerfile -+ push: true -+ tags: ghcr.io/${{ github.repository_owner }}/shell-arch-env:latest -diff --git a/.gitignore b/.gitignore -index a114b1bc..c30a6d92 100644 ---- a/.gitignore -+++ b/.gitignore -@@ -3,3 +3,4 @@ - /.qmlls.ini - build/ - .cache/ -+logs -\ No newline at end of file -diff --git a/CMakeLists.txt b/CMakeLists.txt -index 7b95855b..b23f7b48 100644 ---- a/CMakeLists.txt -+++ b/CMakeLists.txt -@@ -60,8 +60,14 @@ if("plugin" IN_LIST ENABLE_MODULES) - endif() - - if("shell" IN_LIST ENABLE_MODULES) -- foreach(dir assets components config modules services utils) -+ foreach(dir assets components modules services utils) - install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") - endforeach() -- install(FILES shell.qml LICENSE DESTINATION "${INSTALL_QSCONFDIR}") -+ -+ file(READ shell.qml SHELL_QML) -+ string(REPLACE "settings.watchFiles: true" "settings.watchFiles: false" SHELL_QML "${SHELL_QML}") -+ file(WRITE "${CMAKE_BINARY_DIR}/qml/shell.qml" "${SHELL_QML}") -+ install(FILES "${CMAKE_BINARY_DIR}/qml/shell.qml" DESTINATION "${INSTALL_QSCONFDIR}") -+ -+ install(FILES LICENSE DESTINATION "${INSTALL_QSCONFDIR}") - endif() -diff --git a/README.md b/README.md -index 6bbc8db9..13b5ec8a 100644 ---- a/README.md -+++ b/README.md -@@ -205,20 +205,66 @@ git pull - ## Configuring - - All configuration options should be put in `~/.config/caelestia/shell.json`. This file is _not_ created by --default, you must create it manually. -+default, you must create it manually. Options that you omit from the config file will use their default -+values. -+ -+### Per-monitor configuration -+ -+You can configure options per-monitor in `~/.config/caelestia/monitors//shell.json`. Options -+set in this file will **override** the respective options in the global config. Otherwise, the options will -+use their values from the global config. -+ -+For example, to disable the bar on DP-1: -+ -+**`~/.config/caelestia/monitors/DP-1/shell.json`** -+ -+```json -+{ -+ "bar": { -+ "persistent": false -+ } -+} -+``` -+ -+> [!NOTE] -+> Not all options are respect per-monitor overrides. Most notably, the following options will only read -+> from the global config, and ignore the respective option in per-monitor config files. -+> -+>
Ignored options -+> -+> - `appearance` (`anim`, `transparency`) -+> - `general` (`logo`, `apps`, `idle`, `battery`) -+> - `bar.workspaces` (`perMonitorWorkspaces`, `specialWorkspaceIcons`, `windowIcons`) -+> - `bar.tray` (`iconSubs`, `hiddenIcons`) -+> - `dashboard` (`mediaUpdateInterval`, `resourceUpdateInterval`) -+> - `launcher` (`specialPrefix`, `actionPrefix`, `enableDangerousActions`, `vimKeybinds`, -+> `favouriteApps`, `hiddenApps`, `actions`) -+> - `launcher.useFuzzy` (`apps`, `actions`, `schemes`, `variants`, `wallpapers`) -+> - `notifs` (`expire`, `fullscreen`, `defaultExpireTimeout`, `actionOnClick`) -+> - `lock` (`enableFprint`, `maxFprintTries`) -+> - `utilities` (`toasts`, `vpn`) -+> - `services` (`weatherLocation`, `useFahrenheit`, `useFahrenheitPerformance`, `useTwelveHourClock`, -+> `gpuType`, `visualiserBars`, `audioIncrement`, `brightnessIncrement`, `maxVolume`, `smartScheme`, -+> `defaultPlayer`, `playerAliases`, `showLyrics`, `lyricsBackend`) -+> - `paths` (`wallpaperDir`, `lyricsDir`) -+> -+>
- - ### Example configuration - - > [!NOTE] --> The example configuration only includes recommended configuration options. For more advanced customisation --> such as modifying the size of individual items or changing constants in the code, there are some other --> options which can be found in the source files in the `config` directory. -+> The example configuration includes ALL configuration options in `shell.json`. You are -+> **not** recommended to copy and paste this entire configuration into `shell.json`. -+> This is meant to serve as a reference of all the available options, and you should -+> only add the ones you want to change to `shell.json`. - -
Example - - ```json - { -+ "enabled": true, - "appearance": { -+ "deformScale": 1, - "anim": { - "durations": { - "scale": 1 -@@ -251,6 +297,10 @@ default, you must create it manually. - } - }, - "general": { -+ "logo": "caelestia", -+ "showOverFullscreen": false, -+ "mediaGifSpeedAdjustment": 300, -+ "sessionGifSpeed": 0.7, - "apps": { - "terminal": ["foot"], - "audio": ["pavucontrol"], -@@ -328,7 +378,14 @@ default, you must create it manually. - } - }, - "bar": { -+ "activeWindow": { -+ "compact": false, -+ "inverted": false, -+ "showOnHover": true -+ }, - "clock": { -+ "background": false, -+ "showDate": false, - "showIcon": true - }, - "dragThreshold": 20, -@@ -413,6 +470,12 @@ default, you must create it manually. - "name": "steam", - "icon": "sports_esports" - } -+ ], -+ "windowIcons": [ -+ { -+ "regex": "steam(_app_(default|[0-9]+))?", -+ "icon": "sports_esports" -+ } - ] - }, - "excludedScreens": [""], -@@ -422,13 +485,18 @@ default, you must create it manually. - }, - "border": { - "rounding": 25, -+ "smoothing": 32, - "thickness": 10 - }, - "dashboard": { - "enabled": true, -+ "showOnHover": true, -+ "showDashboard": true, -+ "showMedia": true, -+ "showPerformance": true, -+ "showWeather": true, - "dragThreshold": 50, -- "mediaUpdateInterval": 500, -- "showOnHover": true -+ "mediaUpdateInterval": 500 - }, - "launcher": { - "actionPrefix": ">", -@@ -560,10 +628,12 @@ default, you must create it manually. - "wallpapers": false - }, - "showOnHover": false, -+ "favouriteApps": [], - "hiddenApps": [] - }, - "lock": { -- "recolourLogo": false -+ "recolourLogo": false, -+ "hideNotifs": false - }, - "notifs": { - "actionOnClick": false, -@@ -582,7 +652,10 @@ default, you must create it manually. - "paths": { - "mediaGif": "root:/assets/bongocat.gif", - "sessionGif": "root:/assets/kurukuru.gif", -- "wallpaperDir": "~/Pictures/Wallpapers" -+ "noNotifsPic": "root:/assets/dino.png", -+ "lockNoNotifsPic": "root:/assets/dino.png", -+ "wallpaperDir": "~/Pictures/Wallpapers", -+ "lyricsDir": "~/Music/lyrics" - }, - "services": { - "audioIncrement": 0.1, -@@ -593,6 +666,7 @@ default, you must create it manually. - "playerAliases": [{ "from": "com.github.th_ch.youtube_music", "to": "YT Music" }], - "weatherLocation": "", - "useFahrenheit": false, -+ "useFahrenheitPerformance": false, - "useTwelveHourClock": false, - "smartScheme": true, - "visualiserBars": 45 -@@ -601,6 +675,12 @@ default, you must create it manually. - "dragThreshold": 30, - "enabled": true, - "vimKeybinds": false, -+ "icons": { -+ "logout": "logout", -+ "shutdown": "power_settings_new", -+ "hibernate": "downloading", -+ "reboot": "cached" -+ }, - "commands": { - "logout": ["loginctl", "terminate-user", ""], - "shutdown": ["systemctl", "poweroff"], -@@ -639,13 +719,59 @@ default, you must create it manually. - "enabled": false - } - ] -- } -+ }, -+ "quickToggles": [ -+ { -+ "id": "wifi", -+ "enabled": true -+ }, -+ { -+ "id": "bluetooth", -+ "enabled": true -+ }, -+ { -+ "id": "mic", -+ "enabled": true -+ }, -+ { -+ "enabled": true, -+ "id": "settings" -+ }, -+ { -+ "id": "gameMode", -+ "enabled": true -+ }, -+ { -+ "id": "dnd", -+ "enabled": true -+ }, -+ { -+ "id": "vpn", -+ "enabled": true -+ } -+ ] - } - } - ``` - -
- -+### Advanced configuration -+ -+> [!WARNING] -+> Do NOT change any of these options if you do not know what you are doing. These options control the -+> tokens used internally within the shell, and can cause visual issues if changed. The existence of -+> the options are also not guaranteed across versions, and may change or be removed without notice. -+ -+A separate `~/.config/caelestia/shell-tokens.json` file allows editing the internal tokens without -+touching the source code of the shell. These tokens affect, for example, individual rounding, -+spacing, padding, font size, animation duration and easing curves tokens, and the sizes of certain -+components. The appearance scale values in `shell.json` are multiplied against these base -+token values to produce the final computed values. -+ -+Per-monitor token overrides are also available at -+`~/.config/caelestia/monitors//shell-tokens.json`. -+ - ### Home Manager Module - - For NixOS users, a home manager module is also available. -diff --git a/assets/logo.svg b/assets/logo.svg -index 712d1e36..6879c92b 100644 ---- a/assets/logo.svg -+++ b/assets/logo.svg -@@ -1,21 +1,19 @@ - -- -- - - -- -- -+ inkscape:zoom="1.28" -+ inkscape:cx="43.359375" -+ inkscape:cy="183.98438" -+ inkscape:window-width="1824" -+ inkscape:window-height="1034" -+ inkscape:window-x="0" -+ inkscape:window-y="0" -+ inkscape:window-maximized="1" -+ inkscape:current-layer="Layer_2" /> - -+ id="defs1"> -+ -+ - -- -- -- -- -- -- -- -- -- -- -+ id="dark-4" -+ transform="matrix(0.08939103,0,0,0.08939103,-3.899948e-4,18.839439)"> -+ -+ -+ -+ -+ - - -diff --git a/assets/shaders/fade.frag b/assets/shaders/fade.frag -new file mode 100644 -index 00000000..a6cdf70d ---- /dev/null -+++ b/assets/shaders/fade.frag -@@ -0,0 +1,26 @@ -+#version 440 -+ -+layout(location = 0) in vec2 qt_TexCoord0; -+layout(location = 0) out vec4 fragColor; -+ -+layout(std140, binding = 0) uniform buf { -+ mat4 qt_Matrix; -+ float qt_Opacity; -+ float fadeMargin; -+}; -+ -+layout(binding = 1) uniform sampler2D source; -+ -+void main() { -+ vec4 tex = texture(source, qt_TexCoord0); -+ float factor = 1.0; -+ float margin = 0.1; -+ -+ if (qt_TexCoord0.y < margin) { -+ factor = qt_TexCoord0.y / margin; -+ } else if (qt_TexCoord0.y > (1.0 - margin)) { -+ factor = (1.0 - qt_TexCoord0.y) / margin; -+ } -+ -+ fragColor = tex * factor * qt_Opacity; -+} -diff --git a/assets/shaders/fade.frag.qsb b/assets/shaders/fade.frag.qsb -new file mode 100644 -index 00000000..888e4c10 -Binary files /dev/null and b/assets/shaders/fade.frag.qsb differ -diff --git a/components/AnchorAnim.qml b/components/AnchorAnim.qml -new file mode 100644 -index 00000000..dd62dc36 ---- /dev/null -+++ b/components/AnchorAnim.qml -@@ -0,0 +1,51 @@ -+import QtQuick -+import Caelestia.Config -+ -+AnchorAnimation { -+ enum Type { -+ StandardSmall = 0, -+ Standard, -+ StandardLarge, -+ StandardExtraLarge, -+ EmphasizedSmall, -+ Emphasized, -+ EmphasizedLarge, -+ EmphasizedExtraLarge, -+ FastSpatial, -+ DefaultSpatial, -+ SlowSpatial -+ } -+ -+ property int type: AnchorAnim.DefaultSpatial -+ -+ duration: { -+ if (type < AnchorAnim.StandardSmall || type > AnchorAnim.SlowSpatial) -+ return Tokens.anim.durations.expressiveDefaultSpatial; -+ -+ if (type == AnchorAnim.FastSpatial) -+ return Tokens.anim.durations.expressiveFastSpatial; -+ if (type == AnchorAnim.DefaultSpatial) -+ return Tokens.anim.durations.expressiveDefaultSpatial; -+ if (type == AnchorAnim.SlowSpatial) -+ return Tokens.anim.durations.expressiveSlowSpatial; -+ -+ const types = ["small", "normal", "large", "extraLarge"]; -+ const idx = type % 4; // 0-7 are the 4 standard types -+ return Tokens.anim.durations[types[idx]]; -+ } -+ easing: { -+ if (type == AnchorAnim.FastSpatial) -+ return Tokens.anim.expressiveFastSpatial; -+ if (type == AnchorAnim.DefaultSpatial) -+ return Tokens.anim.expressiveDefaultSpatial; -+ if (type == AnchorAnim.SlowSpatial) -+ return Tokens.anim.expressiveSlowSpatial; -+ -+ if (type >= AnchorAnim.StandardSmall && type <= AnchorAnim.StandardExtraLarge) -+ return Tokens.anim.standard; -+ if (type >= AnchorAnim.EmphasizedSmall && type <= AnchorAnim.EmphasizedExtraLarge) -+ return Tokens.anim.emphasized; -+ -+ return Tokens.anim.expressiveDefaultSpatial; -+ } -+} -diff --git a/components/Anim.qml b/components/Anim.qml -index 6883a798..8bc4468a 100644 ---- a/components/Anim.qml -+++ b/components/Anim.qml -@@ -1,8 +1,48 @@ --import qs.config - import QtQuick -+import Caelestia.Config - - NumberAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard -+ enum Type { -+ StandardSmall = 0, -+ Standard, -+ StandardLarge, -+ StandardExtraLarge, -+ EmphasizedSmall, -+ Emphasized, -+ EmphasizedLarge, -+ EmphasizedExtraLarge, -+ FastSpatial, -+ DefaultSpatial, -+ SlowSpatial -+ } -+ -+ property int type: Anim.Standard -+ -+ duration: { -+ if (type < Anim.StandardSmall || type > Anim.SlowSpatial) -+ return Tokens.anim.durations.normal; -+ -+ if (type == Anim.FastSpatial) -+ return Tokens.anim.durations.expressiveFastSpatial; -+ if (type == Anim.DefaultSpatial) -+ return Tokens.anim.durations.expressiveDefaultSpatial; -+ if (type == Anim.SlowSpatial) -+ return Tokens.anim.durations.expressiveSlowSpatial; -+ -+ const types = ["small", "normal", "large", "extraLarge"]; -+ const idx = type % 4; // 0-7 are the 4 standard types -+ return Tokens.anim.durations[types[idx]]; -+ } -+ easing: { -+ if (type == Anim.FastSpatial) -+ return Tokens.anim.expressiveFastSpatial; -+ if (type == Anim.DefaultSpatial) -+ return Tokens.anim.expressiveDefaultSpatial; -+ if (type == Anim.SlowSpatial) -+ return Tokens.anim.expressiveSlowSpatial; -+ -+ if (type >= Anim.EmphasizedSmall && type <= Anim.EmphasizedExtraLarge) -+ return Tokens.anim.emphasized; -+ return Tokens.anim.standard; -+ } - } -diff --git a/components/CAnim.qml b/components/CAnim.qml -index 49484b78..b86fcce1 100644 ---- a/components/CAnim.qml -+++ b/components/CAnim.qml -@@ -1,8 +1,7 @@ --import qs.config - import QtQuick -+import Caelestia.Config - - ColorAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard -+ duration: Tokens.anim.durations.normal -+ easing: Tokens.anim.standard - } -diff --git a/components/ConnectionHeader.qml b/components/ConnectionHeader.qml -index 12b42764..691fd3c5 100644 ---- a/components/ConnectionHeader.qml -+++ b/components/ConnectionHeader.qml -@@ -1,8 +1,7 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components - - ColumnLayout { - id: root -@@ -10,14 +9,14 @@ ColumnLayout { - required property string icon - required property string title - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - Layout.alignment: Qt.AlignHCenter - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - animate: true - text: root.icon -- font.pointSize: Appearance.font.size.extraLarge * 3 -+ font.pointSize: Tokens.font.size.extraLarge * 3 - font.bold: true - } - -@@ -25,7 +24,7 @@ ColumnLayout { - Layout.alignment: Qt.AlignHCenter - animate: true - text: root.title -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.bold: true - } - } -diff --git a/components/ConnectionInfoSection.qml b/components/ConnectionInfoSection.qml -index 927ef287..d94c3a6e 100644 ---- a/components/ConnectionInfoSection.qml -+++ b/components/ConnectionInfoSection.qml -@@ -1,16 +1,15 @@ --import qs.components --import qs.components.effects --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root - - required property var deviceDetails - -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { - text: qsTr("IP Address") -@@ -19,40 +18,40 @@ ColumnLayout { - StyledText { - text: root.deviceDetails?.ipAddress || qsTr("Not available") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Subnet Mask") - } - - StyledText { - text: root.deviceDetails?.subnet || qsTr("Not available") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Gateway") - } - - StyledText { - text: root.deviceDetails?.gateway || qsTr("Not available") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("DNS Servers") - } - - StyledText { - text: (root.deviceDetails && root.deviceDetails.dns && root.deviceDetails.dns.length > 0) ? root.deviceDetails.dns.join(", ") : qsTr("Not available") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - wrapMode: Text.Wrap - Layout.maximumWidth: parent.width - } -diff --git a/components/DashboardState.qml b/components/DashboardState.qml -new file mode 100644 -index 00000000..b4355cd7 ---- /dev/null -+++ b/components/DashboardState.qml -@@ -0,0 +1,6 @@ -+import Quickshell -+ -+PersistentProperties { -+ property int currentTab -+ property date currentDate: new Date() -+} -diff --git a/components/DrawerVisibilities.qml b/components/DrawerVisibilities.qml -new file mode 100644 -index 00000000..3286e319 ---- /dev/null -+++ b/components/DrawerVisibilities.qml -@@ -0,0 +1,11 @@ -+import Quickshell -+ -+PersistentProperties { -+ property bool bar -+ property bool osd -+ property bool session -+ property bool launcher -+ property bool dashboard -+ property bool utilities -+ property bool sidebar -+} -diff --git a/components/Logo.qml b/components/Logo.qml -new file mode 100644 -index 00000000..7cd41e17 ---- /dev/null -+++ b/components/Logo.qml -@@ -0,0 +1,70 @@ -+import QtQuick -+import QtQuick.Shapes -+import qs.services -+ -+Item { -+ id: root -+ -+ readonly property real designWidth: 128 -+ readonly property real designHeight: 90.38 -+ -+ property color topColour: Colours.palette.m3primary -+ property color bottomColour: Colours.palette.m3onSurface -+ -+ implicitWidth: designWidth -+ implicitHeight: designHeight -+ -+ Shape { -+ anchors.centerIn: parent -+ width: root.designWidth -+ height: root.designHeight -+ scale: Math.min(root.width / width, root.height / height) -+ transformOrigin: Item.Center -+ preferredRendererType: Shape.CurveRenderer -+ -+ ShapePath { -+ fillColor: root.topColour -+ strokeColor: "transparent" -+ -+ PathSvg { -+ path: "m42.56,42.96c-7.76,1.6-16.36,4.22-22.44,6.22-.49.16-.88-.44-.53-.82,5.37-5.85,9.66-13.3,9.66-13.3,8.66-14.67,22.97-23.51,39.85-21.14,6.47.91,12.33,3.38,17.26,6.98.99.72,1.14,2.14.31,3.04-.4.44-.95.67-1.51.67-.34,0-.69-.09-1-.26-3.21-1.84-6.82-2.69-10.71-3.24-13.1-1.84-25.41,4.75-31.06,15.83-.94,1.84-.61,3.81.45,5.21.22.3.07.72-.29.8Z" -+ } -+ } -+ -+ ShapePath { -+ fillColor: root.bottomColour -+ strokeColor: "transparent" -+ -+ PathSvg { -+ path: "m103.02,51.8c-.65.11-1.26-.37-1.28-1.03-.06-1.96.15-3.89-.2-5.78-.28-1.48-1.66-2.5-3.16-2.34h-.05c-6.53.73-24.63,3.1-48,9.32-6.89,1.83-9.83,10-5.67,15.79,4.62,6.44,11.84,10.93,20.41,12.13,11.82,1.66,22.99-3.36,29.21-12.65.54-.81,1.54-1.17,2.47-.86.91.3,1.47,1.15,1.47,2.04,0,.33-.08.66-.24.98-7.23,14.21-22.91,22.95-39.59,20.6-7.84-1.1-14.8-4.5-20.28-9.43,0,0,0,0-.02-.01-7.28-5.14-14.7-9.99-27.24-11.98-18.82-2.98-9.53-8.75.46-13.78,7.36-3.13,25.17-7.9,36.24-10.73.16-.03.31-.06.47-.1,1.52-.4,3.2-.83,5.02-1.29,1.06-.26,1.93-.48,2.58-.64.09-.02.18-.04.26-.06.31-.08.56-.14.73-.18.03,0,.06-.01.08-.02.03,0,.05-.01.07-.02.02,0,.04,0,.06-.01.01,0,.03,0,.04-.01,0,0,.02,0,.03,0,.01,0,.02,0,.02,0,10.62-2.58,24.63-5.62,37.74-7.34,1.02-.13,2.03-.26,3.03-.37,7.49-.87,14.58-1.26,20.42-.81,25.43,1.95-4.71,16.77-15.12,18.61Z" -+ } -+ } -+ -+ ShapePath { -+ fillColor: root.topColour -+ strokeColor: "transparent" -+ -+ PathSvg { -+ path: "m98.12.06c-.29,2.08-1.72,8.42-8.36,9.19-.09,0-.09.13,0,.14,6.64.78,8.07,7.11,8.36,9.19.01.08.13.08.14,0,.29-2.08,1.72-8.42,8.36-9.19.09,0,.09-.13,0-.14-6.64-.78-8.07-7.11-8.36-9.19-.01-.08-.13-.08-.14,0Z" -+ } -+ } -+ -+ ShapePath { -+ fillColor: root.topColour -+ strokeColor: "transparent" -+ -+ PathSvg { -+ path: "m113.36,15.5c-.22,1.29-1.08,4.35-4.38,4.87-.08.01-.08.13,0,.14,3.3.52,4.17,3.58,4.38,4.87.01.08.13.08.14,0,.22-1.29,1.08-4.35,4.38-4.87.08-.01.08-.13,0-.14-3.3-.52-4.17-3.58-4.38-4.87-.01-.08-.13-.08-.14,0Z" -+ } -+ } -+ -+ ShapePath { -+ fillColor: root.topColour -+ strokeColor: "transparent" -+ -+ PathSvg { -+ path: "m112.69,65.22c-.19,1.01-.86,3.15-3.2,3.57-.08.01-.08.13,0,.14,2.34.42,3.01,2.56,3.2,3.57.01.08.13.08.14,0,.19-1.01.86-3.15,3.2-3.57.08-.01.08-.13,0-.14-2.34-.42-3.01-2.56-3.2-3.57-.01-.08-.13-.08-.14,0Z" -+ } -+ } -+ } -+} -diff --git a/components/MaterialIcon.qml b/components/MaterialIcon.qml -index a1d19d3c..739c50ba 100644 ---- a/components/MaterialIcon.qml -+++ b/components/MaterialIcon.qml -@@ -1,12 +1,12 @@ -+import Caelestia.Config - import qs.services --import qs.config - - StyledText { - property real fill - property int grade: Colours.light ? 0 : -25 - -- font.family: Appearance.font.family.material -- font.pointSize: Appearance.font.size.larger -+ font.family: Tokens.font.family.material -+ font.pointSize: Tokens.font.size.larger - font.variableAxes: ({ - FILL: fill.toFixed(1), - GRAD: grade, -diff --git a/components/PropertyRow.qml b/components/PropertyRow.qml -index 640d5f74..4d302509 100644 ---- a/components/PropertyRow.qml -+++ b/components/PropertyRow.qml -@@ -1,8 +1,8 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root -@@ -11,16 +11,16 @@ ColumnLayout { - required property string value - property bool showTopMargin: false - -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { -- Layout.topMargin: root.showTopMargin ? Appearance.spacing.normal : 0 -+ Layout.topMargin: root.showTopMargin ? Tokens.spacing.normal : 0 - text: root.label - } - - StyledText { - text: root.value - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } -diff --git a/components/SectionContainer.qml b/components/SectionContainer.qml -index 2b653a5d..7aa3f13f 100644 ---- a/components/SectionContainer.qml -+++ b/components/SectionContainer.qml -@@ -1,21 +1,20 @@ --import qs.components --import qs.components.effects --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root - - default property alias content: contentColumn.data -- property real contentSpacing: Appearance.spacing.larger -+ property real contentSpacing: Tokens.spacing.larger - property bool alignTop: false - - Layout.fillWidth: true -- implicitHeight: contentColumn.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: contentColumn.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : Colours.palette.m3surfaceContainerHigh - - ColumnLayout { -@@ -25,7 +24,7 @@ StyledRect { - anchors.right: parent.right - anchors.top: root.alignTop ? parent.top : undefined - anchors.verticalCenter: root.alignTop ? undefined : parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - - spacing: root.contentSpacing - } -diff --git a/components/SectionHeader.qml b/components/SectionHeader.qml -index 502e9189..09364143 100644 ---- a/components/SectionHeader.qml -+++ b/components/SectionHeader.qml -@@ -1,8 +1,8 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root -@@ -13,9 +13,9 @@ ColumnLayout { - spacing: 0 - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: root.title -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -diff --git a/components/StateLayer.qml b/components/StateLayer.qml -index a20e2661..2ce8c500 100644 ---- a/components/StateLayer.qml -+++ b/components/StateLayer.qml -@@ -1,95 +1,186 @@ --import qs.services --import qs.config - import QtQuick -+import QtQuick.Shapes -+import Caelestia.Config -+import qs.services - - MouseArea { - id: root - - property bool disabled - property bool showHoverBackground: true -- property color color: Colours.palette.m3onSurface -- property real radius: parent?.radius ?? 0 -- property alias rect: hoverLayer -+ readonly property alias rect: base -+ -+ property bool shapeMorph -+ property real stateOpacity: pressed ? 0.1 : containsMouse ? 0.08 : 0 -+ -+ property real pressX: width / 2 -+ property real pressY: height / 2 -+ property real circleRadius -+ -+ property alias color: base.color -+ property alias radius: base.radius -+ property alias topLeftRadius: base.topLeftRadius -+ property alias topRightRadius: base.topRightRadius -+ property alias bottomLeftRadius: base.bottomLeftRadius -+ property alias bottomRightRadius: base.bottomRightRadius -+ -+ readonly property real endRadius: { -+ const d1 = distSq(0, 0); -+ const d2 = distSq(width, 0); -+ const d3 = distSq(0, height); -+ const d4 = distSq(width, height); -+ return Math.sqrt(Math.max(d1, d2, d3, d4)) * (shapeMorph ? 1.16 : 1); -+ } -+ property real endRadiusAtPress - -- function onClicked(): void { -+ function distSq(x: real, y: real): real { -+ return (pressX - x) ** 2 + (pressY - y) ** 2; - } - -- anchors.fill: parent -+ function clamp(r: real): real { -+ return Math.max(0, Math.min(r, width / 2, height / 2)); -+ } - -+ function press(x: real, y: real): void { -+ pressX = x; -+ pressY = y; -+ fadeAnim.complete(); -+ circleRadius = 0; -+ circle.opacity = 0.1; -+ rippleAnim.restart(); -+ endRadiusAtPress = endRadius; -+ } -+ -+ anchors.fill: parent - enabled: !disabled - cursorShape: disabled ? undefined : Qt.PointingHandCursor - hoverEnabled: true - -- onPressed: event => { -- if (disabled) -- return; -+ onPressed: e => press(e.x, e.y) - -- rippleAnim.x = event.x; -- rippleAnim.y = event.y; -- -- const dist = (ox, oy) => ox * ox + oy * oy; -- rippleAnim.radius = Math.sqrt(Math.max(dist(event.x, event.y), dist(event.x, height - event.y), dist(width - event.x, event.y), dist(width - event.x, height - event.y))); -- -- rippleAnim.restart(); -+ onPressedChanged: { -+ if (!pressed && !rippleAnim.running && circle.opacity > 0) -+ fadeAnim.start(); - } - -- onClicked: event => !disabled && onClicked(event) -+ onCircleRadiusChanged: { -+ if (!pressed && circleRadius > endRadiusAtPress * 0.99 && !fadeAnim.running) -+ fadeAnim.start(); -+ } - -- SequentialAnimation { -+ Anim { - id: rippleAnim - -- property real x -- property real y -- property real radius -+ alwaysRunToEnd: true -+ target: root -+ property: "circleRadius" -+ to: root.endRadius -+ easing: Tokens.anim.expressiveSlowEffects -+ duration: Tokens.anim.durations.expressiveSlowEffects * 2 -+ } - -- PropertyAction { -- target: ripple -- property: "x" -- value: rippleAnim.x -- } -- PropertyAction { -- target: ripple -- property: "y" -- value: rippleAnim.y -- } -- PropertyAction { -- target: ripple -- property: "opacity" -- value: 0.08 -- } -- Anim { -- target: ripple -- properties: "implicitWidth,implicitHeight" -- from: 0 -- to: rippleAnim.radius * 2 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -- } -- Anim { -- target: ripple -- property: "opacity" -- to: 0 -- } -+ Anim { -+ id: fadeAnim -+ -+ target: circle -+ property: "opacity" -+ to: 0 -+ easing: Tokens.anim.expressiveSlowEffects -+ duration: Tokens.anim.durations.expressiveSlowEffects - } - -- StyledClippingRect { -- id: hoverLayer -+ StyledRect { -+ id: base - - anchors.fill: parent -+ opacity: root.stateOpacity -+ color: Colours.palette.m3onSurface -+ // Pick up radius from parent if it has one (parent can be anything with a radius property) -+ radius: root.parent?.radius ?? 0 // qmllint disable missing-property -+ } - -- color: Qt.alpha(root.color, root.disabled ? 0 : root.pressed ? 0.12 : (root.showHoverBackground && root.containsMouse) ? 0.08 : 0) -- radius: root.radius -+ Shape { -+ id: circle - -- StyledRect { -- id: ripple -+ anchors.fill: parent -+ opacity: 0 -+ preferredRendererType: Shape.CurveRenderer -+ -+ ShapePath { -+ strokeWidth: 0 -+ strokeColor: "transparent" -+ fillColor: base.color -+ fillGradient: RadialGradient { -+ centerX: root.pressX -+ centerY: root.pressY -+ centerRadius: root.circleRadius -+ focalX: centerX -+ focalY: centerY -+ -+ GradientStop { -+ position: 0 -+ color: Qt.alpha(base.color, 1) -+ } -+ GradientStop { -+ position: 0.99 -+ color: Qt.alpha(base.color, 1) -+ } -+ GradientStop { -+ position: 1 -+ color: Qt.alpha(base.color, 0) -+ } -+ } - -- radius: Appearance.rounding.full -- color: root.color -- opacity: 0 -+ startX: root.clamp(base.topLeftRadius) -+ startY: 0 - -- transform: Translate { -- x: -ripple.width / 2 -- y: -ripple.height / 2 -+ PathLine { -+ x: root.width - root.clamp(base.topLeftRadius) -+ y: 0 -+ } -+ PathArc { -+ relativeX: root.clamp(base.topLeftRadius) -+ relativeY: root.clamp(base.topLeftRadius) -+ radiusX: root.clamp(base.topLeftRadius) -+ radiusY: root.clamp(base.topLeftRadius) -+ } -+ PathLine { -+ x: root.width -+ y: root.height - root.clamp(base.bottomRightRadius) - } -+ PathArc { -+ relativeX: -root.clamp(base.bottomRightRadius) -+ relativeY: root.clamp(base.bottomRightRadius) -+ radiusX: root.clamp(base.bottomRightRadius) -+ radiusY: root.clamp(base.bottomRightRadius) -+ } -+ PathLine { -+ x: root.clamp(base.bottomLeftRadius) -+ y: root.height -+ } -+ PathArc { -+ relativeX: -root.clamp(base.bottomLeftRadius) -+ relativeY: -root.clamp(base.bottomLeftRadius) -+ radiusX: root.clamp(base.bottomLeftRadius) -+ radiusY: root.clamp(base.bottomLeftRadius) -+ } -+ PathLine { -+ x: 0 -+ y: root.clamp(base.topLeftRadius) -+ } -+ PathArc { -+ x: root.clamp(base.topLeftRadius) -+ y: 0 -+ radiusX: root.clamp(base.topLeftRadius) -+ radiusY: root.clamp(base.topLeftRadius) -+ } -+ } -+ } -+ -+ Behavior on stateOpacity { -+ Anim { -+ easing: Tokens.anim.expressiveDefaultEffects -+ duration: Tokens.anim.durations.expressiveDefaultEffects - } - } - } -diff --git a/components/StyledClippingRect.qml b/components/StyledClippingRect.qml -index 8f2630c1..6484e0e6 100644 ---- a/components/StyledClippingRect.qml -+++ b/components/StyledClippingRect.qml -@@ -1,5 +1,5 @@ --import Quickshell.Widgets - import QtQuick -+import Quickshell.Widgets - - ClippingRectangle { - id: root -diff --git a/components/StyledText.qml b/components/StyledText.qml -index ed961d26..d3624c58 100644 ---- a/components/StyledText.qml -+++ b/components/StyledText.qml -@@ -1,8 +1,8 @@ - pragma ComponentBehavior: Bound - --import qs.services --import qs.config - import QtQuick -+import Caelestia.Config -+import qs.services - - Text { - id: root -@@ -11,13 +11,13 @@ Text { - property string animateProp: "scale" - property real animateFrom: 0 - property real animateTo: 1 -- property int animateDuration: Appearance.anim.durations.normal -+ property int animateDuration: Tokens.anim.durations.normal - - renderType: Text.NativeRendering - textFormat: Text.PlainText - color: Colours.palette.m3onSurface -- font.family: Appearance.font.family.sans -- font.pointSize: Appearance.font.size.smaller -+ font.family: Tokens.font.family.sans -+ font.pointSize: Tokens.font.size.smaller - - Behavior on color { - CAnim {} -@@ -29,12 +29,12 @@ Text { - SequentialAnimation { - Anim { - to: root.animateFrom -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ easing: Tokens.anim.standardAccel - } - PropertyAction {} - Anim { - to: root.animateTo -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - } - } -@@ -43,6 +43,5 @@ Text { - target: root - property: root.animateProp - duration: root.animateDuration / 2 -- easing.type: Easing.BezierSpline - } - } -diff --git a/components/containers/StyledFlickable.qml b/components/containers/StyledFlickable.qml -index bc6ae0f6..b792e583 100644 ---- a/components/containers/StyledFlickable.qml -+++ b/components/containers/StyledFlickable.qml -@@ -1,5 +1,5 @@ --import ".." - import QtQuick -+import qs.components - - Flickable { - id: root -diff --git a/components/containers/StyledListView.qml b/components/containers/StyledListView.qml -index 626d2063..8b5a4401 100644 ---- a/components/containers/StyledListView.qml -+++ b/components/containers/StyledListView.qml -@@ -1,5 +1,5 @@ --import ".." - import QtQuick -+import qs.components - - ListView { - id: root -diff --git a/components/containers/StyledWindow.qml b/components/containers/StyledWindow.qml -index 8c6e39fc..72bbed6f 100644 ---- a/components/containers/StyledWindow.qml -+++ b/components/containers/StyledWindow.qml -@@ -1,9 +1,15 @@ - import Quickshell - import Quickshell.Wayland -+import Caelestia.Config - -+// qmllint disable uncreatable-type - PanelWindow { -+ // qmllint enable uncreatable-type - required property string name - - WlrLayershell.namespace: `caelestia-${name}` - color: "transparent" -+ -+ contentItem.Config.screen: screen.name -+ contentItem.Tokens.screen: screen.name - } -diff --git a/components/controls/CircularIndicator.qml b/components/controls/CircularIndicator.qml -index 957899e5..aabda13a 100644 ---- a/components/controls/CircularIndicator.qml -+++ b/components/controls/CircularIndicator.qml -@@ -1,9 +1,9 @@ --import ".." --import qs.services --import qs.config --import Caelestia.Internal - import QtQuick - import QtQuick.Templates -+import Caelestia.Config -+import Caelestia.Internal -+import qs.components -+import qs.services - - BusyIndicator { - id: root -@@ -19,8 +19,8 @@ BusyIndicator { - Completing - } - -- property real implicitSize: Appearance.font.size.normal * 3 -- property real strokeWidth: Appearance.padding.small * 0.8 -+ property real implicitSize: Tokens.font.size.normal * 3 -+ property real strokeWidth: Tokens.padding.small * 0.8 - property color fgColour: Colours.palette.m3primary - property color bgColour: Colours.palette.m3secondaryContainer - -@@ -64,7 +64,7 @@ BusyIndicator { - transitions: Transition { - Anim { - properties: "opacity,internalStrokeWidth" -- duration: manager.completeEndDuration * Appearance.anim.durations.scale -+ duration: manager.completeEndDuration * Tokens.anim.durations.scale - } - } - -@@ -90,7 +90,7 @@ BusyIndicator { - property: "progress" - from: 0 - to: 1 -- duration: manager.duration * Appearance.anim.durations.scale -+ duration: manager.duration * Tokens.anim.durations.scale - } - - NumberAnimation { -@@ -99,7 +99,7 @@ BusyIndicator { - property: "completeEndProgress" - from: 0 - to: 1 -- duration: manager.completeEndDuration * Appearance.anim.durations.scale -+ duration: manager.completeEndDuration * Tokens.anim.durations.scale - onFinished: { - if (root.animState === CircularIndicator.Completing) - root.animState = CircularIndicator.Stopped; -diff --git a/components/controls/CircularProgress.qml b/components/controls/CircularProgress.qml -index a15cd900..2372c86c 100644 ---- a/components/controls/CircularProgress.qml -+++ b/components/controls/CircularProgress.qml -@@ -1,17 +1,17 @@ --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Shapes -+import Caelestia.Config -+import qs.components -+import qs.services - - Shape { - id: root - - property real value - property int startAngle: -90 -- property int strokeWidth: Appearance.padding.smaller -+ property int strokeWidth: Tokens.padding.smaller - property int padding: 0 -- property int spacing: Appearance.spacing.small -+ property int spacing: Tokens.spacing.small - property color fgColour: Colours.palette.m3primary - property color bgColour: Colours.palette.m3secondaryContainer - -@@ -27,7 +27,7 @@ Shape { - fillColor: "transparent" - strokeColor: root.bgColour - strokeWidth: root.strokeWidth -- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap -+ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - - PathAngleArc { - startAngle: root.startAngle + 360 * root.vValue + root.gapAngle -@@ -40,7 +40,7 @@ Shape { - - Behavior on strokeColor { - CAnim { -- duration: Appearance.anim.durations.large -+ duration: Tokens.anim.durations.large - } - } - } -@@ -49,7 +49,7 @@ Shape { - fillColor: "transparent" - strokeColor: root.fgColour - strokeWidth: root.strokeWidth -- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap -+ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - - PathAngleArc { - startAngle: root.startAngle -@@ -62,7 +62,7 @@ Shape { - - Behavior on strokeColor { - CAnim { -- duration: Appearance.anim.durations.large -+ duration: Tokens.anim.durations.large - } - } - } -diff --git a/components/controls/CollapsibleSection.qml b/components/controls/CollapsibleSection.qml -index e3d8eefd..80ea8fe8 100644 ---- a/components/controls/CollapsibleSection.qml -+++ b/components/controls/CollapsibleSection.qml -@@ -1,10 +1,8 @@ --import ".." --import qs.components --import qs.components.effects --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root -@@ -15,28 +13,32 @@ ColumnLayout { - property bool showBackground: false - property bool nested: false - -+ default property alias content: contentColumn.data -+ - signal toggleRequested - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - Layout.fillWidth: true - - Item { - id: sectionHeaderItem -+ - Layout.fillWidth: true -- Layout.preferredHeight: Math.max(titleRow.implicitHeight + Appearance.padding.normal * 2, 48) -+ Layout.preferredHeight: Math.max(titleRow.implicitHeight + Tokens.padding.normal * 2, 48) - - RowLayout { - id: titleRow -+ - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: Appearance.padding.normal -- anchors.rightMargin: Appearance.padding.normal -- spacing: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.padding.normal -+ anchors.rightMargin: Tokens.padding.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: root.title -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -48,11 +50,11 @@ ColumnLayout { - text: "expand_more" - rotation: root.expanded ? 180 : 0 - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal -+ - Behavior on rotation { - Anim { -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.standard -+ type: Anim.StandardSmall - } - } - } -@@ -61,70 +63,66 @@ ColumnLayout { - StateLayer { - anchors.fill: parent - color: Colours.palette.m3onSurface -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - showHoverBackground: false -- function onClicked(): void { -+ onClicked: { - root.toggleRequested(); - root.expanded = !root.expanded; - } - } - } - -- default property alias content: contentColumn.data -- - Item { - id: contentWrapper -+ - Layout.fillWidth: true -- Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Appearance.spacing.small * 2) : 0 -+ Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Tokens.spacing.small * 2) : 0 - clip: true - - Behavior on Layout.preferredHeight { -- Anim { -- easing.bezierCurve: Appearance.anim.curves.standard -- } -+ Anim {} - } - - StyledRect { - id: backgroundRect -+ - anchors.fill: parent -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.transparency.enabled ? Colours.layer(Colours.palette.m3surfaceContainer, root.nested ? 3 : 2) : (root.nested ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3surfaceContainer) - opacity: root.showBackground && root.expanded ? 1.0 : 0.0 - visible: root.showBackground - - Behavior on opacity { -- Anim { -- easing.bezierCurve: Appearance.anim.curves.standard -- } -+ Anim {} - } - } - - ColumnLayout { - id: contentColumn -+ - anchors.left: parent.left - anchors.right: parent.right -- y: Appearance.spacing.small -- anchors.leftMargin: Appearance.padding.normal -- anchors.rightMargin: Appearance.padding.normal -- anchors.bottomMargin: Appearance.spacing.small -- spacing: Appearance.spacing.small -+ y: Tokens.spacing.small -+ anchors.leftMargin: Tokens.padding.normal -+ anchors.rightMargin: Tokens.padding.normal -+ anchors.bottomMargin: Tokens.spacing.small -+ spacing: Tokens.spacing.small - opacity: root.expanded ? 1.0 : 0.0 - - Behavior on opacity { -- Anim { -- easing.bezierCurve: Appearance.anim.curves.standard -- } -+ Anim {} - } - - StyledText { - id: descriptionText -+ - Layout.fillWidth: true -- Layout.topMargin: root.description !== "" ? Appearance.spacing.smaller : 0 -- Layout.bottomMargin: root.description !== "" ? Appearance.spacing.small : 0 -+ Layout.topMargin: root.description !== "" ? Tokens.spacing.smaller : 0 -+ Layout.bottomMargin: root.description !== "" ? Tokens.spacing.small : 0 - visible: root.description !== "" - text: root.description - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - wrapMode: Text.Wrap - } - } -diff --git a/components/controls/CustomSpinBox.qml b/components/controls/CustomSpinBox.qml -index 438dc080..d7885c30 100644 ---- a/components/controls/CustomSpinBox.qml -+++ b/components/controls/CustomSpinBox.qml -@@ -1,10 +1,10 @@ - pragma ComponentBehavior: Bound - --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - RowLayout { - id: root -@@ -15,13 +15,13 @@ RowLayout { - property real step: 1 - property alias repeatRate: timer.interval - -- signal valueModified(value: real) -- -- spacing: Appearance.spacing.small -- - property bool isEditing: false - property string displayText: root.value.toString() - -+ signal valueModified(value: real) -+ -+ spacing: Tokens.spacing.small -+ - onValueChanged: { - if (!root.isEditing) { - root.displayText = root.value.toString(); -@@ -73,23 +73,23 @@ RowLayout { - root.isEditing = false; - } - -- padding: Appearance.padding.small -- leftPadding: Appearance.padding.normal -- rightPadding: Appearance.padding.normal -+ padding: Tokens.padding.small -+ leftPadding: Tokens.padding.normal -+ rightPadding: Tokens.padding.normal - - background: StyledRect { - implicitWidth: 100 -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainerHigh - } - } - - StyledRect { -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.palette.m3primary - - implicitWidth: implicitHeight -- implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: upState -@@ -99,7 +99,7 @@ RowLayout { - onPressAndHold: timer.start() - onReleased: timer.stop() - -- function onClicked(): void { -+ onClicked: { - let newValue = Math.min(root.max, root.value + root.step); - // Round to avoid floating point precision errors - const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; -@@ -120,21 +120,16 @@ RowLayout { - } - - StyledRect { -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.palette.m3primary - - implicitWidth: implicitHeight -- implicitHeight: downIcon.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: downIcon.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: downState - -- color: Colours.palette.m3onPrimary -- -- onPressAndHold: timer.start() -- onReleased: timer.stop() -- -- function onClicked(): void { -+ onClicked: { - let newValue = Math.max(root.min, root.value - root.step); - // Round to avoid floating point precision errors - const decimals = root.step < 1 ? Math.max(1, Math.ceil(-Math.log10(root.step))) : 0; -@@ -143,6 +138,11 @@ RowLayout { - root.displayText = newValue.toString(); - root.valueModified(newValue); - } -+ -+ color: Colours.palette.m3onPrimary -+ -+ onPressAndHold: timer.start() -+ onReleased: timer.stop() - } - - MaterialIcon { -@@ -162,9 +162,9 @@ RowLayout { - triggeredOnStart: true - onTriggered: { - if (upState.pressed) -- upState.onClicked(); -+ upState.clicked(); - else if (downState.pressed) -- downState.onClicked(); -+ downState.clicked(); - } - } - } -diff --git a/components/controls/FilledSlider.qml b/components/controls/FilledSlider.qml -index 80dd44c5..1d8c85a9 100644 ---- a/components/controls/FilledSlider.qml -+++ b/components/controls/FilledSlider.qml -@@ -1,9 +1,9 @@ --import ".." - import "../effects" --import qs.services --import qs.config - import QtQuick - import QtQuick.Templates -+import Caelestia.Config -+import qs.components -+import qs.services - - Slider { - id: root -@@ -16,7 +16,7 @@ Slider { - - background: StyledRect { - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - StyledRect { - anchors.left: parent.left -@@ -51,7 +51,7 @@ Slider { - anchors.fill: parent - - color: Colours.palette.m3inverseSurface -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - MouseArea { - id: handleInteraction -@@ -70,8 +70,8 @@ Slider { - function update(): void { - animate = !moving; - binding.when = moving; -- font.pointSize = moving ? Appearance.font.size.small : Appearance.font.size.larger; -- font.family = moving ? Appearance.font.family.sans : Appearance.font.family.material; -+ font.pointSize = moving ? Tokens.font.size.small : Tokens.font.size.larger; -+ font.family = moving ? Tokens.font.family.sans : Tokens.font.family.material; - } - - text: root.icon -@@ -96,8 +96,8 @@ Slider { - target: icon - property: "scale" - to: 0 -- duration: Appearance.anim.durations.normal / 2 -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ duration: Tokens.anim.durations.normal / 2 -+ easing: Tokens.anim.standardAccel - } - ScriptAction { - script: icon.update() -@@ -106,8 +106,8 @@ Slider { - target: icon - property: "scale" - to: 1 -- duration: Appearance.anim.durations.normal / 2 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ duration: Tokens.anim.durations.normal / 2 -+ easing: Tokens.anim.standardDecel - } - } - } -@@ -140,7 +140,7 @@ Slider { - - Behavior on value { - Anim { -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - } - } -diff --git a/components/controls/IconButton.qml b/components/controls/IconButton.qml -index ffb1d066..58af806a 100644 ---- a/components/controls/IconButton.qml -+++ b/components/controls/IconButton.qml -@@ -1,7 +1,7 @@ --import ".." --import qs.services --import qs.config - import QtQuick -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root -@@ -15,7 +15,7 @@ StyledRect { - property alias icon: label.text - property bool checked - property bool toggle -- property real padding: type === IconButton.Text ? Appearance.padding.small / 2 : Appearance.padding.smaller -+ property real padding: type === IconButton.Text ? Tokens.padding.small / 2 : Tokens.padding.smaller - property alias font: label.font - property int type: IconButton.Filled - property bool disabled -@@ -44,7 +44,7 @@ StyledRect { - - onCheckedChanged: internalChecked = checked - -- radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) -+ radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour - - implicitWidth: implicitHeight -@@ -55,8 +55,7 @@ StyledRect { - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour - disabled: root.disabled -- -- function onClicked(): void { -+ onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); -diff --git a/components/controls/IconTextButton.qml b/components/controls/IconTextButton.qml -index b2bb96cc..2a1dacec 100644 ---- a/components/controls/IconTextButton.qml -+++ b/components/controls/IconTextButton.qml -@@ -1,8 +1,8 @@ --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root -@@ -17,8 +17,8 @@ StyledRect { - property alias text: label.text - property bool checked - property bool toggle -- property real horizontalPadding: Appearance.padding.normal -- property real verticalPadding: Appearance.padding.smaller -+ property real horizontalPadding: Tokens.padding.normal -+ property real verticalPadding: Tokens.padding.smaller - property alias font: label.font - property int type: IconTextButton.Filled - -@@ -36,7 +36,7 @@ StyledRect { - - onCheckedChanged: internalChecked = checked - -- radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) -+ radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === IconTextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour - - implicitWidth: row.implicitWidth + horizontalPadding * 2 -@@ -46,8 +46,7 @@ StyledRect { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour -- -- function onClicked(): void { -+ onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); -@@ -58,7 +57,7 @@ StyledRect { - id: row - - anchors.centerIn: parent -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - MaterialIcon { - id: iconLabel -diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml -index c763b54a..482a4d40 100644 ---- a/components/controls/Menu.qml -+++ b/components/controls/Menu.qml -@@ -1,95 +1,187 @@ - pragma ComponentBehavior: Bound - --import ".." --import "../effects" --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.components.effects -+import qs.services -+import qs.modules.drawers - --Elevation { -+MouseArea { - id: root - -+ enum Side { -+ Top, -+ Bottom, -+ Left, -+ Right -+ } -+ -+ required property Item attachTo -+ property int attachSideX: Menu.Right -+ property int attachSideY: Menu.Bottom -+ property int thisSideX: Menu.Right -+ property int thisSideY: Menu.Top -+ property real marginX -+ property real marginY -+ - property list items - property MenuItem active: items[0] ?? null - property bool expanded - - signal itemSelected(item: MenuItem) - -- radius: Appearance.rounding.small / 2 -- level: 2 -+ parent: { -+ const win = QsWindow.window; -+ const contentWin = win as ContentWindow; // If inside the drawer content window, put it inside the interaction wrapper so hover works -+ return contentWin ? contentWin.interactionWrapper : (win as QsWindow).contentItem; -+ } -+ anchors.fill: parent - -- implicitWidth: Math.max(200, column.implicitWidth) -- implicitHeight: root.expanded ? column.implicitHeight : 0 -- opacity: root.expanded ? 1 : 0 -+ enabled: expanded -+ onClicked: expanded = false - -- StyledClippingRect { -- anchors.fill: parent -- radius: parent.radius -- color: Colours.palette.m3surfaceContainer -+ opacity: expanded ? 1 : 0 -+ layer.enabled: opacity < 1 - -- ColumnLayout { -- id: column -+ Behavior on opacity { -+ Anim { -+ duration: Tokens.anim.durations.small -+ } -+ } - -- anchors.left: parent.left -- anchors.right: parent.right -- spacing: 0 -+ TransformWatcher { -+ id: watcher - -- Repeater { -- model: root.items -+ a: root.parent -+ b: root.attachTo -+ } - -- StyledRect { -- id: item -+ Elevation { -+ id: menu - -- required property int index -- required property MenuItem modelData -- readonly property bool active: modelData === root.active -+ x: { -+ watcher.transform; // mapToItem is not reactive so this forces updates -+ const item = root.attachTo; -+ let off = root.attachSideX === Menu.Left ? 0 : item.width; -+ if (root.thisSideX === Menu.Right) -+ off -= width; -+ return item.mapToItem(root.parent, off, 0).x + root.marginX; -+ } -+ y: { -+ watcher.transform; // mapToItem is not reactive so this forces updates -+ const item = root.attachTo; -+ let off = root.attachSideY === Menu.Top ? 0 : item.height; -+ if (root.thisSideY === Menu.Bottom) -+ off -= height; -+ return item.mapToItem(root.parent, 0, off).y + root.marginY; -+ } - -- Layout.fillWidth: true -- implicitWidth: menuOptionRow.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: menuOptionRow.implicitHeight + Appearance.padding.normal * 2 -+ radius: Tokens.rounding.normal -+ level: 2 - -- color: Qt.alpha(Colours.palette.m3secondaryContainer, active ? 1 : 0) -+ implicitWidth: Math.max(200, column.implicitWidth + column.anchors.margins * 2) -+ implicitHeight: column.implicitHeight + column.anchors.margins * 2 - -- StateLayer { -- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- disabled: !root.expanded -+ transform: Scale { -+ yScale: root.expanded ? 1 : 0.1 -+ origin.y: root.thisSideY === Menu.Bottom ? menu.height : 0 - -- function onClicked(): void { -- root.itemSelected(item.modelData); -- root.active = item.modelData; -- root.expanded = false; -- } -- } -+ Behavior on yScale { -+ Anim { -+ type: Anim.DefaultSpatial -+ } -+ } -+ } -+ -+ StyledRect { -+ anchors.fill: parent -+ radius: parent.radius -+ color: Colours.palette.m3surfaceContainerLow -+ -+ ColumnLayout { -+ id: column -+ -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.small -+ spacing: 0 - -- RowLayout { -- id: menuOptionRow -+ Repeater { -+ id: repeater - -- anchors.fill: parent -- anchors.margins: Appearance.padding.normal -- spacing: Appearance.spacing.small -+ model: root.items - -- MaterialIcon { -- Layout.alignment: Qt.AlignVCenter -- text: item.modelData.icon -- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurfaceVariant -+ StyledRect { -+ id: item -+ -+ required property int index -+ required property MenuItem modelData -+ readonly property bool active: modelData === root.active -+ -+ Layout.fillWidth: true -+ implicitWidth: menuOptionRow.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: menuOptionRow.implicitHeight + Tokens.padding.normal * 2 -+ -+ radius: active ? 12 : Tokens.rounding.extraSmall // This should use a token, but tokens are currently extremely scuffed -+ topLeftRadius: index === 0 ? Tokens.rounding.small : radius -+ topRightRadius: index === 0 ? Tokens.rounding.small : radius -+ bottomLeftRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius -+ bottomRightRadius: index === repeater.count - 1 ? Tokens.rounding.small : radius -+ -+ color: Qt.alpha(Colours.palette.m3tertiaryContainer, active ? 1 : 0) -+ -+ Behavior on radius { -+ Anim {} - } - -- StyledText { -- Layout.alignment: Qt.AlignVCenter -- Layout.fillWidth: true -- text: item.modelData.text -- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -+ StateLayer { -+ topLeftRadius: parent.topLeftRadius -+ topRightRadius: parent.topRightRadius -+ bottomLeftRadius: parent.bottomLeftRadius -+ bottomRightRadius: parent.bottomRightRadius -+ -+ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface -+ disabled: !root.expanded -+ onClicked: { -+ root.itemSelected(item.modelData); -+ root.active = item.modelData; -+ item.modelData.clicked(); -+ root.expanded = false; -+ } - } - -- Loader { -- Layout.alignment: Qt.AlignVCenter -- active: item.modelData.trailingIcon.length > 0 -- visible: active -+ RowLayout { -+ id: menuOptionRow - -- sourceComponent: MaterialIcon { -- text: item.modelData.trailingIcon -- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.small -+ -+ MaterialIcon { -+ Layout.alignment: Qt.AlignVCenter -+ text: item.modelData.icon -+ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant -+ } -+ -+ StyledText { -+ Layout.alignment: Qt.AlignVCenter -+ Layout.fillWidth: true -+ text: item.modelData.text -+ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurface -+ } -+ -+ Loader { -+ asynchronous: true -+ Layout.alignment: Qt.AlignVCenter -+ active: item.modelData.trailingIcon.length > 0 -+ visible: active -+ -+ sourceComponent: MaterialIcon { -+ text: item.modelData.trailingIcon -+ color: item.active ? Colours.palette.m3onTertiaryContainer : Colours.palette.m3onSurfaceVariant -+ } - } - } - } -@@ -97,17 +189,4 @@ Elevation { - } - } - } -- -- Behavior on opacity { -- Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- } -- } -- -- Behavior on implicitHeight { -- Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- } - } -diff --git a/components/controls/SpinBoxRow.qml b/components/controls/SpinBoxRow.qml -index fe6a1982..dc52c19c 100644 ---- a/components/controls/SpinBoxRow.qml -+++ b/components/controls/SpinBoxRow.qml -@@ -1,10 +1,8 @@ --import ".." --import qs.components --import qs.components.effects --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root -@@ -17,8 +15,8 @@ StyledRect { - property var onValueModified: function (value) {} - - Layout.fillWidth: true -- implicitHeight: row.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -+ implicitHeight: row.implicitHeight + Tokens.padding.large * 2 -+ radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { -@@ -31,8 +29,8 @@ StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -@@ -45,7 +43,7 @@ StyledRect { - step: root.step - value: root.value - onValueModified: value => { -- root.onValueModified(value); -+ root.onValueModified(value); // qmllint disable use-proper-function - } - } - } -diff --git a/components/controls/SplitButton.qml b/components/controls/SplitButton.qml -index c91474ea..4acd98cf 100644 ---- a/components/controls/SplitButton.qml -+++ b/components/controls/SplitButton.qml -@@ -1,8 +1,8 @@ --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - Row { - id: root -@@ -12,8 +12,8 @@ Row { - Tonal - } - -- property real horizontalPadding: Appearance.padding.normal -- property real verticalPadding: Appearance.padding.smaller -+ property real horizontalPadding: Tokens.padding.normal -+ property real verticalPadding: Tokens.padding.smaller - property int type: SplitButton.Filled - property bool disabled - property bool menuOnTop -@@ -33,12 +33,12 @@ Row { - property color disabledColour: Qt.alpha(Colours.palette.m3onSurface, 0.1) - property color disabledTextColour: Qt.alpha(Colours.palette.m3onSurface, 0.38) - -- spacing: Math.floor(Appearance.spacing.small / 2) -+ spacing: Math.floor(Tokens.spacing.small / 2) - - StyledRect { -- radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) -- topRightRadius: Appearance.rounding.small / 2 -- bottomRightRadius: Appearance.rounding.small / 2 -+ radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) -+ topRightRadius: Tokens.rounding.small / 2 -+ bottomRightRadius: Tokens.rounding.small / 2 - color: root.disabled ? root.disabledColour : root.colour - - implicitWidth: textRow.implicitWidth + root.horizontalPadding * 2 -@@ -51,10 +51,7 @@ Row { - rect.bottomRightRadius: parent.bottomRightRadius - color: root.textColour - disabled: root.disabled -- -- function onClicked(): void { -- root.active?.clicked(); -- } -+ onClicked: root.active?.clicked() - } - - RowLayout { -@@ -62,7 +59,7 @@ Row { - - anchors.centerIn: parent - anchors.horizontalCenterOffset: Math.floor(root.verticalPadding / 4) -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - MaterialIcon { - id: iconLabel -@@ -86,7 +83,7 @@ Row { - - Behavior on Layout.preferredWidth { - Anim { -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - } -@@ -96,9 +93,9 @@ Row { - StyledRect { - id: expandBtn - -- property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) : Appearance.rounding.small / 2 -+ property real rad: root.expanded ? implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) : Tokens.rounding.small / 2 - -- radius: implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) -+ radius: implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - topLeftRadius: rad - bottomLeftRadius: rad - color: root.disabled ? root.disabledColour : root.colour -@@ -113,10 +110,7 @@ Row { - rect.bottomLeftRadius: parent.bottomLeftRadius - color: root.textColour - disabled: root.disabled -- -- function onClicked(): void { -- root.expanded = !root.expanded; -- } -+ onClicked: root.expanded = !root.expanded - } - - MaterialIcon { -@@ -141,24 +135,14 @@ Row { - Behavior on rad { - Anim {} - } -+ } - -- Menu { -- id: menu -- -- states: State { -- when: root.menuOnTop -- -- AnchorChanges { -- target: menu -- anchors.top: undefined -- anchors.bottom: expandBtn.top -- } -- } -+ Menu { -+ id: menu - -- anchors.top: parent.bottom -- anchors.right: parent.right -- anchors.topMargin: Appearance.spacing.small -- anchors.bottomMargin: Appearance.spacing.small -- } -+ attachTo: expandBtn -+ attachSideY: root.menuOnTop ? Menu.Top : Menu.Bottom -+ thisSideY: root.menuOnTop ? Menu.Bottom : Menu.Top -+ marginY: Tokens.spacing.small * (root.menuOnTop ? -1 : 1) - } - } -diff --git a/components/controls/SplitButtonRow.qml b/components/controls/SplitButtonRow.qml -index db9925ff..bd1ecc62 100644 ---- a/components/controls/SplitButtonRow.qml -+++ b/components/controls/SplitButtonRow.qml -@@ -1,19 +1,16 @@ - pragma ComponentBehavior: Bound - --import ".." --import qs.components --import qs.components.effects --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root - - required property string label - property int expandedZ: 100 -- property bool enabled: true - - property alias menuItems: splitButton.menuItems - property alias active: splitButton.active -@@ -23,8 +20,8 @@ StyledRect { - signal selected(item: MenuItem) - - Layout.fillWidth: true -- implicitHeight: row.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -+ implicitHeight: row.implicitHeight + Tokens.padding.large * 2 -+ radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - clip: false -@@ -33,9 +30,10 @@ StyledRect { - - RowLayout { - id: row -+ - anchors.fill: parent -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -@@ -45,6 +43,7 @@ StyledRect { - - SplitButton { - id: splitButton -+ - enabled: root.enabled - type: SplitButton.Filled - -diff --git a/components/controls/StyledInputField.qml b/components/controls/StyledInputField.qml -index 0d199c73..ff7772ca 100644 ---- a/components/controls/StyledInputField.qml -+++ b/components/controls/StyledInputField.qml -@@ -1,10 +1,9 @@ - pragma ComponentBehavior: Bound - --import ".." -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import QtQuick - - Item { - id: root -@@ -13,23 +12,23 @@ Item { - property var validator: null - property bool readOnly: false - property int horizontalAlignment: TextInput.AlignHCenter -- property int implicitWidth: 70 -- property bool enabled: true - - // Expose activeFocus through alias to avoid FINAL property override - readonly property alias hasFocus: inputField.activeFocus - - signal textEdited(string text) -+ - signal editingFinished - -- implicitHeight: inputField.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: 70 -+ implicitHeight: inputField.implicitHeight + Tokens.padding.small * 2 - - StyledRect { - id: container - - anchors.fill: parent - color: inputHover.containsMouse || inputField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - border.width: 1 - border.color: inputField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - opacity: root.enabled ? 1 : 0.5 -@@ -43,6 +42,7 @@ Item { - - MouseArea { - id: inputHover -+ - anchors.fill: parent - hoverEnabled: true - cursorShape: Qt.IBeamCursor -@@ -52,20 +52,14 @@ Item { - - StyledTextField { - id: inputField -+ - anchors.centerIn: parent -- width: parent.width - Appearance.padding.normal -+ width: parent.width - Tokens.padding.normal - horizontalAlignment: root.horizontalAlignment - validator: root.validator - readOnly: root.readOnly - enabled: root.enabled - -- Binding { -- target: inputField -- property: "text" -- value: root.text -- when: !inputField.activeFocus -- } -- - onTextChanged: { - root.text = text; - root.textEdited(text); -@@ -74,6 +68,13 @@ Item { - onEditingFinished: { - root.editingFinished(); - } -+ -+ Binding { -+ target: inputField -+ property: "text" -+ value: root.text -+ when: !inputField.activeFocus -+ } - } - } - } -diff --git a/components/controls/StyledRadioButton.qml b/components/controls/StyledRadioButton.qml -index b72fc77f..24c7d6e8 100644 ---- a/components/controls/StyledRadioButton.qml -+++ b/components/controls/StyledRadioButton.qml -@@ -1,13 +1,13 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Templates -+import Caelestia.Config -+import qs.components -+import qs.services - - RadioButton { - id: root - -- font.pointSize: Appearance.font.size.smaller -+ font.pointSize: Tokens.font.size.smaller - - implicitWidth: implicitIndicatorWidth + implicitContentWidth + contentItem.anchors.leftMargin - implicitHeight: Math.max(implicitIndicatorHeight, implicitContentHeight) -@@ -17,20 +17,17 @@ RadioButton { - - implicitWidth: 20 - implicitHeight: 20 -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: "transparent" - border.color: root.checked ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - border.width: 2 - anchors.verticalCenter: parent.verticalCenter - - StateLayer { -- anchors.margins: -Appearance.padding.smaller -+ anchors.margins: -Tokens.padding.smaller - color: root.checked ? Colours.palette.m3onSurface : Colours.palette.m3primary - z: -1 -- -- function onClicked(): void { -- root.click(); -- } -+ onClicked: root.click() - } - - StyledRect { -@@ -38,7 +35,7 @@ RadioButton { - implicitWidth: 8 - implicitHeight: 8 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primary, root.checked ? 1 : 0) - } - -@@ -52,6 +49,6 @@ RadioButton { - font.pointSize: root.font.pointSize - anchors.verticalCenter: parent.verticalCenter - anchors.left: outerCircle.right -- anchors.leftMargin: Appearance.spacing.smaller -+ anchors.leftMargin: Tokens.spacing.smaller - } - } -diff --git a/components/controls/StyledScrollBar.qml b/components/controls/StyledScrollBar.qml -index de8b679c..8cfe3f8d 100644 ---- a/components/controls/StyledScrollBar.qml -+++ b/components/controls/StyledScrollBar.qml -@@ -1,8 +1,8 @@ --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Templates -+import Caelestia.Config -+import qs.components -+import qs.services - - ScrollBar { - id: root -@@ -11,6 +11,8 @@ ScrollBar { - property bool shouldBeActive - property real nonAnimPosition - property bool animating -+ property bool _updatingFromFlickable: false -+ property bool _updatingFromUser: false - - onHoveredChanged: { - if (hovered) -@@ -19,9 +21,6 @@ ScrollBar { - shouldBeActive = flickable.moving; - } - -- property bool _updatingFromFlickable: false -- property bool _updatingFromUser: false -- - // Sync nonAnimPosition with Qt's automatic position binding - onPositionChanged: { - if (_updatingFromUser) { -@@ -37,24 +36,6 @@ ScrollBar { - } - } - -- // Sync nonAnimPosition with flickable when not animating -- Connections { -- target: flickable -- function onContentYChanged() { -- if (!animating && !fullMouse.pressed) { -- _updatingFromFlickable = true; -- const contentHeight = flickable.contentHeight; -- const height = flickable.height; -- if (contentHeight > height) { -- nonAnimPosition = Math.max(0, Math.min(1, flickable.contentY / (contentHeight - height))); -- } else { -- nonAnimPosition = 0; -- } -- _updatingFromFlickable = false; -- } -- } -- } -- - Component.onCompleted: { - if (flickable) { - const contentHeight = flickable.contentHeight; -@@ -64,7 +45,7 @@ ScrollBar { - } - } - } -- implicitWidth: Appearance.padding.small -+ implicitWidth: Tokens.padding.small - - contentItem: StyledRect { - anchors.left: parent.left -@@ -80,7 +61,7 @@ ScrollBar { - return 0.6; - return 0; - } -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3secondary - - MouseArea { -@@ -97,15 +78,34 @@ ScrollBar { - } - } - -+ // Sync nonAnimPosition with flickable when not animating - Connections { -+ function onContentYChanged() { -+ if (!root.animating && !fullMouse.pressed) { -+ root._updatingFromFlickable = true; -+ const contentHeight = root.flickable.contentHeight; -+ const height = root.flickable.height; -+ if (contentHeight > height) { -+ root.nonAnimPosition = Math.max(0, Math.min(1, root.flickable.contentY / (contentHeight - height))); -+ } else { -+ root.nonAnimPosition = 0; -+ } -+ root._updatingFromFlickable = false; -+ } -+ } -+ - target: root.flickable -+ } - -+ Connections { - function onMovingChanged(): void { - if (root.flickable.moving) - root.shouldBeActive = true; - else - hideDelay.restart(); - } -+ -+ target: root.flickable - } - - Timer { -@@ -118,13 +118,14 @@ ScrollBar { - CustomMouseArea { - id: fullMouse - -- anchors.fill: parent -- preventStealing: true -- -- onPressed: event => { -+ function onWheel(event: WheelEvent): void { - root.animating = true; - root._updatingFromUser = true; -- const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); -+ let newPos = root.nonAnimPosition; -+ if (event.angleDelta.y > 0) -+ newPos = Math.max(0, root.nonAnimPosition - 0.1); -+ else if (event.angleDelta.y < 0) -+ newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); - root.nonAnimPosition = newPos; - // Update flickable position - // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] -@@ -140,7 +141,11 @@ ScrollBar { - } - } - -- onPositionChanged: event => { -+ anchors.fill: parent -+ preventStealing: true -+ -+ onPressed: event => { -+ root.animating = true; - root._updatingFromUser = true; - const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); - root.nonAnimPosition = newPos; -@@ -158,14 +163,9 @@ ScrollBar { - } - } - -- function onWheel(event: WheelEvent): void { -- root.animating = true; -+ onPositionChanged: event => { - root._updatingFromUser = true; -- let newPos = root.nonAnimPosition; -- if (event.angleDelta.y > 0) -- newPos = Math.max(0, root.nonAnimPosition - 0.1); -- else if (event.angleDelta.y < 0) -- newPos = Math.min(1 - root.size, root.nonAnimPosition + 0.1); -+ const newPos = Math.max(0, Math.min(1 - root.size, event.y / root.height - root.size / 2)); - root.nonAnimPosition = newPos; - // Update flickable position - // Map scrollbar position [0, 1-size] to contentY [0, maxContentY] -diff --git a/components/controls/StyledSlider.qml b/components/controls/StyledSlider.qml -index 0ef229df..f169691b 100644 ---- a/components/controls/StyledSlider.qml -+++ b/components/controls/StyledSlider.qml -@@ -1,8 +1,8 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Templates -+import Caelestia.Config -+import qs.components -+import qs.services - - Slider { - id: root -@@ -18,7 +18,7 @@ Slider { - implicitWidth: root.handle.x - root.implicitHeight / 6 - - color: Colours.palette.m3primary -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - topRightRadius: root.implicitHeight / 15 - bottomRightRadius: root.implicitHeight / 15 - } -@@ -33,7 +33,7 @@ Slider { - implicitWidth: parent.width - root.handle.x - root.handle.implicitWidth - root.implicitHeight / 6 - - color: Colours.palette.m3surfaceContainerHighest -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - topLeftRadius: root.implicitHeight / 15 - bottomLeftRadius: root.implicitHeight / 15 - } -@@ -46,7 +46,7 @@ Slider { - implicitHeight: root.implicitHeight - - color: Colours.palette.m3primary -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - MouseArea { - anchors.fill: parent -diff --git a/components/controls/StyledSwitch.qml b/components/controls/StyledSwitch.qml -index ce93cd50..2e31adbf 100644 ---- a/components/controls/StyledSwitch.qml -+++ b/components/controls/StyledSwitch.qml -@@ -1,9 +1,9 @@ --import ".." --import qs.services --import qs.config - import QtQuick --import QtQuick.Templates - import QtQuick.Shapes -+import QtQuick.Templates -+import Caelestia.Config -+import qs.components -+import qs.services - - Switch { - id: root -@@ -14,21 +14,21 @@ Switch { - implicitHeight: implicitIndicatorHeight - - indicator: StyledRect { -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: root.checked ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHighest, root.cLayer) - - implicitWidth: implicitHeight * 1.7 -- implicitHeight: Appearance.font.size.normal + Appearance.padding.smaller * 2 -+ implicitHeight: Tokens.font.size.normal + Tokens.padding.smaller * 2 - - StyledRect { - readonly property real nonAnimWidth: root.pressed ? implicitHeight * 1.3 : implicitHeight - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: root.checked ? Colours.palette.m3onPrimary : Colours.layer(Colours.palette.m3outline, root.cLayer + 1) - -- x: root.checked ? parent.implicitWidth - nonAnimWidth - Appearance.padding.small / 2 : Appearance.padding.small / 2 -+ x: root.checked ? parent.implicitWidth - nonAnimWidth - Tokens.padding.small / 2 : Tokens.padding.small / 2 - implicitWidth: nonAnimWidth -- implicitHeight: parent.implicitHeight - Appearance.padding.small -+ implicitHeight: parent.implicitHeight - Tokens.padding.small - anchors.verticalCenter: parent.verticalCenter - - StyledRect { -@@ -83,15 +83,15 @@ Switch { - - anchors.centerIn: parent - width: height -- height: parent.implicitHeight - Appearance.padding.small * 2 -+ height: parent.implicitHeight - Tokens.padding.small * 2 - preferredRendererType: Shape.CurveRenderer - asynchronous: true - - ShapePath { -- strokeWidth: Appearance.font.size.larger * 0.15 -+ strokeWidth: root.Tokens.font.size.larger * 0.15 - strokeColor: root.checked ? Colours.palette.m3primary : Colours.palette.m3surfaceContainerHighest - fillColor: "transparent" -- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap -+ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - - startX: icon.start1.x - startY: icon.start1.y -@@ -145,8 +145,7 @@ Switch { - } - - component PropAnim: PropertyAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard -+ duration: Tokens.anim.durations.normal -+ easing: Tokens.anim.standard - } - } -diff --git a/components/controls/StyledTextField.qml b/components/controls/StyledTextField.qml -index 60bcff25..4ab2f824 100644 ---- a/components/controls/StyledTextField.qml -+++ b/components/controls/StyledTextField.qml -@@ -1,18 +1,18 @@ - pragma ComponentBehavior: Bound - --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Controls -+import Caelestia.Config -+import qs.components -+import qs.services - - TextField { - id: root - - color: Colours.palette.m3onSurface - placeholderTextColor: Colours.palette.m3outline -- font.family: Appearance.font.family.sans -- font.pointSize: Appearance.font.size.smaller -+ font.family: Tokens.font.family.sans -+ font.pointSize: Tokens.font.size.smaller - renderType: echoMode === TextField.Password ? TextField.QtRendering : TextField.NativeRendering - cursorVisible: !readOnly - -@@ -25,11 +25,9 @@ TextField { - - implicitWidth: 2 - color: Colours.palette.m3primary -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - Connections { -- target: root -- - function onCursorPositionChanged(): void { - if (root.activeFocus && root.cursorVisible) { - cursor.opacity = 1; -@@ -37,6 +35,8 @@ TextField { - enableBlink.restart(); - } - } -+ -+ target: root - } - - Timer { -@@ -61,7 +61,7 @@ TextField { - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -diff --git a/components/controls/SwitchRow.qml b/components/controls/SwitchRow.qml -index 6dda3f0c..b909739f 100644 ---- a/components/controls/SwitchRow.qml -+++ b/components/controls/SwitchRow.qml -@@ -1,22 +1,20 @@ --import ".." --import qs.components --import qs.components.effects --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root - - required property string label - required property bool checked -- property bool enabled: true -- property var onToggled: function (checked) {} -+ -+ signal toggled(checked: bool) - - Layout.fillWidth: true -- implicitHeight: row.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -+ implicitHeight: row.implicitHeight + Tokens.padding.large * 2 -+ radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { -@@ -29,8 +27,8 @@ StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -@@ -40,9 +38,7 @@ StyledRect { - StyledSwitch { - checked: root.checked - enabled: root.enabled -- onToggled: { -- root.onToggled(checked); -- } -+ onToggled: root.toggled(checked) - } - } - } -diff --git a/components/controls/TextButton.qml b/components/controls/TextButton.qml -index ecf7eb13..94d76ff8 100644 ---- a/components/controls/TextButton.qml -+++ b/components/controls/TextButton.qml -@@ -1,7 +1,7 @@ --import ".." --import qs.services --import qs.config - import QtQuick -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root -@@ -15,8 +15,8 @@ StyledRect { - property alias text: label.text - property bool checked - property bool toggle -- property real horizontalPadding: Appearance.padding.normal -- property real verticalPadding: Appearance.padding.smaller -+ property real horizontalPadding: Tokens.padding.normal -+ property real verticalPadding: Tokens.padding.smaller - property alias font: label.font - property int type: TextButton.Filled - -@@ -47,7 +47,7 @@ StyledRect { - - onCheckedChanged: internalChecked = checked - -- radius: internalChecked ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) -+ radius: internalChecked ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: type === TextButton.Text ? "transparent" : internalChecked ? activeColour : inactiveColour - - implicitWidth: label.implicitWidth + horizontalPadding * 2 -@@ -57,8 +57,7 @@ StyledRect { - id: stateLayer - - color: root.internalChecked ? root.activeOnColour : root.inactiveOnColour -- -- function onClicked(): void { -+ onClicked: { - if (root.toggle) - root.internalChecked = !root.internalChecked; - root.clicked(); -diff --git a/components/controls/ToggleButton.qml b/components/controls/ToggleButton.qml -index 98c7564f..232426c1 100644 ---- a/components/controls/ToggleButton.qml -+++ b/components/controls/ToggleButton.qml -@@ -1,11 +1,11 @@ --import ".." -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls --import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - StyledRect { - id: root -@@ -14,50 +14,45 @@ StyledRect { - property string icon - property string label - property string accent: "Secondary" -- property real iconSize: Appearance.font.size.large -- property real horizontalPadding: Appearance.padding.large -- property real verticalPadding: Appearance.padding.normal -+ property real iconSize: Tokens.font.size.large -+ property real horizontalPadding: Tokens.padding.large -+ property real verticalPadding: Tokens.padding.normal - property string tooltip: "" -- - property bool hovered: false -+ - signal clicked - -- Component.onCompleted: { -- hovered = toggleStateLayer.containsMouse; -- } -+ Component.onCompleted: hovered = toggleStateLayer.containsMouse -+ -+ Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Tokens.padding.normal * 2 : toggled ? Tokens.padding.small * 2 : 0) -+ implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 -+ implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 -+ radius: toggled || toggleStateLayer.pressed ? Tokens.rounding.small : Math.min(width, height) / 2 * Math.min(1, Tokens.rounding.scale) -+ color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] - - Connections { -- target: toggleStateLayer - function onContainsMouseChanged() { - const newHovered = toggleStateLayer.containsMouse; -- if (hovered !== newHovered) { -- hovered = newHovered; -+ if (root.hovered !== newHovered) { -+ root.hovered = newHovered; - } - } -- } -- -- Layout.preferredWidth: implicitWidth + (toggleStateLayer.pressed ? Appearance.padding.normal * 2 : toggled ? Appearance.padding.small * 2 : 0) -- implicitWidth: toggleBtnInner.implicitWidth + horizontalPadding * 2 -- implicitHeight: toggleBtnIcon.implicitHeight + verticalPadding * 2 - -- radius: toggled || toggleStateLayer.pressed ? Appearance.rounding.small : Math.min(width, height) / 2 * Math.min(1, Appearance.rounding.scale) -- color: toggled ? Colours.palette[`m3${accent.toLowerCase()}`] : Colours.palette[`m3${accent.toLowerCase()}Container`] -+ target: toggleStateLayer -+ } - - StateLayer { - id: toggleStateLayer - - color: root.toggled ? Colours.palette[`m3on${root.accent}`] : Colours.palette[`m3on${root.accent}Container`] -- -- function onClicked(): void { -- root.clicked(); -- } -+ onClicked: root.clicked() - } - - RowLayout { - id: toggleBtnInner - - anchors.centerIn: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - id: toggleBtnIcon -@@ -74,6 +69,7 @@ StyledRect { - } - - Loader { -+ asynchronous: true - active: !!root.label - visible: active - -@@ -86,21 +82,21 @@ StyledRect { - - Behavior on radius { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - Behavior on Layout.preferredWidth { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - // Tooltip - positioned absolutely, doesn't affect layout - Loader { - id: tooltipLoader -+ -+ asynchronous: true - active: root.tooltip !== "" - z: 10000 - width: 0 -diff --git a/components/controls/ToggleRow.qml b/components/controls/ToggleRow.qml -index 269d3d6a..beb6a15c 100644 ---- a/components/controls/ToggleRow.qml -+++ b/components/controls/ToggleRow.qml -@@ -1,9 +1,8 @@ --import qs.components --import qs.components.controls --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.components.controls - - RowLayout { - id: root -@@ -13,7 +12,7 @@ RowLayout { - property alias toggle: toggle - - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -diff --git a/components/controls/Tooltip.qml b/components/controls/Tooltip.qml -index b129a37b..6c62f43d 100644 ---- a/components/controls/Tooltip.qml -+++ b/components/controls/Tooltip.qml -@@ -1,10 +1,9 @@ --import ".." --import qs.components.effects --import qs.services --import qs.config - import QtQuick - import QtQuick.Controls --import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.components.effects -+import qs.services - - Popup { - id: root -@@ -24,55 +23,6 @@ Popup { - onTriggered: root.tooltipVisible = false - } - -- // Popup properties - doesn't affect layout -- parent: { -- let p = target; -- // Walk up to find the root Item (usually has anchors.fill: parent) -- while (p && p.parent) { -- const parentItem = p.parent; -- // Check if this looks like a root pane Item -- if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { -- return parentItem; -- } -- p = parentItem; -- } -- // Fallback -- return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; -- } -- -- visible: tooltipVisible -- modal: false -- closePolicy: Popup.NoAutoClose -- padding: 0 -- margins: 0 -- background: Item {} -- -- // Update position when target moves or tooltip becomes visible -- onTooltipVisibleChanged: { -- if (tooltipVisible) { -- Qt.callLater(updatePosition); -- } -- } -- Connections { -- target: root.target -- function onXChanged() { -- if (root.tooltipVisible) -- root.updatePosition(); -- } -- function onYChanged() { -- if (root.tooltipVisible) -- root.updatePosition(); -- } -- function onWidthChanged() { -- if (root.tooltipVisible) -- root.updatePosition(); -- } -- function onHeightChanged() { -- if (root.tooltipVisible) -- root.updatePosition(); -- } -- } -- - function updatePosition() { - if (!target || !parent) - return; -@@ -94,10 +44,10 @@ Popup { - let newX = targetCenterX - tooltipWidth / 2; - - // Position tooltip above target -- let newY = targetPos.y - tooltipHeight - Appearance.spacing.small; -+ let newY = targetPos.y - tooltipHeight - Tokens.spacing.small; - - // Keep within bounds -- const padding = Appearance.padding.normal; -+ const padding = Tokens.padding.normal; - if (newX < padding) { - newX = padding; - } else if (newX + tooltipWidth > (parent.width - padding)) { -@@ -110,13 +60,47 @@ Popup { - }); - } - -+ // Popup properties - doesn't affect layout -+ parent: { -+ let p = target; -+ // Walk up to find the root Item (usually has anchors.fill: parent) -+ while (p && p.parent) { -+ const parentItem = p.parent; -+ // Check if this looks like a root pane Item -+ if (parentItem && parentItem.anchors && parentItem.anchors.fill !== undefined) { -+ return parentItem; -+ } -+ p = parentItem; -+ } -+ // Fallback -+ return target.parent?.parent?.parent ?? target.parent?.parent ?? target.parent ?? target; -+ } -+ -+ visible: tooltipVisible -+ modal: false -+ closePolicy: Popup.NoAutoClose -+ padding: 0 -+ margins: 0 -+ background: Item {} -+ -+ // Update position when target moves or tooltip becomes visible -+ onTooltipVisibleChanged: { -+ if (tooltipVisible) { -+ Qt.callLater(updatePosition); -+ } -+ } -+ Component.onCompleted: { -+ if (tooltipVisible) { -+ updatePosition(); -+ } -+ } -+ - enter: Transition { - Anim { - property: "opacity" - from: 0 - to: 1 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - -@@ -125,37 +109,18 @@ Popup { - property: "opacity" - from: 1 - to: 0 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -- } -- } -- -- // Monitor hover state -- Connections { -- target: root.target -- function onHoveredChanged() { -- if (target.hovered) { -- showTimer.start(); -- if (timeout > 0) { -- hideTimer.stop(); -- hideTimer.start(); -- } -- } else { -- showTimer.stop(); -- hideTimer.stop(); -- tooltipVisible = false; -- } -+ type: Anim.FastSpatial - } - } - - contentItem: StyledRect { - id: tooltipRect - -- implicitWidth: tooltipText.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: tooltipText.implicitHeight + Appearance.padding.smaller * 2 -+ implicitWidth: tooltipText.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: tooltipText.implicitHeight + Tokens.padding.smaller * 2 - - color: Colours.palette.m3surfaceContainerHighest -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - antialiasing: true - - // Add elevation for depth -@@ -173,13 +138,47 @@ Popup { - - text: root.text - color: Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - -- Component.onCompleted: { -- if (tooltipVisible) { -- updatePosition(); -+ Connections { -+ function onXChanged() { -+ if (root.tooltipVisible) -+ root.updatePosition(); -+ } -+ function onYChanged() { -+ if (root.tooltipVisible) -+ root.updatePosition(); -+ } -+ function onWidthChanged() { -+ if (root.tooltipVisible) -+ root.updatePosition(); -+ } -+ function onHeightChanged() { -+ if (root.tooltipVisible) -+ root.updatePosition(); -+ } -+ -+ target: root.target -+ } -+ -+ // Monitor hover state -+ Connections { -+ function onHoveredChanged() { -+ if (target.hovered) { -+ showTimer.start(); -+ if (timeout > 0) { -+ hideTimer.stop(); -+ hideTimer.start(); -+ } -+ } else { -+ showTimer.stop(); -+ hideTimer.stop(); -+ tooltipVisible = false; -+ } - } -+ -+ target: root.target - } - } -diff --git a/components/effects/ColouredIcon.qml b/components/effects/ColouredIcon.qml -index 5ef4d4cc..570246bf 100644 ---- a/components/effects/ColouredIcon.qml -+++ b/components/effects/ColouredIcon.qml -@@ -1,8 +1,8 @@ - pragma ComponentBehavior: Bound - --import Caelestia --import Quickshell.Widgets - import QtQuick -+import Quickshell.Widgets -+import Caelestia - - IconImage { - id: root -diff --git a/components/effects/Colouriser.qml b/components/effects/Colouriser.qml -index 2948155d..b8abebb1 100644 ---- a/components/effects/Colouriser.qml -+++ b/components/effects/Colouriser.qml -@@ -1,6 +1,6 @@ --import ".." - import QtQuick - import QtQuick.Effects -+import qs.components - - MultiEffect { - property color sourceColor: "black" -diff --git a/components/effects/Elevation.qml b/components/effects/Elevation.qml -index fb29f16e..9e0962da 100644 ---- a/components/effects/Elevation.qml -+++ b/components/effects/Elevation.qml -@@ -1,7 +1,7 @@ --import ".." --import qs.services - import QtQuick - import QtQuick.Effects -+import qs.components -+import qs.services - - RectangularShadow { - property int level -diff --git a/components/effects/InnerBorder.qml b/components/effects/InnerBorder.qml -index d4a751f8..1b502e29 100644 ---- a/components/effects/InnerBorder.qml -+++ b/components/effects/InnerBorder.qml -@@ -1,10 +1,10 @@ - pragma ComponentBehavior: Bound - --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Effects -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - property alias innerRadius: maskInner.radius -@@ -37,8 +37,8 @@ StyledRect { - id: maskInner - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -- radius: Appearance.rounding.small -+ anchors.margins: Tokens.padding.normal -+ radius: Tokens.rounding.normal - } - } - } -diff --git a/components/effects/OpacityMask.qml b/components/effects/OpacityMask.qml -index 22e42496..8e625034 100644 ---- a/components/effects/OpacityMask.qml -+++ b/components/effects/OpacityMask.qml -@@ -1,9 +1,9 @@ --import Quickshell - import QtQuick -+import Quickshell - - ShaderEffect { - required property Item source - required property Item maskSource - -- fragmentShader: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/shaders/opacitymask.frag.qsb`) -+ fragmentShader: Quickshell.shellPath("assets/shaders/opacitymask.frag.qsb") - } -diff --git a/components/filedialog/CurrentItem.qml b/components/filedialog/CurrentItem.qml -index bb87133c..83cb4a92 100644 ---- a/components/filedialog/CurrentItem.qml -+++ b/components/filedialog/CurrentItem.qml -@@ -1,16 +1,16 @@ --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Shapes -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root - - required property var currentItem - -- implicitWidth: content.implicitWidth + Appearance.padding.larger + content.anchors.rightMargin -- implicitHeight: currentItem ? content.implicitHeight + Appearance.padding.normal + content.anchors.bottomMargin : 0 -+ implicitWidth: content.implicitWidth + Tokens.padding.larger + content.anchors.rightMargin -+ implicitHeight: currentItem ? content.implicitHeight + Tokens.padding.normal + content.anchors.bottomMargin : 0 - - Shape { - preferredRendererType: Shape.CurveRenderer -@@ -18,7 +18,7 @@ Item { - ShapePath { - id: path - -- readonly property real rounding: Appearance.rounding.small -+ readonly property real rounding: Tokens.rounding.small - readonly property bool flatten: root.implicitHeight < rounding * 2 - readonly property real roundingY: flatten ? root.implicitHeight / 2 : rounding - -@@ -76,16 +76,16 @@ Item { - - anchors.right: parent.right - anchors.bottom: parent.bottom -- anchors.rightMargin: Appearance.padding.larger - Appearance.padding.small -- anchors.bottomMargin: Appearance.padding.normal - Appearance.padding.small -+ anchors.rightMargin: Tokens.padding.larger - Tokens.padding.small -+ anchors.bottomMargin: Tokens.padding.normal - Tokens.padding.small - - Connections { -- target: root -- - function onCurrentItemChanged(): void { - if (root.currentItem) - content.text = qsTr(`"%1" selected`).arg(root.currentItem.modelData.name); - } -+ -+ target: root - } - } - } -diff --git a/components/filedialog/DialogButtons.qml b/components/filedialog/DialogButtons.qml -index bde9ac27..b46b4f72 100644 ---- a/components/filedialog/DialogButtons.qml -+++ b/components/filedialog/DialogButtons.qml -@@ -1,7 +1,7 @@ --import ".." --import qs.services --import qs.config - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root -@@ -9,7 +9,7 @@ StyledRect { - required property var dialog - required property FolderContents folder - -- implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 - - color: Colours.tPalette.m3surfaceContainer - -@@ -17,9 +17,9 @@ StyledRect { - id: inner - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Filter:") -@@ -28,14 +28,14 @@ StyledRect { - StyledRect { - Layout.fillWidth: true - Layout.fillHeight: true -- Layout.rightMargin: Appearance.spacing.normal -+ Layout.rightMargin: Tokens.spacing.normal - - color: Colours.tPalette.m3surfaceContainerHigh -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - - StyledText { - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - text: `${root.dialog.filterLabel} (${root.dialog.filters.map(f => `*.${f}`).join(", ")})` - } -@@ -43,24 +43,21 @@ StyledRect { - - StyledRect { - color: Colours.tPalette.m3surfaceContainerHigh -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - -- implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 -+ implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { - disabled: !root.dialog.selectionValid -- -- function onClicked(): void { -- root.dialog.accepted(root.folder.currentItem.modelData.path); -- } -+ onClicked: root.dialog.accepted(root.folder.currentItem.modelData.path) - } - - StyledText { - id: selectText - - anchors.centerIn: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - text: qsTr("Select") - color: root.dialog.selectionValid ? Colours.palette.m3onSurface : Colours.palette.m3outline -@@ -69,13 +66,13 @@ StyledRect { - - StyledRect { - color: Colours.tPalette.m3surfaceContainerHigh -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - -- implicitWidth: cancelText.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: cancelText.implicitHeight + Appearance.padding.normal * 2 -+ implicitWidth: cancelText.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: cancelText.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -+ onClicked: { - root.dialog.rejected(); - } - } -@@ -84,7 +81,7 @@ StyledRect { - id: cancelText - - anchors.centerIn: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - text: qsTr("Cancel") - } -diff --git a/components/filedialog/FileDialog.qml b/components/filedialog/FileDialog.qml -index f3187a55..90dd2bc2 100644 ---- a/components/filedialog/FileDialog.qml -+++ b/components/filedialog/FileDialog.qml -@@ -1,10 +1,10 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import Quickshell - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import qs.components -+import qs.services - - LazyLoader { - id: loader -diff --git a/components/filedialog/FolderContents.qml b/components/filedialog/FolderContents.qml -index e16c7a15..d8869b83 100644 ---- a/components/filedialog/FolderContents.qml -+++ b/components/filedialog/FolderContents.qml -@@ -1,22 +1,23 @@ - pragma ComponentBehavior: Bound - --import ".." --import "../controls" --import "../images" --import qs.services --import qs.config --import qs.utils --import Caelestia.Models --import Quickshell - import QtQuick --import QtQuick.Layouts - import QtQuick.Effects -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config -+import Caelestia.Models -+import qs.components -+import qs.components.controls -+import qs.components.filedialog -+import qs.components.images -+import qs.services -+import qs.utils - - Item { - id: root - - required property var dialog -- property alias currentItem: view.currentItem -+ readonly property FileEntry currentItem: view.currentItem as FileEntry - - StyledRect { - anchors.fill: parent -@@ -41,12 +42,13 @@ Item { - - Rectangle { - anchors.fill: parent -- anchors.margins: Appearance.padding.small -- radius: Appearance.rounding.small -+ anchors.margins: Tokens.padding.small -+ radius: Tokens.rounding.small - } - } - - Loader { -+ asynchronous: true - anchors.centerIn: parent - - opacity: view.count === 0 ? 1 : 0 -@@ -57,14 +59,14 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: "scan_delete" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.extraLarge * 2 -+ font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 500 - } - - StyledText { - text: qsTr("This folder is empty") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - } -@@ -78,10 +80,10 @@ Item { - id: view - - anchors.fill: parent -- anchors.margins: Appearance.padding.small + Appearance.padding.normal -+ anchors.margins: Tokens.padding.small + Tokens.padding.normal - -- cellWidth: Sizes.itemWidth + Appearance.spacing.small -- cellHeight: Sizes.itemWidth + Appearance.spacing.small * 2 + Appearance.padding.normal * 2 + 1 -+ cellWidth: Sizes.itemWidth + Tokens.spacing.small -+ cellHeight: Sizes.itemWidth + Tokens.spacing.small * 2 + Tokens.padding.normal * 2 + 1 - - clip: true - focus: true -@@ -90,11 +92,11 @@ Item { - - Keys.onReturnPressed: { - if (root.dialog.selectionValid) -- root.dialog.accepted(currentItem.modelData.path); -+ root.dialog.accepted((currentItem as FileEntry).modelData.path); - } - Keys.onEnterPressed: { - if (root.dialog.selectionValid) -- root.dialog.accepted(currentItem.modelData.path); -+ root.dialog.accepted((currentItem as FileEntry).modelData.path); - } - - StyledScrollBar.vertical: StyledScrollBar { -@@ -104,92 +106,21 @@ Item { - model: FileSystemModel { - path: { - if (root.dialog.cwd[0] === "Home") -- return `${Paths.home}/${root.dialog.cwd.slice(1).join("/")}`; -+ return Paths.home + `/${root.dialog.cwd.slice(1).join("/")}`; - else - return root.dialog.cwd.join("/"); - } - onPathChanged: view.currentIndex = -1 - } - -- delegate: StyledRect { -- id: item -- -- required property int index -- required property FileSystemEntry modelData -- -- readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Appearance.padding.normal * 2 -- -- implicitWidth: Sizes.itemWidth -- implicitHeight: nonAnimHeight -- -- radius: Appearance.rounding.normal -- color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) -- z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 -- clip: true -- -- StateLayer { -- onDoubleClicked: { -- if (item.modelData.isDir) -- root.dialog.cwd.push(item.modelData.name); -- else if (root.dialog.selectionValid) -- root.dialog.accepted(item.modelData.path); -- } -- -- function onClicked(): void { -- view.currentIndex = item.index; -- } -- } -- -- CachingIconImage { -- id: icon -- -- anchors.horizontalCenter: parent.horizontalCenter -- anchors.top: parent.top -- anchors.topMargin: Appearance.padding.normal -- -- implicitSize: Sizes.itemWidth - Appearance.padding.normal * 2 -- -- Component.onCompleted: { -- const file = item.modelData; -- if (file.isImage) -- source = Qt.resolvedUrl(file.path); -- else if (!file.isDir) -- source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); -- else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) -- source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); -- else -- source = Quickshell.iconPath("inode-directory"); -- } -- } -- -- StyledText { -- id: name -- -- anchors.left: parent.left -- anchors.right: parent.right -- anchors.top: icon.bottom -- anchors.topMargin: Appearance.spacing.small -- anchors.margins: Appearance.padding.normal -- -- horizontalAlignment: Text.AlignHCenter -- elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight -- wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap -- -- Component.onCompleted: text = item.modelData.name -- } -- -- Behavior on implicitHeight { -- Anim {} -- } -- } -+ delegate: FileEntry {} - - add: Transition { - Anim { - properties: "opacity,scale" - from: 0 - to: 1 -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - -@@ -208,12 +139,11 @@ Item { - Anim { - properties: "opacity,scale" - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - Anim { - properties: "x,y" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -@@ -221,8 +151,77 @@ Item { - CurrentItem { - anchors.right: parent.right - anchors.bottom: parent.bottom -- anchors.margins: Appearance.padding.small -+ anchors.margins: Tokens.padding.small - - currentItem: view.currentItem - } -+ -+ component FileEntry: StyledRect { -+ id: item -+ -+ required property int index -+ required property FileSystemEntry modelData -+ -+ readonly property real nonAnimHeight: icon.implicitHeight + name.anchors.topMargin + name.implicitHeight + Tokens.padding.normal * 2 -+ -+ implicitWidth: Sizes.itemWidth -+ implicitHeight: nonAnimHeight -+ -+ radius: Tokens.rounding.normal -+ color: Qt.alpha(Colours.tPalette.m3surfaceContainerHighest, GridView.isCurrentItem ? Colours.tPalette.m3surfaceContainerHighest.a : 0) -+ z: GridView.isCurrentItem || implicitHeight !== nonAnimHeight ? 1 : 0 -+ clip: true -+ -+ StateLayer { -+ onClicked: view.currentIndex = item.index -+ onDoubleClicked: { -+ if (item.modelData.isDir) -+ root.dialog.cwd.push(item.modelData.name); -+ else if (root.dialog.selectionValid) -+ root.dialog.accepted(item.modelData.path); -+ } -+ } -+ -+ CachingIconImage { -+ id: icon -+ -+ anchors.horizontalCenter: parent.horizontalCenter -+ anchors.top: parent.top -+ anchors.topMargin: Tokens.padding.normal -+ -+ implicitSize: Sizes.itemWidth - Tokens.padding.normal * 2 -+ -+ Component.onCompleted: { -+ const file = item.modelData; -+ if (file.isImage) -+ source = Qt.resolvedUrl(file.path); -+ else if (!file.isDir) -+ source = Quickshell.iconPath(file.mimeType.replace("/", "-"), "application-x-zerosize"); -+ else if (root.dialog.cwd.length === 1 && ["Desktop", "Documents", "Downloads", "Music", "Pictures", "Public", "Templates", "Videos"].includes(file.name)) -+ source = Quickshell.iconPath(`folder-${file.name.toLowerCase()}`); -+ else -+ source = Quickshell.iconPath("inode-directory"); -+ } -+ } -+ -+ StyledText { -+ id: name -+ -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.top: icon.bottom -+ anchors.topMargin: Tokens.spacing.small -+ anchors.margins: Tokens.padding.normal -+ -+ horizontalAlignment: Text.AlignHCenter -+ elide: item.GridView.isCurrentItem ? Text.ElideNone : Text.ElideRight -+ wrapMode: item.GridView.isCurrentItem ? Text.WrapAtWordBoundaryOrAnywhere : Text.NoWrap -+ -+ Component.onCompleted: text = item.modelData.name -+ } -+ -+ Behavior on implicitHeight { -+ Anim {} -+ } -+ } - } -diff --git a/components/filedialog/HeaderBar.qml b/components/filedialog/HeaderBar.qml -index c9a3feb5..c53d8f76 100644 ---- a/components/filedialog/HeaderBar.qml -+++ b/components/filedialog/HeaderBar.qml -@@ -1,18 +1,18 @@ - pragma ComponentBehavior: Bound - --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root - - required property var dialog - -- implicitWidth: inner.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 -+ implicitWidth: inner.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 - - color: Colours.tPalette.m3surfaceContainer - -@@ -20,20 +20,17 @@ StyledRect { - id: inner - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -- spacing: Appearance.spacing.small -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.small - - Item { - implicitWidth: implicitHeight -- implicitHeight: upIcon.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: upIcon.implicitHeight + Tokens.padding.small * 2 - - StateLayer { -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - disabled: root.dialog.cwd.length === 1 -- -- function onClicked(): void { -- root.dialog.cwd.pop(); -- } -+ onClicked: root.dialog.cwd.pop() - } - - MaterialIcon { -@@ -49,7 +46,7 @@ StyledRect { - StyledRect { - Layout.fillWidth: true - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainerHigh - - implicitHeight: pathComponents.implicitHeight + pathComponents.anchors.margins * 2 -@@ -58,10 +55,10 @@ StyledRect { - id: pathComponents - - anchors.fill: parent -- anchors.margins: Appearance.padding.small / 2 -+ anchors.margins: Tokens.padding.small / 2 - anchors.leftMargin: 0 - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Repeater { - model: root.dialog.cwd -@@ -75,7 +72,8 @@ StyledRect { - spacing: 0 - - Loader { -- Layout.rightMargin: Appearance.spacing.small -+ asynchronous: true -+ Layout.rightMargin: Tokens.spacing.small - active: folder.index > 0 - sourceComponent: StyledText { - text: "/" -@@ -85,27 +83,30 @@ StyledRect { - } - - Item { -- implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Appearance.padding.small : 0) + folderName.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: folderName.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: homeIcon.implicitWidth + (homeIcon.active ? Tokens.padding.small : 0) + folderName.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: folderName.implicitHeight + Tokens.padding.small * 2 - - Loader { -+ asynchronous: true - anchors.fill: parent - active: folder.index < root.dialog.cwd.length - 1 - sourceComponent: StateLayer { -- radius: Appearance.rounding.small -- -- function onClicked(): void { -+ onClicked: { - root.dialog.cwd = root.dialog.cwd.slice(0, folder.index + 1); - } -+ -+ radius: Tokens.rounding.small - } - } - - Loader { - id: homeIcon - -+ asynchronous: true -+ - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: Appearance.padding.normal -+ anchors.leftMargin: Tokens.padding.normal - - active: folder.index === 0 && folder.modelData === "Home" - sourceComponent: MaterialIcon { -@@ -120,7 +121,7 @@ StyledRect { - - anchors.left: homeIcon.right - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: homeIcon.active ? Appearance.padding.small : 0 -+ anchors.leftMargin: homeIcon.active ? Tokens.padding.small : 0 - - text: folder.modelData - color: folder.index < root.dialog.cwd.length - 1 ? Colours.palette.m3onSurfaceVariant : Colours.palette.m3onSurface -diff --git a/components/filedialog/Sidebar.qml b/components/filedialog/Sidebar.qml -index b55d7b37..e9c0918b 100644 ---- a/components/filedialog/Sidebar.qml -+++ b/components/filedialog/Sidebar.qml -@@ -1,10 +1,11 @@ - pragma ComponentBehavior: Bound - --import ".." --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.components.filedialog -+import qs.services - - StyledRect { - id: root -@@ -12,7 +13,7 @@ StyledRect { - required property var dialog - - implicitWidth: Sizes.sidebarWidth -- implicitHeight: inner.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: inner.implicitHeight + Tokens.padding.normal * 2 - - color: Colours.tPalette.m3surfaceContainer - -@@ -22,16 +23,16 @@ StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top -- anchors.margins: Appearance.padding.normal -- spacing: Appearance.spacing.small / 2 -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.small / 2 - - StyledText { - Layout.alignment: Qt.AlignHCenter -- Layout.topMargin: Appearance.padding.small / 2 -- Layout.bottomMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.padding.small / 2 -+ Layout.bottomMargin: Tokens.spacing.normal - text: qsTr("Files") - color: Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.bold: true - } - -@@ -45,15 +46,14 @@ StyledRect { - readonly property bool selected: modelData === root.dialog.cwd[root.dialog.cwd.length - 1] - - Layout.fillWidth: true -- implicitHeight: placeInner.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: placeInner.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3secondaryContainer, selected ? 1 : 0) - - StateLayer { - color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- -- function onClicked(): void { -+ onClicked: { - if (place.modelData === "Home") - root.dialog.cwd = ["Home"]; - else -@@ -65,11 +65,11 @@ StyledRect { - id: placeInner - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -- anchors.leftMargin: Appearance.padding.large -- anchors.rightMargin: Appearance.padding.large -+ anchors.margins: Tokens.padding.normal -+ anchors.leftMargin: Tokens.padding.large -+ anchors.rightMargin: Tokens.padding.large - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: { -@@ -91,7 +91,7 @@ StyledRect { - return "folder"; - } - color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: place.selected ? 1 : 0 - - Behavior on fill { -@@ -103,7 +103,7 @@ StyledRect { - Layout.fillWidth: true - text: place.modelData - color: place.selected ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - } -diff --git a/components/images/CachingIconImage.qml b/components/images/CachingIconImage.qml -index 1acc6a18..001e95de 100644 ---- a/components/images/CachingIconImage.qml -+++ b/components/images/CachingIconImage.qml -@@ -1,13 +1,14 @@ - pragma ComponentBehavior: Bound - --import qs.utils --import Quickshell.Widgets - import QtQuick -+import Quickshell.Widgets -+import qs.utils - - Item { - id: root - -- readonly property int status: loader.item?.status ?? Image.Null -+ // Easier (and more efficient) to ignore it than to check type and cast -+ readonly property int status: loader.item?.status ?? Image.Null // qmllint disable missing-property - readonly property real actualSize: Math.min(width, height) - property real implicitSize - property url source -@@ -18,6 +19,7 @@ Item { - Loader { - id: loader - -+ asynchronous: true - anchors.fill: parent - sourceComponent: root.source ? root.source.toString().startsWith("image://icon/") ? iconImage : cachingImage : null - } -diff --git a/components/images/CachingImage.qml b/components/images/CachingImage.qml -index e8f957a7..1e932dba 100644 ---- a/components/images/CachingImage.qml -+++ b/components/images/CachingImage.qml -@@ -1,28 +1,17 @@ --import qs.utils --import Caelestia.Internal --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia.Images - - Image { - id: root - -- property alias path: manager.path -+ property string path - - asynchronous: true - fillMode: Image.PreserveAspectCrop -- -- Connections { -- target: QsWindow.window -- -- function onDevicePixelRatioChanged(): void { -- manager.updateSource(); -- } -- } -- -- CachingImageManager { -- id: manager -- -- item: root -- cacheDir: Qt.resolvedUrl(Paths.imagecache) -+ source: IUtils.urlForPath(path, fillMode) -+ sourceSize: { -+ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; -+ return Qt.size(width * dpr, height * dpr); - } - } -diff --git a/components/misc/CustomShortcut.qml b/components/misc/CustomShortcut.qml -index aa35ed8f..9bbcf1f7 100644 ---- a/components/misc/CustomShortcut.qml -+++ b/components/misc/CustomShortcut.qml -@@ -1,5 +1,7 @@ - import Quickshell.Hyprland - -+// qmllint disable unresolved-type - GlobalShortcut { -+ // qmllint enable unresolved-type - appid: "caelestia" - } -diff --git a/components/widgets/ExtraIndicator.qml b/components/widgets/ExtraIndicator.qml -index db73ea08..d426eed8 100644 ---- a/components/widgets/ExtraIndicator.qml -+++ b/components/widgets/ExtraIndicator.qml -@@ -1,20 +1,20 @@ --import ".." - import "../effects" --import qs.services --import qs.config - import QtQuick -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - required property int extra - - anchors.right: parent.right -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - color: Colours.palette.m3tertiary -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - -- implicitWidth: count.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: count.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: count.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: count.implicitHeight + Tokens.padding.small * 2 - - opacity: extra > 0 ? 1 : 0 - scale: extra > 0 ? 1 : 0.5 -@@ -38,14 +38,13 @@ StyledRect { - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -+ duration: Tokens.anim.durations.expressiveFastSpatial - } - } - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -diff --git a/config/Appearance.qml b/config/Appearance.qml -deleted file mode 100644 -index 241c21a7..00000000 ---- a/config/Appearance.qml -+++ /dev/null -@@ -1,14 +0,0 @@ --pragma Singleton -- --import Quickshell -- --Singleton { -- // Literally just here to shorten accessing stuff :woe: -- // Also kinda so I can keep accessing it with `Appearance.xxx` instead of `Config.appearance.xxx` -- readonly property AppearanceConfig.Rounding rounding: Config.appearance.rounding -- readonly property AppearanceConfig.Spacing spacing: Config.appearance.spacing -- readonly property AppearanceConfig.Padding padding: Config.appearance.padding -- readonly property AppearanceConfig.FontStuff font: Config.appearance.font -- readonly property AppearanceConfig.Anim anim: Config.appearance.anim -- readonly property AppearanceConfig.Transparency transparency: Config.appearance.transparency --} -diff --git a/config/AppearanceConfig.qml b/config/AppearanceConfig.qml -deleted file mode 100644 -index b25945b1..00000000 ---- a/config/AppearanceConfig.qml -+++ /dev/null -@@ -1,92 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property Rounding rounding: Rounding {} -- property Spacing spacing: Spacing {} -- property Padding padding: Padding {} -- property FontStuff font: FontStuff {} -- property Anim anim: Anim {} -- property Transparency transparency: Transparency {} -- -- component Rounding: JsonObject { -- property real scale: 1 -- property int small: 12 * scale -- property int normal: 17 * scale -- property int large: 25 * scale -- property int full: 1000 * scale -- } -- -- component Spacing: JsonObject { -- property real scale: 1 -- property int small: 7 * scale -- property int smaller: 10 * scale -- property int normal: 12 * scale -- property int larger: 15 * scale -- property int large: 20 * scale -- } -- -- component Padding: JsonObject { -- property real scale: 1 -- property int small: 5 * scale -- property int smaller: 7 * scale -- property int normal: 10 * scale -- property int larger: 12 * scale -- property int large: 15 * scale -- } -- -- component FontFamily: JsonObject { -- property string sans: "Rubik" -- property string mono: "CaskaydiaCove NF" -- property string material: "Material Symbols Rounded" -- property string clock: "Rubik" -- } -- -- component FontSize: JsonObject { -- property real scale: 1 -- property int small: 11 * scale -- property int smaller: 12 * scale -- property int normal: 13 * scale -- property int larger: 15 * scale -- property int large: 18 * scale -- property int extraLarge: 28 * scale -- } -- -- component FontStuff: JsonObject { -- property FontFamily family: FontFamily {} -- property FontSize size: FontSize {} -- } -- -- component AnimCurves: JsonObject { -- property list emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] -- property list emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] -- property list emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] -- property list standard: [0.2, 0, 0, 1, 1, 1] -- property list standardAccel: [0.3, 0, 1, 1, 1, 1] -- property list standardDecel: [0, 0, 0, 1, 1, 1] -- property list expressiveFastSpatial: [0.42, 1.67, 0.21, 0.9, 1, 1] -- property list expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1, 1, 1] -- property list expressiveEffects: [0.34, 0.8, 0.34, 1, 1, 1] -- } -- -- component AnimDurations: JsonObject { -- property real scale: 1 -- property int small: 200 * scale -- property int normal: 400 * scale -- property int large: 600 * scale -- property int extraLarge: 1000 * scale -- property int expressiveFastSpatial: 350 * scale -- property int expressiveDefaultSpatial: 500 * scale -- property int expressiveEffects: 200 * scale -- } -- -- component Anim: JsonObject { -- property AnimCurves curves: AnimCurves {} -- property AnimDurations durations: AnimDurations {} -- } -- -- component Transparency: JsonObject { -- property bool enabled: false -- property real base: 0.85 -- property real layers: 0.4 -- } --} -diff --git a/config/BackgroundConfig.qml b/config/BackgroundConfig.qml -deleted file mode 100644 -index b8a8ad92..00000000 ---- a/config/BackgroundConfig.qml -+++ /dev/null -@@ -1,36 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool enabled: true -- property DesktopClock desktopClock: DesktopClock {} -- property Visualiser visualiser: Visualiser {} -- -- component DesktopClock: JsonObject { -- property bool enabled: false -- property real scale: 1.0 -- property string position: "bottom-right" -- property bool invertColors: false -- property DesktopClockBackground background: DesktopClockBackground {} -- property DesktopClockShadow shadow: DesktopClockShadow {} -- } -- -- component DesktopClockBackground: JsonObject { -- property bool enabled: false -- property real opacity: 0.7 -- property bool blur: true -- } -- -- component DesktopClockShadow: JsonObject { -- property bool enabled: true -- property real opacity: 0.7 -- property real blur: 0.4 -- } -- -- component Visualiser: JsonObject { -- property bool enabled: false -- property bool autoHide: true -- property bool blur: false -- property real rounding: 1 -- property real spacing: 1 -- } --} -diff --git a/config/BarConfig.qml b/config/BarConfig.qml -deleted file mode 100644 -index cf33fd21..00000000 ---- a/config/BarConfig.qml -+++ /dev/null -@@ -1,117 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool persistent: true -- property bool showOnHover: true -- property int dragThreshold: 20 -- property ScrollActions scrollActions: ScrollActions {} -- property Popouts popouts: Popouts {} -- property Workspaces workspaces: Workspaces {} -- property ActiveWindow activeWindow: ActiveWindow {} -- property Tray tray: Tray {} -- property Status status: Status {} -- property Clock clock: Clock {} -- property Sizes sizes: Sizes {} -- property list excludedScreens: [] -- -- property list entries: [ -- { -- id: "logo", -- enabled: true -- }, -- { -- id: "workspaces", -- enabled: true -- }, -- { -- id: "spacer", -- enabled: true -- }, -- { -- id: "activeWindow", -- enabled: true -- }, -- { -- id: "spacer", -- enabled: true -- }, -- { -- id: "tray", -- enabled: true -- }, -- { -- id: "clock", -- enabled: true -- }, -- { -- id: "statusIcons", -- enabled: true -- }, -- { -- id: "power", -- enabled: true -- } -- ] -- -- component ScrollActions: JsonObject { -- property bool workspaces: true -- property bool volume: true -- property bool brightness: true -- } -- -- component Popouts: JsonObject { -- property bool activeWindow: true -- property bool tray: true -- property bool statusIcons: true -- } -- -- component Workspaces: JsonObject { -- property int shown: 5 -- property bool activeIndicator: true -- property bool occupiedBg: false -- property bool showWindows: true -- property bool showWindowsOnSpecialWorkspaces: showWindows -- property bool activeTrail: false -- property bool perMonitorWorkspaces: true -- property string label: " " // if empty, will show workspace name's first letter -- property string occupiedLabel: "󰮯" -- property string activeLabel: "󰮯" -- property string capitalisation: "preserve" // upper, lower, or preserve - relevant only if label is empty -- property list specialWorkspaceIcons: [] -- } -- -- component ActiveWindow: JsonObject { -- property bool inverted: false -- } -- -- component Tray: JsonObject { -- property bool background: false -- property bool recolour: false -- property bool compact: false -- property list iconSubs: [] -- } -- -- component Status: JsonObject { -- property bool showAudio: false -- property bool showMicrophone: false -- property bool showKbLayout: false -- property bool showNetwork: true -- property bool showWifi: true -- property bool showBluetooth: true -- property bool showBattery: true -- property bool showLockStatus: true -- } -- -- component Clock: JsonObject { -- property bool showIcon: true -- } -- -- component Sizes: JsonObject { -- property int innerWidth: 40 -- property int windowPreviewSize: 400 -- property int trayMenuWidth: 300 -- property int batteryWidth: 250 -- property int networkWidth: 320 -- property int kbLayoutWidth: 320 -- } --} -diff --git a/config/BorderConfig.qml b/config/BorderConfig.qml -deleted file mode 100644 -index b15811fd..00000000 ---- a/config/BorderConfig.qml -+++ /dev/null -@@ -1,6 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property int thickness: Appearance.padding.normal -- property int rounding: Appearance.rounding.large --} -diff --git a/config/Config.qml b/config/Config.qml -deleted file mode 100644 -index 191c8556..00000000 ---- a/config/Config.qml -+++ /dev/null -@@ -1,507 +0,0 @@ --pragma Singleton -- --import qs.utils --import Caelestia --import Quickshell --import Quickshell.Io --import QtQuick -- --Singleton { -- id: root -- -- property alias appearance: adapter.appearance -- property alias general: adapter.general -- property alias background: adapter.background -- property alias bar: adapter.bar -- property alias border: adapter.border -- property alias dashboard: adapter.dashboard -- property alias controlCenter: adapter.controlCenter -- property alias launcher: adapter.launcher -- property alias notifs: adapter.notifs -- property alias osd: adapter.osd -- property alias session: adapter.session -- property alias winfo: adapter.winfo -- property alias lock: adapter.lock -- property alias utilities: adapter.utilities -- property alias sidebar: adapter.sidebar -- property alias services: adapter.services -- property alias paths: adapter.paths -- -- // Public save function - call this to persist config changes -- function save(): void { -- saveTimer.restart(); -- recentlySaved = true; -- recentSaveCooldown.restart(); -- } -- -- property bool recentlySaved: false -- -- ElapsedTimer { -- id: timer -- } -- -- Timer { -- id: saveTimer -- -- interval: 500 -- onTriggered: { -- timer.restart(); -- try { -- // Parse current config to preserve structure and comments if possible -- let config = {}; -- try { -- config = JSON.parse(fileView.text()); -- } catch (e) { -- // If parsing fails, start with empty object -- config = {}; -- } -- -- // Update config with current values -- config = serializeConfig(); -- -- // Save to file with pretty printing -- fileView.setText(JSON.stringify(config, null, 2)); -- } catch (e) { -- Toaster.toast(qsTr("Failed to serialize config"), e.message, "settings_alert", Toast.Error); -- } -- } -- } -- -- Timer { -- id: recentSaveCooldown -- -- interval: 2000 -- onTriggered: { -- recentlySaved = false; -- } -- } -- -- // Helper function to serialize the config object -- function serializeConfig(): var { -- return { -- appearance: serializeAppearance(), -- general: serializeGeneral(), -- background: serializeBackground(), -- bar: serializeBar(), -- border: serializeBorder(), -- dashboard: serializeDashboard(), -- controlCenter: serializeControlCenter(), -- launcher: serializeLauncher(), -- notifs: serializeNotifs(), -- osd: serializeOsd(), -- session: serializeSession(), -- winfo: serializeWinfo(), -- lock: serializeLock(), -- utilities: serializeUtilities(), -- sidebar: serializeSidebar(), -- services: serializeServices(), -- paths: serializePaths() -- }; -- } -- -- function serializeAppearance(): var { -- return { -- rounding: { -- scale: appearance.rounding.scale -- }, -- spacing: { -- scale: appearance.spacing.scale -- }, -- padding: { -- scale: appearance.padding.scale -- }, -- font: { -- family: { -- sans: appearance.font.family.sans, -- mono: appearance.font.family.mono, -- material: appearance.font.family.material, -- clock: appearance.font.family.clock -- }, -- size: { -- scale: appearance.font.size.scale -- } -- }, -- anim: { -- durations: { -- scale: appearance.anim.durations.scale -- } -- }, -- transparency: { -- enabled: appearance.transparency.enabled, -- base: appearance.transparency.base, -- layers: appearance.transparency.layers -- } -- }; -- } -- -- function serializeGeneral(): var { -- return { -- logo: general.logo, -- apps: { -- terminal: general.apps.terminal, -- audio: general.apps.audio, -- playback: general.apps.playback, -- explorer: general.apps.explorer -- }, -- idle: { -- lockBeforeSleep: general.idle.lockBeforeSleep, -- inhibitWhenAudio: general.idle.inhibitWhenAudio, -- timeouts: general.idle.timeouts -- }, -- battery: { -- warnLevels: general.battery.warnLevels, -- criticalLevel: general.battery.criticalLevel -- } -- }; -- } -- -- function serializeBackground(): var { -- return { -- enabled: background.enabled, -- desktopClock: { -- enabled: background.desktopClock.enabled, -- scale: background.desktopClock.scale, -- position: background.desktopClock.position, -- invertColors: background.desktopClock.invertColors, -- background: { -- enabled: background.desktopClock.background.enabled, -- opacity: background.desktopClock.background.opacity, -- blur: background.desktopClock.background.blur -- }, -- shadow: { -- enabled: background.desktopClock.shadow.enabled, -- opacity: background.desktopClock.shadow.opacity, -- blur: background.desktopClock.shadow.blur -- } -- }, -- visualiser: { -- enabled: background.visualiser.enabled, -- autoHide: background.visualiser.autoHide, -- blur: background.visualiser.blur, -- rounding: background.visualiser.rounding, -- spacing: background.visualiser.spacing -- } -- }; -- } -- -- function serializeBar(): var { -- return { -- persistent: bar.persistent, -- showOnHover: bar.showOnHover, -- dragThreshold: bar.dragThreshold, -- scrollActions: { -- workspaces: bar.scrollActions.workspaces, -- volume: bar.scrollActions.volume, -- brightness: bar.scrollActions.brightness -- }, -- popouts: { -- activeWindow: bar.popouts.activeWindow, -- tray: bar.popouts.tray, -- statusIcons: bar.popouts.statusIcons -- }, -- workspaces: { -- shown: bar.workspaces.shown, -- activeIndicator: bar.workspaces.activeIndicator, -- occupiedBg: bar.workspaces.occupiedBg, -- showWindows: bar.workspaces.showWindows, -- showWindowsOnSpecialWorkspaces: bar.workspaces.showWindowsOnSpecialWorkspaces, -- activeTrail: bar.workspaces.activeTrail, -- perMonitorWorkspaces: bar.workspaces.perMonitorWorkspaces, -- label: bar.workspaces.label, -- occupiedLabel: bar.workspaces.occupiedLabel, -- activeLabel: bar.workspaces.activeLabel, -- capitalisation: bar.workspaces.capitalisation, -- specialWorkspaceIcons: bar.workspaces.specialWorkspaceIcons -- }, -- tray: { -- background: bar.tray.background, -- recolour: bar.tray.recolour, -- compact: bar.tray.compact, -- iconSubs: bar.tray.iconSubs -- }, -- status: { -- showAudio: bar.status.showAudio, -- showMicrophone: bar.status.showMicrophone, -- showKbLayout: bar.status.showKbLayout, -- showNetwork: bar.status.showNetwork, -- showWifi: bar.status.showWifi, -- showBluetooth: bar.status.showBluetooth, -- showBattery: bar.status.showBattery, -- showLockStatus: bar.status.showLockStatus -- }, -- clock: { -- showIcon: bar.clock.showIcon -- }, -- sizes: { -- innerWidth: bar.sizes.innerWidth, -- windowPreviewSize: bar.sizes.windowPreviewSize, -- trayMenuWidth: bar.sizes.trayMenuWidth, -- batteryWidth: bar.sizes.batteryWidth, -- networkWidth: bar.sizes.networkWidth -- }, -- entries: bar.entries -- }; -- } -- -- function serializeBorder(): var { -- return { -- thickness: border.thickness, -- rounding: border.rounding -- }; -- } -- -- function serializeDashboard(): var { -- return { -- enabled: dashboard.enabled, -- showOnHover: dashboard.showOnHover, -- mediaUpdateInterval: dashboard.mediaUpdateInterval, -- dragThreshold: dashboard.dragThreshold, -- sizes: { -- tabIndicatorHeight: dashboard.sizes.tabIndicatorHeight, -- tabIndicatorSpacing: dashboard.sizes.tabIndicatorSpacing, -- infoWidth: dashboard.sizes.infoWidth, -- infoIconSize: dashboard.sizes.infoIconSize, -- dateTimeWidth: dashboard.sizes.dateTimeWidth, -- mediaWidth: dashboard.sizes.mediaWidth, -- mediaProgressSweep: dashboard.sizes.mediaProgressSweep, -- mediaProgressThickness: dashboard.sizes.mediaProgressThickness, -- resourceProgessThickness: dashboard.sizes.resourceProgessThickness, -- weatherWidth: dashboard.sizes.weatherWidth, -- mediaCoverArtSize: dashboard.sizes.mediaCoverArtSize, -- mediaVisualiserSize: dashboard.sizes.mediaVisualiserSize, -- resourceSize: dashboard.sizes.resourceSize -- } -- }; -- } -- -- function serializeControlCenter(): var { -- return { -- sizes: { -- heightMult: controlCenter.sizes.heightMult, -- ratio: controlCenter.sizes.ratio -- } -- }; -- } -- -- function serializeLauncher(): var { -- return { -- enabled: launcher.enabled, -- showOnHover: launcher.showOnHover, -- maxShown: launcher.maxShown, -- maxWallpapers: launcher.maxWallpapers, -- specialPrefix: launcher.specialPrefix, -- actionPrefix: launcher.actionPrefix, -- enableDangerousActions: launcher.enableDangerousActions, -- dragThreshold: launcher.dragThreshold, -- vimKeybinds: launcher.vimKeybinds, -- hiddenApps: launcher.hiddenApps, -- useFuzzy: { -- apps: launcher.useFuzzy.apps, -- actions: launcher.useFuzzy.actions, -- schemes: launcher.useFuzzy.schemes, -- variants: launcher.useFuzzy.variants, -- wallpapers: launcher.useFuzzy.wallpapers -- }, -- sizes: { -- itemWidth: launcher.sizes.itemWidth, -- itemHeight: launcher.sizes.itemHeight, -- wallpaperWidth: launcher.sizes.wallpaperWidth, -- wallpaperHeight: launcher.sizes.wallpaperHeight -- }, -- actions: launcher.actions -- }; -- } -- -- function serializeNotifs(): var { -- return { -- expire: notifs.expire, -- defaultExpireTimeout: notifs.defaultExpireTimeout, -- clearThreshold: notifs.clearThreshold, -- expandThreshold: notifs.expandThreshold, -- actionOnClick: notifs.actionOnClick, -- groupPreviewNum: notifs.groupPreviewNum, -- sizes: { -- width: notifs.sizes.width, -- image: notifs.sizes.image, -- badge: notifs.sizes.badge -- } -- }; -- } -- -- function serializeOsd(): var { -- return { -- enabled: osd.enabled, -- hideDelay: osd.hideDelay, -- enableBrightness: osd.enableBrightness, -- enableMicrophone: osd.enableMicrophone, -- sizes: { -- sliderWidth: osd.sizes.sliderWidth, -- sliderHeight: osd.sizes.sliderHeight -- } -- }; -- } -- -- function serializeSession(): var { -- return { -- enabled: session.enabled, -- dragThreshold: session.dragThreshold, -- vimKeybinds: session.vimKeybinds, -- commands: { -- logout: session.commands.logout, -- shutdown: session.commands.shutdown, -- hibernate: session.commands.hibernate, -- reboot: session.commands.reboot -- }, -- sizes: { -- button: session.sizes.button -- } -- }; -- } -- -- function serializeWinfo(): var { -- return { -- sizes: { -- heightMult: winfo.sizes.heightMult, -- detailsWidth: winfo.sizes.detailsWidth -- } -- }; -- } -- -- function serializeLock(): var { -- return { -- recolourLogo: lock.recolourLogo, -- enableFprint: lock.enableFprint, -- maxFprintTries: lock.maxFprintTries, -- sizes: { -- heightMult: lock.sizes.heightMult, -- ratio: lock.sizes.ratio, -- centerWidth: lock.sizes.centerWidth -- } -- }; -- } -- -- function serializeUtilities(): var { -- return { -- enabled: utilities.enabled, -- maxToasts: utilities.maxToasts, -- sizes: { -- width: utilities.sizes.width, -- toastWidth: utilities.sizes.toastWidth -- }, -- toasts: { -- configLoaded: utilities.toasts.configLoaded, -- chargingChanged: utilities.toasts.chargingChanged, -- gameModeChanged: utilities.toasts.gameModeChanged, -- dndChanged: utilities.toasts.dndChanged, -- audioOutputChanged: utilities.toasts.audioOutputChanged, -- audioInputChanged: utilities.toasts.audioInputChanged, -- capsLockChanged: utilities.toasts.capsLockChanged, -- numLockChanged: utilities.toasts.numLockChanged, -- kbLayoutChanged: utilities.toasts.kbLayoutChanged, -- vpnChanged: utilities.toasts.vpnChanged, -- nowPlaying: utilities.toasts.nowPlaying -- }, -- vpn: { -- enabled: utilities.vpn.enabled, -- provider: utilities.vpn.provider -- }, -- recording: { -- videoMode: utilities.recording.videoMode, -- recordSystem: utilities.recording.recordSystem, -- recordMicrophone: utilities.recording.recordMicrophone -- } -- }; -- } -- -- function serializeSidebar(): var { -- return { -- enabled: sidebar.enabled, -- dragThreshold: sidebar.dragThreshold, -- sizes: { -- width: sidebar.sizes.width -- } -- }; -- } -- -- function serializeServices(): var { -- return { -- weatherLocation: services.weatherLocation, -- useFahrenheit: services.useFahrenheit, -- useTwelveHourClock: services.useTwelveHourClock, -- gpuType: services.gpuType, -- visualiserBars: services.visualiserBars, -- audioIncrement: services.audioIncrement, -- brightnessIncrement: services.brightnessIncrement, -- maxVolume: services.maxVolume, -- smartScheme: services.smartScheme, -- defaultPlayer: services.defaultPlayer, -- playerAliases: services.playerAliases -- }; -- } -- -- function serializePaths(): var { -- return { -- wallpaperDir: paths.wallpaperDir, -- sessionGif: paths.sessionGif, -- mediaGif: paths.mediaGif -- }; -- } -- -- FileView { -- id: fileView -- -- path: `${Paths.config}/shell.json` -- watchChanges: true -- onFileChanged: { -- // Prevent reload loop - don't reload if we just saved -- if (!recentlySaved) { -- timer.restart(); -- reload(); -- } else { -- // Self-initiated save - reload without toast -- reload(); -- } -- } -- onLoaded: { -- try { -- JSON.parse(text()); -- const elapsed = timer.elapsedMs(); -- // Only show toast for external changes (not our own saves) and when elapsed time is meaningful -- if (adapter.utilities.toasts.configLoaded && !recentlySaved && elapsed > 0) { -- Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded in %1ms").arg(elapsed), "rule_settings"); -- } else if (adapter.utilities.toasts.configLoaded && recentlySaved && elapsed > 0) { -- Toaster.toast(qsTr("Config saved"), qsTr("Config reloaded in %1ms").arg(elapsed), "rule_settings"); -- } -- } catch (e) { -- Toaster.toast(qsTr("Failed to load config"), e.message, "settings_alert", Toast.Error); -- } -- } -- onLoadFailed: err => { -- if (err !== FileViewError.FileNotFound) -- Toaster.toast(qsTr("Failed to read config file"), FileViewError.toString(err), "settings_alert", Toast.Warning); -- } -- onSaveFailed: err => Toaster.toast(qsTr("Failed to save config"), FileViewError.toString(err), "settings_alert", Toast.Error) -- -- JsonAdapter { -- id: adapter -- -- property AppearanceConfig appearance: AppearanceConfig {} -- property GeneralConfig general: GeneralConfig {} -- property BackgroundConfig background: BackgroundConfig {} -- property BarConfig bar: BarConfig {} -- property BorderConfig border: BorderConfig {} -- property DashboardConfig dashboard: DashboardConfig {} -- property ControlCenterConfig controlCenter: ControlCenterConfig {} -- property LauncherConfig launcher: LauncherConfig {} -- property NotifsConfig notifs: NotifsConfig {} -- property OsdConfig osd: OsdConfig {} -- property SessionConfig session: SessionConfig {} -- property WInfoConfig winfo: WInfoConfig {} -- property LockConfig lock: LockConfig {} -- property UtilitiesConfig utilities: UtilitiesConfig {} -- property SidebarConfig sidebar: SidebarConfig {} -- property ServiceConfig services: ServiceConfig {} -- property UserPaths paths: UserPaths {} -- } -- } --} -diff --git a/config/ControlCenterConfig.qml b/config/ControlCenterConfig.qml -deleted file mode 100644 -index a5889491..00000000 ---- a/config/ControlCenterConfig.qml -+++ /dev/null -@@ -1,10 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property Sizes sizes: Sizes {} -- -- component Sizes: JsonObject { -- property real heightMult: 0.7 -- property real ratio: 16 / 9 -- } --} -diff --git a/config/DashboardConfig.qml b/config/DashboardConfig.qml -deleted file mode 100644 -index 030292b1..00000000 ---- a/config/DashboardConfig.qml -+++ /dev/null -@@ -1,25 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool enabled: true -- property bool showOnHover: true -- property int mediaUpdateInterval: 500 -- property int dragThreshold: 50 -- property Sizes sizes: Sizes {} -- -- component Sizes: JsonObject { -- readonly property int tabIndicatorHeight: 3 -- readonly property int tabIndicatorSpacing: 5 -- readonly property int infoWidth: 200 -- readonly property int infoIconSize: 25 -- readonly property int dateTimeWidth: 110 -- readonly property int mediaWidth: 200 -- readonly property int mediaProgressSweep: 180 -- readonly property int mediaProgressThickness: 8 -- readonly property int resourceProgessThickness: 10 -- readonly property int weatherWidth: 250 -- readonly property int mediaCoverArtSize: 150 -- readonly property int mediaVisualiserSize: 80 -- readonly property int resourceSize: 200 -- } --} -diff --git a/config/GeneralConfig.qml b/config/GeneralConfig.qml -deleted file mode 100644 -index 52ef0de3..00000000 ---- a/config/GeneralConfig.qml -+++ /dev/null -@@ -1,60 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property string logo: "" -- property Apps apps: Apps {} -- property Idle idle: Idle {} -- property Battery battery: Battery {} -- -- component Apps: JsonObject { -- property list terminal: ["foot"] -- property list audio: ["pavucontrol"] -- property list playback: ["mpv"] -- property list explorer: ["thunar"] -- } -- -- component Idle: JsonObject { -- property bool lockBeforeSleep: true -- property bool inhibitWhenAudio: true -- property list timeouts: [ -- { -- timeout: 180, -- idleAction: "lock" -- }, -- { -- timeout: 300, -- idleAction: "dpms off", -- returnAction: "dpms on" -- }, -- { -- timeout: 600, -- idleAction: ["systemctl", "suspend-then-hibernate"] -- } -- ] -- } -- -- component Battery: JsonObject { -- property list warnLevels: [ -- { -- level: 20, -- title: qsTr("Low battery"), -- message: qsTr("You might want to plug in a charger"), -- icon: "battery_android_frame_2" -- }, -- { -- level: 10, -- title: qsTr("Did you see the previous message?"), -- message: qsTr("You should probably plug in a charger now"), -- icon: "battery_android_frame_1" -- }, -- { -- level: 5, -- title: qsTr("Critical battery level"), -- message: qsTr("PLUG THE CHARGER RIGHT NOW!!"), -- icon: "battery_android_alert", -- critical: true -- }, -- ] -- property int criticalLevel: 3 -- } --} -diff --git a/config/LauncherConfig.qml b/config/LauncherConfig.qml -deleted file mode 100644 -index 7f9c7881..00000000 ---- a/config/LauncherConfig.qml -+++ /dev/null -@@ -1,146 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool enabled: true -- property bool showOnHover: false -- property int maxShown: 7 -- property int maxWallpapers: 9 // Warning: even numbers look bad -- property string specialPrefix: "@" -- property string actionPrefix: ">" -- property bool enableDangerousActions: false // Allow actions that can cause losing data, like shutdown, reboot and logout -- property int dragThreshold: 50 -- property bool vimKeybinds: false -- property list hiddenApps: [] -- property UseFuzzy useFuzzy: UseFuzzy {} -- property Sizes sizes: Sizes {} -- -- component UseFuzzy: JsonObject { -- property bool apps: false -- property bool actions: false -- property bool schemes: false -- property bool variants: false -- property bool wallpapers: false -- } -- -- component Sizes: JsonObject { -- property int itemWidth: 600 -- property int itemHeight: 57 -- property int wallpaperWidth: 280 -- property int wallpaperHeight: 200 -- } -- -- property list actions: [ -- { -- name: "Calculator", -- icon: "calculate", -- description: "Do simple math equations (powered by Qalc)", -- command: ["autocomplete", "calc"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Scheme", -- icon: "palette", -- description: "Change the current colour scheme", -- command: ["autocomplete", "scheme"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Wallpaper", -- icon: "image", -- description: "Change the current wallpaper", -- command: ["autocomplete", "wallpaper"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Variant", -- icon: "colors", -- description: "Change the current scheme variant", -- command: ["autocomplete", "variant"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Transparency", -- icon: "opacity", -- description: "Change shell transparency", -- command: ["autocomplete", "transparency"], -- enabled: false, -- dangerous: false -- }, -- { -- name: "Random", -- icon: "casino", -- description: "Switch to a random wallpaper", -- command: ["caelestia", "wallpaper", "-r"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Light", -- icon: "light_mode", -- description: "Change the scheme to light mode", -- command: ["setMode", "light"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Dark", -- icon: "dark_mode", -- description: "Change the scheme to dark mode", -- command: ["setMode", "dark"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Shutdown", -- icon: "power_settings_new", -- description: "Shutdown the system", -- command: ["systemctl", "poweroff"], -- enabled: true, -- dangerous: true -- }, -- { -- name: "Reboot", -- icon: "cached", -- description: "Reboot the system", -- command: ["systemctl", "reboot"], -- enabled: true, -- dangerous: true -- }, -- { -- name: "Logout", -- icon: "exit_to_app", -- description: "Log out of the current session", -- command: ["loginctl", "terminate-user", ""], -- enabled: true, -- dangerous: true -- }, -- { -- name: "Lock", -- icon: "lock", -- description: "Lock the current session", -- command: ["loginctl", "lock-session"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Sleep", -- icon: "bedtime", -- description: "Suspend then hibernate", -- command: ["systemctl", "suspend-then-hibernate"], -- enabled: true, -- dangerous: false -- }, -- { -- name: "Settings", -- icon: "settings", -- description: "Configure the shell", -- command: ["caelestia", "shell", "controlCenter", "open"], -- enabled: true, -- dangerous: false -- } -- ] --} -diff --git a/config/LockConfig.qml b/config/LockConfig.qml -deleted file mode 100644 -index 2af4e2cd..00000000 ---- a/config/LockConfig.qml -+++ /dev/null -@@ -1,14 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool recolourLogo: false -- property bool enableFprint: true -- property int maxFprintTries: 3 -- property Sizes sizes: Sizes {} -- -- component Sizes: JsonObject { -- property real heightMult: 0.7 -- property real ratio: 16 / 9 -- property int centerWidth: 600 -- } --} -diff --git a/config/NotifsConfig.qml b/config/NotifsConfig.qml -deleted file mode 100644 -index fa2db494..00000000 ---- a/config/NotifsConfig.qml -+++ /dev/null -@@ -1,18 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool expire: true -- property int defaultExpireTimeout: 5000 -- property real clearThreshold: 0.3 -- property int expandThreshold: 20 -- property bool actionOnClick: false -- property int groupPreviewNum: 3 -- property bool openExpanded: false // Show the notifichation in expanded state when opening -- property Sizes sizes: Sizes {} -- -- component Sizes: JsonObject { -- property int width: 400 -- property int image: 41 -- property int badge: 20 -- } --} -diff --git a/config/OsdConfig.qml b/config/OsdConfig.qml -deleted file mode 100644 -index 543fc41e..00000000 ---- a/config/OsdConfig.qml -+++ /dev/null -@@ -1,14 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool enabled: true -- property int hideDelay: 2000 -- property bool enableBrightness: true -- property bool enableMicrophone: false -- property Sizes sizes: Sizes {} -- -- component Sizes: JsonObject { -- property int sliderWidth: 30 -- property int sliderHeight: 150 -- } --} -diff --git a/config/ServiceConfig.qml b/config/ServiceConfig.qml -deleted file mode 100644 -index d083b7a1..00000000 ---- a/config/ServiceConfig.qml -+++ /dev/null -@@ -1,21 +0,0 @@ --import Quickshell.Io --import QtQuick -- --JsonObject { -- property string weatherLocation: "" // A lat,long pair or empty for autodetection, e.g. "37.8267,-122.4233" -- property bool useFahrenheit: [Locale.ImperialUSSystem, Locale.ImperialSystem].includes(Qt.locale().measurementSystem) -- property bool useTwelveHourClock: Qt.locale().timeFormat(Locale.ShortFormat).toLowerCase().includes("a") -- property string gpuType: "" -- property int visualiserBars: 45 -- property real audioIncrement: 0.1 -- property real brightnessIncrement: 0.1 -- property real maxVolume: 1.0 -- property bool smartScheme: true -- property string defaultPlayer: "Spotify" -- property list playerAliases: [ -- { -- "from": "com.github.th_ch.youtube_music", -- "to": "YT Music" -- } -- ] --} -diff --git a/config/SessionConfig.qml b/config/SessionConfig.qml -deleted file mode 100644 -index f65ec6d8..00000000 ---- a/config/SessionConfig.qml -+++ /dev/null -@@ -1,21 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool enabled: true -- property int dragThreshold: 30 -- property bool vimKeybinds: false -- property Commands commands: Commands {} -- -- property Sizes sizes: Sizes {} -- -- component Commands: JsonObject { -- property list logout: ["loginctl", "terminate-user", ""] -- property list shutdown: ["systemctl", "poweroff"] -- property list hibernate: ["systemctl", "hibernate"] -- property list reboot: ["systemctl", "reboot"] -- } -- -- component Sizes: JsonObject { -- property int button: 80 -- } --} -diff --git a/config/SidebarConfig.qml b/config/SidebarConfig.qml -deleted file mode 100644 -index a871562b..00000000 ---- a/config/SidebarConfig.qml -+++ /dev/null -@@ -1,11 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool enabled: true -- property int dragThreshold: 80 -- property Sizes sizes: Sizes {} -- -- component Sizes: JsonObject { -- property int width: 430 -- } --} -diff --git a/config/UserPaths.qml b/config/UserPaths.qml -deleted file mode 100644 -index f8de2678..00000000 ---- a/config/UserPaths.qml -+++ /dev/null -@@ -1,8 +0,0 @@ --import qs.utils --import Quickshell.Io -- --JsonObject { -- property string wallpaperDir: `${Paths.pictures}/Wallpapers` -- property string sessionGif: "root:/assets/kurukuru.gif" -- property string mediaGif: "root:/assets/bongocat.gif" --} -diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml -deleted file mode 100644 -index 6b845215..00000000 ---- a/config/UtilitiesConfig.qml -+++ /dev/null -@@ -1,42 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property bool enabled: true -- property int maxToasts: 4 -- -- property Sizes sizes: Sizes {} -- property Toasts toasts: Toasts {} -- property Vpn vpn: Vpn {} -- property Recording recording: Recording {} -- -- component Recording: JsonObject { -- property string videoMode: "fullscreen" -- property bool recordSystem: false -- property bool recordMicrophone: false -- } -- -- component Sizes: JsonObject { -- property int width: 430 -- property int toastWidth: 430 -- } -- -- component Toasts: JsonObject { -- property bool configLoaded: true -- property bool chargingChanged: true -- property bool gameModeChanged: true -- property bool dndChanged: true -- property bool audioOutputChanged: true -- property bool audioInputChanged: true -- property bool capsLockChanged: true -- property bool numLockChanged: true -- property bool kbLayoutChanged: true -- property bool kbLimit: true -- property bool vpnChanged: true -- property bool nowPlaying: false -- } -- -- component Vpn: JsonObject { -- property bool enabled: false -- property list provider: ["netbird"] -- } --} -diff --git a/config/WInfoConfig.qml b/config/WInfoConfig.qml -deleted file mode 100644 -index 50257807..00000000 ---- a/config/WInfoConfig.qml -+++ /dev/null -@@ -1,10 +0,0 @@ --import Quickshell.Io -- --JsonObject { -- property Sizes sizes: Sizes {} -- -- component Sizes: JsonObject { -- property real heightMult: 0.7 -- property real detailsWidth: 500 -- } --} -diff --git a/extras/version.cpp b/extras/version.cpp -index e1a0cf30..d6343417 100644 ---- a/extras/version.cpp -+++ b/extras/version.cpp -@@ -10,7 +10,8 @@ int main(int argc, char* argv[]) { - std::cout << GIT_REVISION << std::endl; - std::cout << DISTRIBUTOR << std::endl; - } else if (arg == "-s" || arg == "--short") { -- std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION << ", distrubuted by: " << DISTRIBUTOR << std::endl; -+ std::cout << PROJECT_NAME << " " << VERSION << ", revision " << GIT_REVISION -+ << ", distributed by: " << DISTRIBUTOR << std::endl; - } else { - std::cout << "Usage: " << argv[0] << " [-t | --terse] [-s | --short]" << std::endl; - return arg != "-h" && arg != "--help"; -diff --git a/flake.lock b/flake.lock -index 5b7fbd21..0721599d 100644 ---- a/flake.lock -+++ b/flake.lock -@@ -8,11 +8,11 @@ - ] - }, - "locked": { -- "lastModified": 1769226332, -- "narHash": "sha256-JKD9M2+/J4e6nRtcY2XRfpLlOHaGXT4aUHyIG/20qlw=", -+ "lastModified": 1778644647, -+ "narHash": "sha256-iQIu4b5by8B3qKeigungSIgRoJg9HS1NiIZwqIbCGYk=", - "owner": "caelestia-dots", - "repo": "cli", -- "rev": "52a3a3c50ef55e3561057e8a74c85cf16f83039f", -+ "rev": "2ce6213698d1cfa15b4b067d35c3cda634f443dd", - "type": "github" - }, - "original": { -@@ -23,11 +23,11 @@ - }, - "nixpkgs": { - "locked": { -- "lastModified": 1769018530, -- "narHash": "sha256-MJ27Cy2NtBEV5tsK+YraYr2g851f3Fl1LpNHDzDX15c=", -+ "lastModified": 1778869304, -+ "narHash": "sha256-30sZNZoA1cqF5JNO9fVX+wgiQYjB7HJqqJ4ztCDeBZE=", - "owner": "nixos", - "repo": "nixpkgs", -- "rev": "88d3861acdd3d2f0e361767018218e51810df8a1", -+ "rev": "d233902339c02a9c334e7e593de68855ad26c4cb", - "type": "github" - }, - "original": { -@@ -44,11 +44,11 @@ - ] - }, - "locked": { -- "lastModified": 1768985439, -- "narHash": "sha256-qkU4r+l+UPz4dutMMRZSin64HuVZkEv9iFpu9yMWVY0=", -+ "lastModified": 1778488696, -+ "narHash": "sha256-QSWgYuZUCNUJ/cxmaq83WkcT7lHQDDfsPVgH+96kIl0=", - "ref": "refs/heads/master", -- "rev": "191085a8821b35680bba16ce5411fc9dbe912237", -- "revCount": 731, -+ "rev": "7d1c9a9c6721606b129829134d6f614f015621e2", -+ "revCount": 818, - "type": "git", - "url": "https://git.outfoxxed.me/outfoxxed/quickshell" - }, -diff --git a/flake.nix b/flake.nix -index 5c884115..0d7ebb8c 100644 ---- a/flake.nix -+++ b/flake.nix -@@ -36,7 +36,6 @@ - withX11 = false; - withI3 = false; - }; -- app2unit = pkgs.callPackage ./nix/app2unit.nix {inherit pkgs;}; - caelestia-cli = inputs.caelestia-cli.packages.${pkgs.stdenv.hostPlatform.system}.default; - }; - with-cli = caelestia-shell.override {withCli = true;}; -diff --git a/modules/BatteryMonitor.qml b/modules/BatteryMonitor.qml -index d24cff27..2d13b5a3 100644 ---- a/modules/BatteryMonitor.qml -+++ b/modules/BatteryMonitor.qml -@@ -1,33 +1,31 @@ --import qs.config --import Caelestia -+import QtQuick - import Quickshell - import Quickshell.Services.UPower --import QtQuick -+import Caelestia -+import Caelestia.Config - - Scope { - id: root - -- readonly property list warnLevels: [...Config.general.battery.warnLevels].sort((a, b) => b.level - a.level) -+ readonly property list warnLevels: [...GlobalConfig.general.battery.warnLevels].sort((a, b) => b.level - a.level) - - Connections { -- target: UPower -- - function onOnBatteryChanged(): void { - if (UPower.onBattery) { -- if (Config.utilities.toasts.chargingChanged) -+ if (GlobalConfig.utilities.toasts.chargingChanged) - Toaster.toast(qsTr("Charger unplugged"), qsTr("Battery is discharging"), "power_off"); - } else { -- if (Config.utilities.toasts.chargingChanged) -+ if (GlobalConfig.utilities.toasts.chargingChanged) - Toaster.toast(qsTr("Charger plugged in"), qsTr("Battery is charging"), "power"); - for (const level of root.warnLevels) - level.warned = false; - } - } -+ -+ target: UPower - } - - Connections { -- target: UPower.displayDevice -- - function onPercentageChanged(): void { - if (!UPower.onBattery) - return; -@@ -40,11 +38,13 @@ Scope { - } - } - -- if (!hibernateTimer.running && p <= Config.general.battery.criticalLevel) { -+ if (!hibernateTimer.running && p <= GlobalConfig.general.battery.criticalLevel) { - Toaster.toast(qsTr("Hibernating in 5 seconds"), qsTr("Hibernating to prevent data loss"), "battery_android_alert", Toast.Error); - hibernateTimer.start(); - } - } -+ -+ target: UPower.displayDevice - } - - Timer { -diff --git a/modules/ConfigToasts.qml b/modules/ConfigToasts.qml -new file mode 100644 -index 00000000..81463b8d ---- /dev/null -+++ b/modules/ConfigToasts.qml -@@ -0,0 +1,39 @@ -+import QtQuick -+import Quickshell -+import Caelestia -+import Caelestia.Config -+ -+Scope { -+ Connections { -+ function onLoaded(): void { -+ if (GlobalConfig.utilities.toasts.configLoaded) -+ Toaster.toast(qsTr("Config loaded"), qsTr("Config loaded successfully!"), "rule_settings"); -+ } -+ -+ function onLoadFailed(error: string, screen: string): void { -+ Toaster.toast(qsTr("Failed to parse config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Warning); -+ } -+ -+ function onSaveFailed(error: string, screen: string): void { -+ Toaster.toast(qsTr("Failed to save config%1").arg(screen ? " for " + screen : ""), error, "settings_alert", Toast.Error); -+ } -+ -+ function onUnknownOption(key: string, screen: string): void { -+ Toaster.toast(qsTr("Unknown option in%1 config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); -+ } -+ -+ target: GlobalConfig -+ } -+ -+ Connections { -+ function onLoadFailed(error: string, screen: string): void { -+ Toaster.toast(qsTr("Failed to parse token config%1").arg(screen ? "for " + screen : ""), error, "settings_alert", Toast.Warning); -+ } -+ -+ function onUnknownOption(key: string, screen: string): void { -+ Toaster.toast(qsTr("Unknown option in%1 token config").arg(screen ? " " + screen : ""), key, "question_mark", Toast.Warning); -+ } -+ -+ target: TokenConfig -+ } -+} -diff --git a/modules/IdleMonitors.qml b/modules/IdleMonitors.qml -index b7ce0584..417b0e8a 100644 ---- a/modules/IdleMonitors.qml -+++ b/modules/IdleMonitors.qml -@@ -1,17 +1,17 @@ - pragma ComponentBehavior: Bound - - import "lock" --import qs.config --import qs.services --import Caelestia.Internal - import Quickshell - import Quickshell.Wayland -+import Caelestia.Config -+import Caelestia.Internal -+import qs.services - - Scope { - id: root - - required property Lock lock -- readonly property bool enabled: !Config.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) -+ readonly property bool enabled: !GlobalConfig.general.idle.inhibitWhenAudio || !Players.list.some(p => p.isPlaying) - - function handleIdleAction(action: var): void { - if (!action) -@@ -29,7 +29,7 @@ Scope { - - LogindManager { - onAboutToSleep: { -- if (Config.general.idle.lockBeforeSleep) -+ if (GlobalConfig.general.idle.lockBeforeSleep) - root.lock.lock.locked = true; - } - onLockRequested: root.lock.lock.locked = true -@@ -37,7 +37,7 @@ Scope { - } - - Variants { -- model: Config.general.idle.timeouts -+ model: GlobalConfig.general.idle.timeouts - - IdleMonitor { - required property var modelData -diff --git a/modules/MonitorIdentifier.qml b/modules/MonitorIdentifier.qml -index 096660a4..a618c6b5 100644 ---- a/modules/MonitorIdentifier.qml -+++ b/modules/MonitorIdentifier.qml -@@ -3,7 +3,7 @@ pragma ComponentBehavior: Bound - import qs.components - import qs.components.containers - import qs.services --import qs.config -+import Caelestia.Config - import Quickshell - import Quickshell.Wayland - import Quickshell.Hyprland -@@ -43,9 +43,9 @@ Variants { - StyledRect { - id: identifierRect - anchors.centerIn: parent -- implicitWidth: Appearance.padding.large * 14 -- implicitHeight: Appearance.padding.large * 14 -- radius: Appearance.rounding.large -+ implicitWidth: Tokens.padding.large * 14 -+ implicitHeight: Tokens.padding.large * 14 -+ radius: Tokens.rounding.large - color: Colours.tPalette.m3surfaceContainer - opacity: root.active ? 0.92 : 0 - -@@ -57,7 +57,7 @@ Variants { - - ColumnLayout { - anchors.centerIn: parent -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - Layout.alignment: Qt.AlignHCenter -@@ -70,7 +70,7 @@ Variants { - StyledText { - Layout.alignment: Qt.AlignHCenter - text: win.monitor?.name ?? "" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - -diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml -index a62b827e..9ee79676 100644 ---- a/modules/Shortcuts.qml -+++ b/modules/Shortcuts.qml -@@ -1,23 +1,28 @@ --import qs.components.misc --import qs.modules.controlcenter --import qs.services --import Caelestia -+import QtQuick - import Quickshell - import Quickshell.Io -+import Caelestia -+import qs.components.misc -+import qs.services -+import qs.modules.controlcenter - - Scope { - id: root - - property bool launcherInterrupted -- readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false -+ readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "controlCenter" - description: "Open control center" - onPressed: WindowFactory.create() - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "showall" - description: "Toggle launcher, dashboard and osd" - onPressed: { -@@ -28,7 +33,9 @@ Scope { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "dashboard" - description: "Toggle dashboard" - onPressed: { -@@ -39,7 +46,9 @@ Scope { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "session" - description: "Toggle session menu" - onPressed: { -@@ -50,7 +59,9 @@ Scope { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "launcher" - description: "Toggle launcher" - onPressed: root.launcherInterrupted = false -@@ -63,15 +74,41 @@ Scope { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "launcherInterrupt" - description: "Interrupt launcher keybind" - onPressed: root.launcherInterrupted = true - } - -- IpcHandler { -- target: "drawers" -+ // qmllint disable unresolved-type -+ CustomShortcut { -+ // qmllint enable unresolved-type -+ name: "sidebar" -+ description: "Toggle sidebar" -+ onPressed: { -+ if (root.hasFullscreen) -+ return; -+ const visibilities = Visibilities.getForActive(); -+ visibilities.sidebar = !visibilities.sidebar; -+ } -+ } - -+ // qmllint disable unresolved-type -+ CustomShortcut { -+ // qmllint enable unresolved-type -+ name: "utilities" -+ description: "Toggle utilities" -+ onPressed: { -+ if (root.hasFullscreen) -+ return; -+ const visibilities = Visibilities.getForActive(); -+ visibilities.utilities = !visibilities.utilities; -+ } -+ } -+ -+ IpcHandler { - function toggle(drawer: string): void { - if (list().split("\n").includes(drawer)) { - if (root.hasFullscreen && ["launcher", "session", "dashboard"].includes(drawer)) -@@ -79,7 +116,7 @@ Scope { - const visibilities = Visibilities.getForActive(); - visibilities[drawer] = !visibilities[drawer]; - } else { -- console.warn(`[IPC] Drawer "${drawer}" does not exist`); -+ console.warn(lc, `Drawer "${drawer}" does not exist`); - } - } - -@@ -87,19 +124,19 @@ Scope { - const visibilities = Visibilities.getForActive(); - return Object.keys(visibilities).filter(k => typeof visibilities[k] === "boolean").join("\n"); - } -+ -+ target: "drawers" - } - - IpcHandler { -- target: "controlCenter" -- - function open(): void { - WindowFactory.create(); - } -+ -+ target: "controlCenter" - } - - IpcHandler { -- target: "toaster" -- - function info(title: string, message: string, icon: string): void { - Toaster.toast(title, message, icon, Toast.Info); - } -@@ -115,5 +152,14 @@ Scope { - function error(title: string, message: string, icon: string): void { - Toaster.toast(title, message, icon, Toast.Error); - } -+ -+ target: "toaster" -+ } -+ -+ LoggingCategory { -+ id: lc -+ -+ name: "caelestia.qml.shortcuts" -+ defaultLogLevel: LoggingCategory.Info - } - } -diff --git a/modules/areapicker/AreaPicker.qml b/modules/areapicker/AreaPicker.qml -index 0d8b2fe1..76cc1039 100644 ---- a/modules/areapicker/AreaPicker.qml -+++ b/modules/areapicker/AreaPicker.qml -@@ -1,10 +1,11 @@ - pragma ComponentBehavior: Bound - --import qs.components.containers --import qs.components.misc - import Quickshell --import Quickshell.Wayland - import Quickshell.Io -+import Quickshell.Wayland -+import qs.components.containers -+import qs.components.misc -+import qs.services - - Scope { - LazyLoader { -@@ -15,7 +16,7 @@ Scope { - property bool clipboardOnly - - Variants { -- model: Quickshell.screens -+ model: Screens.screens - - StyledWindow { - id: win -@@ -47,8 +48,6 @@ Scope { - } - - IpcHandler { -- target: "picker" -- - function open(): void { - root.freeze = false; - root.closing = false; -@@ -76,9 +75,13 @@ Scope { - root.clipboardOnly = true; - root.activeAsync = true; - } -+ -+ target: "picker" - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "screenshot" - description: "Open screenshot tool" - onPressed: { -@@ -89,7 +92,9 @@ Scope { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "screenshotFreeze" - description: "Open screenshot tool (freeze mode)" - onPressed: { -@@ -100,7 +105,9 @@ Scope { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "screenshotClip" - description: "Open screenshot tool (clipboard)" - onPressed: { -@@ -111,7 +118,9 @@ Scope { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "screenshotFreezeClip" - description: "Open screenshot tool (freeze mode, clipboard)" - onPressed: { -diff --git a/modules/areapicker/Picker.qml b/modules/areapicker/Picker.qml -index 3be2bf30..75c45ab9 100644 ---- a/modules/areapicker/Picker.qml -+++ b/modules/areapicker/Picker.qml -@@ -1,13 +1,13 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import Caelestia --import Quickshell --import Quickshell.Wayland - import QtQuick - import QtQuick.Effects -+import Quickshell -+import Quickshell.Io -+import Quickshell.Wayland -+import Caelestia -+import qs.components -+import qs.services - - MouseArea { - id: root -@@ -80,9 +80,12 @@ MouseArea { - } else { - Quickshell.execDetached(["swappy", "-f", path]); - } -- closeAnim.start(); -+ closeAnim.start(); -+ }, () => { -+ console.error("Failed to save screenshot"); -+ closeAnim.start(); - }) --} -+} - - onClientsChanged: checkClientRects(mouseX, mouseY) - -@@ -166,7 +169,7 @@ onClientsChanged: checkClientRects(mouseX, mouseY) - target: root - property: "opacity" - to: 0 -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - ExAnim { - target: root -@@ -191,9 +194,21 @@ onClientsChanged: checkClientRects(mouseX, mouseY) - } - } - -+ Process { -+ running: true -+ command: ["hyprctl", "cursorpos", "-j"] -+ stdout: StdioCollector { -+ onStreamFinished: { -+ const pos = JSON.parse(text); -+ root.checkClientRects(pos.x - root.screen.x, pos.y - root.screen.y); -+ } -+ } -+ } -+ - Loader { - id: screencopy - -+ asynchronous: true - anchors.fill: parent - - active: root.loader.freeze -@@ -207,6 +222,13 @@ onClientsChanged: checkClientRects(mouseX, mouseY) - root.save(); - } - } -+ -+ Component.onCompleted: { -+ if (hasContent && !root.loader.freeze) { -+ overlay.visible = border.visible = true; -+ root.save(); -+ } -+ } - } - } - -@@ -265,7 +287,7 @@ onClientsChanged: checkClientRects(mouseX, mouseY) - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - } - -@@ -294,7 +316,6 @@ onClientsChanged: checkClientRects(mouseX, mouseY) - } - - component ExAnim: Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } -diff --git a/modules/background/Background.qml b/modules/background/Background.qml -index f8484e16..70f6914d 100644 ---- a/modules/background/Background.qml -+++ b/modules/background/Background.qml -@@ -1,146 +1,158 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Quickshell.Wayland -+import Caelestia.Config - import qs.components - import qs.components.containers - import qs.services --import qs.config --import Quickshell --import Quickshell.Wayland --import QtQuick - --Loader { -- active: Config.background.enabled -+Variants { -+ model: Screens.screens.filter(s => GlobalConfig.forScreen(s.name).background.enabled) - -- sourceComponent: Variants { -- model: Quickshell.screens -+ StyledWindow { -+ id: win - -- StyledWindow { -- id: win -+ required property ShellScreen modelData - -- required property ShellScreen modelData -+ screen: modelData -+ name: "background" -+ WlrLayershell.exclusionMode: ExclusionMode.Ignore -+ WlrLayershell.layer: contentItem.Config.background.wallpaperEnabled ? WlrLayer.Background : WlrLayer.Bottom -+ color: contentItem.Config.background.wallpaperEnabled ? "black" : "transparent" -+ surfaceFormat.opaque: false - -- screen: modelData -- name: "background" -- WlrLayershell.exclusionMode: ExclusionMode.Ignore -- WlrLayershell.layer: WlrLayer.Background -- color: "black" -+ anchors.top: true -+ anchors.bottom: true -+ anchors.left: true -+ anchors.right: true - -- anchors.top: true -- anchors.bottom: true -- anchors.left: true -- anchors.right: true -+ Item { -+ id: behindClock - -- Item { -- id: behindClock -+ anchors.fill: parent -+ -+ Loader { -+ id: wallpaper -+ -+ asynchronous: true - - anchors.fill: parent -+ active: Config.background.wallpaperEnabled - -- Wallpaper { -- id: wallpaper -- } -+ sourceComponent: Wallpaper {} -+ } - -- Visualiser { -- anchors.fill: parent -- screen: win.modelData -- wallpaper: wallpaper -- } -+ Visualiser { -+ anchors.fill: parent -+ screen: win.modelData -+ wallpaper: wallpaper - } -+ } - -- Loader { -- id: clockLoader -- active: Config.background.desktopClock.enabled -- -- anchors.margins: Appearance.padding.large * 2 -- anchors.leftMargin: Appearance.padding.large * 2 + Config.bar.sizes.innerWidth + Math.max(Appearance.padding.smaller, Config.border.thickness) -- -- state: Config.background.desktopClock.position -- states: [ -- State { -- name: "top-left" -- AnchorChanges { -- target: clockLoader -- anchors.top: parent.top -- anchors.left: parent.left -- } -- }, -- State { -- name: "top-center" -- AnchorChanges { -- target: clockLoader -- anchors.top: parent.top -- anchors.horizontalCenter: parent.horizontalCenter -- } -- }, -- State { -- name: "top-right" -- AnchorChanges { -- target: clockLoader -- anchors.top: parent.top -- anchors.right: parent.right -- } -- }, -- State { -- name: "middle-left" -- AnchorChanges { -- target: clockLoader -- anchors.verticalCenter: parent.verticalCenter -- anchors.left: parent.left -- } -- }, -- State { -- name: "middle-center" -- AnchorChanges { -- target: clockLoader -- anchors.verticalCenter: parent.verticalCenter -- anchors.horizontalCenter: parent.horizontalCenter -- } -- }, -- State { -- name: "middle-right" -- AnchorChanges { -- target: clockLoader -- anchors.verticalCenter: parent.verticalCenter -- anchors.right: parent.right -- } -- }, -- State { -- name: "bottom-left" -- AnchorChanges { -- target: clockLoader -- anchors.bottom: parent.bottom -- anchors.left: parent.left -- } -- }, -- State { -- name: "bottom-center" -- AnchorChanges { -- target: clockLoader -- anchors.bottom: parent.bottom -- anchors.horizontalCenter: parent.horizontalCenter -- } -- }, -- State { -- name: "bottom-right" -- AnchorChanges { -- target: clockLoader -- anchors.bottom: parent.bottom -- anchors.right: parent.right -- } -- } -- ] -+ Loader { -+ id: clockLoader -+ -+ asynchronous: true -+ active: Config.background.desktopClock.enabled -+ -+ anchors.margins: Tokens.padding.large * 2 -+ anchors.leftMargin: Tokens.padding.large * 2 + Tokens.sizes.bar.innerWidth + Math.max(Tokens.padding.smaller, Config.border.thickness) -+ -+ state: Config.background.desktopClock.position -+ states: [ -+ State { -+ name: "top-left" - -- transitions: Transition { -- AnchorAnimation { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ AnchorChanges { -+ target: clockLoader -+ anchors.top: parent.top -+ anchors.left: parent.left -+ } -+ }, -+ State { -+ name: "top-center" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.top: parent.top -+ anchors.horizontalCenter: parent.horizontalCenter -+ } -+ }, -+ State { -+ name: "top-right" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.top: parent.top -+ anchors.right: parent.right -+ } -+ }, -+ State { -+ name: "middle-left" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.left: parent.left -+ } -+ }, -+ State { -+ name: "middle-center" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.horizontalCenter: parent.horizontalCenter -+ } -+ }, -+ State { -+ name: "middle-right" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.right: parent.right -+ } -+ }, -+ State { -+ name: "bottom-left" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.bottom: parent.bottom -+ anchors.left: parent.left -+ } -+ }, -+ State { -+ name: "bottom-center" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.bottom: parent.bottom -+ anchors.horizontalCenter: parent.horizontalCenter -+ } -+ }, -+ State { -+ name: "bottom-right" -+ -+ AnchorChanges { -+ target: clockLoader -+ anchors.bottom: parent.bottom -+ anchors.right: parent.right - } - } -+ ] - -- sourceComponent: DesktopClock { -- wallpaper: behindClock -- absX: clockLoader.x -- absY: clockLoader.y -- } -+ transitions: Transition { -+ AnchorAnim {} -+ } -+ -+ sourceComponent: DesktopClock { -+ wallpaper: behindClock -+ absX: clockLoader.x -+ absY: clockLoader.y - } - } - } -diff --git a/modules/background/DesktopClock.qml b/modules/background/DesktopClock.qml -index 77fe447f..c7415be8 100644 ---- a/modules/background/DesktopClock.qml -+++ b/modules/background/DesktopClock.qml -@@ -1,11 +1,11 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config - import QtQuick --import QtQuick.Layouts - import QtQuick.Effects -+import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root -@@ -14,17 +14,17 @@ Item { - required property real absX - required property real absY - -- property real scale: Config.background.desktopClock.scale -+ property real clockScale: Config.background.desktopClock.scale - readonly property bool bgEnabled: Config.background.desktopClock.background.enabled -- readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur -+ readonly property bool blurEnabled: bgEnabled && Config.background.desktopClock.background.blur && !GameMode.enabled - readonly property bool invertColors: Config.background.desktopClock.invertColors - readonly property bool useLightSet: Colours.light ? !invertColors : invertColors - readonly property color safePrimary: useLightSet ? Colours.palette.m3primaryContainer : Colours.palette.m3primary - readonly property color safeSecondary: useLightSet ? Colours.palette.m3secondaryContainer : Colours.palette.m3secondary - readonly property color safeTertiary: useLightSet ? Colours.palette.m3tertiaryContainer : Colours.palette.m3tertiary - -- implicitWidth: layout.implicitWidth + (Appearance.padding.large * 4 * root.scale) -- implicitHeight: layout.implicitHeight + (Appearance.padding.large * 2 * root.scale) -+ implicitWidth: layout.implicitWidth + (Tokens.padding.large * 4 * root.clockScale) -+ implicitHeight: layout.implicitHeight + (Tokens.padding.large * 2 * root.clockScale) - - Item { - id: clockContainer -@@ -40,6 +40,7 @@ Item { - } - - Loader { -+ asynchronous: true - anchors.fill: parent - active: root.blurEnabled - -@@ -62,7 +63,7 @@ Item { - - visible: root.bgEnabled - anchors.fill: parent -- radius: Appearance.rounding.large * root.scale -+ radius: Tokens.rounding.large * root.clockScale - opacity: Config.background.desktopClock.background.opacity - color: Colours.palette.m3surface - -@@ -73,43 +74,44 @@ Item { - id: layout - - anchors.centerIn: parent -- spacing: Appearance.spacing.larger * root.scale -+ spacing: Tokens.spacing.larger * root.clockScale - - RowLayout { -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: Time.hourStr -- font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale -+ font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale - font.weight: Font.Bold - color: root.safePrimary - } - - StyledText { - text: ":" -- font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale -+ font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale - color: root.safeTertiary - opacity: 0.8 -- Layout.topMargin: -Appearance.padding.large * 1.5 * root.scale -+ Layout.topMargin: -Tokens.padding.large * 1.5 * root.clockScale - } - - StyledText { - text: Time.minuteStr -- font.pointSize: Appearance.font.size.extraLarge * 3 * root.scale -+ font.pointSize: Tokens.font.size.extraLarge * 3 * root.clockScale - font.weight: Font.Bold - color: root.safeSecondary - } - - Loader { -+ asynchronous: true - Layout.alignment: Qt.AlignTop -- Layout.topMargin: Appearance.padding.large * 1.4 * root.scale -+ Layout.topMargin: Tokens.padding.large * 1.4 * root.clockScale - -- active: Config.services.useTwelveHourClock -+ active: GlobalConfig.services.useTwelveHourClock - visible: active - - sourceComponent: StyledText { - text: Time.amPmStr -- font.pointSize: Appearance.font.size.large * root.scale -+ font.pointSize: Tokens.font.size.large * root.clockScale - color: root.safeSecondary - } - } -@@ -117,10 +119,10 @@ Item { - - StyledRect { - Layout.fillHeight: true -- Layout.preferredWidth: 4 * root.scale -- Layout.topMargin: Appearance.spacing.larger * root.scale -- Layout.bottomMargin: Appearance.spacing.larger * root.scale -- radius: Appearance.rounding.full -+ Layout.preferredWidth: 4 * root.clockScale -+ Layout.topMargin: Tokens.spacing.larger * root.clockScale -+ Layout.bottomMargin: Tokens.spacing.larger * root.clockScale -+ radius: Tokens.rounding.full - color: root.safePrimary - opacity: 0.8 - } -@@ -130,7 +132,7 @@ Item { - - StyledText { - text: Time.format("MMMM").toUpperCase() -- font.pointSize: Appearance.font.size.large * root.scale -+ font.pointSize: Tokens.font.size.large * root.clockScale - font.letterSpacing: 4 - font.weight: Font.Bold - color: root.safeSecondary -@@ -138,7 +140,7 @@ Item { - - StyledText { - text: Time.format("dd") -- font.pointSize: Appearance.font.size.extraLarge * root.scale -+ font.pointSize: Tokens.font.size.extraLarge * root.clockScale - font.letterSpacing: 2 - font.weight: Font.Medium - color: root.safePrimary -@@ -146,7 +148,7 @@ Item { - - StyledText { - text: Time.format("dddd") -- font.pointSize: Appearance.font.size.larger * root.scale -+ font.pointSize: Tokens.font.size.larger * root.clockScale - font.letterSpacing: 2 - color: root.safeSecondary - } -@@ -154,16 +156,15 @@ Item { - } - } - -- Behavior on scale { -+ Behavior on clockScale { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - Behavior on implicitWidth { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -diff --git a/modules/background/Visualiser.qml b/modules/background/Visualiser.qml -index c9bb9efb..45055879 100644 ---- a/modules/background/Visualiser.qml -+++ b/modules/background/Visualiser.qml -@@ -1,19 +1,19 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import Caelestia.Services --import Quickshell --import Quickshell.Widgets - import QtQuick - import QtQuick.Effects -+import Quickshell -+import Caelestia.Config -+import Caelestia.Internal -+import Caelestia.Services -+import qs.components -+import qs.services - - Item { - id: root - - required property ShellScreen screen -- required property Wallpaper wallpaper -+ required property Item wallpaper - - readonly property bool shouldBeActive: Config.background.visualiser.enabled && (!Config.background.visualiser.autoHide || (Hypr.monitorFor(screen)?.activeWorkspace?.toplevels?.values.every(t => t.lastIpcObject?.floating) ?? true)) - property real offset: shouldBeActive ? 0 : screen.height * 0.2 -@@ -21,6 +21,7 @@ Item { - opacity: shouldBeActive ? 1 : 0 - - Loader { -+ asynchronous: true - anchors.fill: parent - active: root.opacity > 0 && Config.background.visualiser.blur - -@@ -42,6 +43,7 @@ Item { - layer.enabled: true - - Loader { -+ asynchronous: true - anchors.fill: parent - anchors.topMargin: root.offset - anchors.bottomMargin: -root.offset -@@ -53,25 +55,29 @@ Item { - service: Audio.cava - } - -- Item { -- id: content -+ VisualiserBars { -+ id: bars - - anchors.fill: parent - anchors.margins: Config.border.thickness -- anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Appearance.spacing.small * Config.background.visualiser.spacing -+ anchors.leftMargin: Visibilities.bars.get(root.screen).exclusiveZone + Tokens.spacing.small * Config.background.visualiser.spacing - -- Side { -- content: content -- } -- Side { -- content: content -- isRight: true -- } -+ values: Audio.cava.values -+ primaryColor: Qt.alpha(Colours.palette.m3primary, 0.7) -+ secondaryColor: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) -+ rounding: Tokens.rounding.small * Config.background.visualiser.rounding -+ spacing: Tokens.spacing.small * Config.background.visualiser.spacing -+ animationDuration: Tokens.anim.durations.normal - - Behavior on anchors.leftMargin { - Anim {} - } - } -+ -+ FrameAnimation { -+ running: root.opacity > 0 && !bars.settled -+ onTriggered: bars.advance(frameTime) -+ } - } - } - } -@@ -83,69 +89,4 @@ Item { - Behavior on opacity { - Anim {} - } -- -- component Side: Repeater { -- id: side -- -- required property Item content -- property bool isRight -- -- model: Config.services.visualiserBars -- -- ClippingRectangle { -- id: bar -- -- required property int modelData -- property real value: Math.max(0, Math.min(1, Audio.cava.values[side.isRight ? modelData : side.count - modelData - 1])) -- -- clip: true -- -- x: modelData * ((side.content.width * 0.4) / Config.services.visualiserBars) + (side.isRight ? side.content.width * 0.6 : 0) -- implicitWidth: (side.content.width * 0.4) / Config.services.visualiserBars - Appearance.spacing.small * Config.background.visualiser.spacing -- -- y: side.content.height - height -- implicitHeight: bar.value * side.content.height * 0.4 -- -- color: "transparent" -- topLeftRadius: Appearance.rounding.small * Config.background.visualiser.rounding -- topRightRadius: Appearance.rounding.small * Config.background.visualiser.rounding -- -- Rectangle { -- topLeftRadius: parent.topLeftRadius -- topRightRadius: parent.topRightRadius -- -- gradient: Gradient { -- orientation: Gradient.Vertical -- -- GradientStop { -- position: 0 -- color: Qt.alpha(Colours.palette.m3primary, 0.7) -- -- Behavior on color { -- CAnim {} -- } -- } -- GradientStop { -- position: 1 -- color: Qt.alpha(Colours.palette.m3inversePrimary, 0.7) -- -- Behavior on color { -- CAnim {} -- } -- } -- } -- -- anchors.left: parent.left -- anchors.right: parent.right -- y: parent.height - height -- implicitHeight: side.content.height * 0.4 -- } -- -- Behavior on value { -- Anim { -- duration: Appearance.anim.durations.small -- } -- } -- } -- } - } -diff --git a/modules/background/Wallpaper.qml b/modules/background/Wallpaper.qml -index b5d7d4af..8cc2df9d 100644 ---- a/modules/background/Wallpaper.qml -+++ b/modules/background/Wallpaper.qml -@@ -1,20 +1,19 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Caelestia.Config - import qs.components --import qs.components.images - import qs.components.filedialog -+import qs.components.images - import qs.services --import qs.config - import qs.utils --import QtQuick - - Item { - id: root - - property string source: Wallpapers.current - property Image current: one -- -- anchors.fill: parent -+ property bool completed - - onSourceChanged: { - if (!source) -@@ -27,43 +26,47 @@ Item { - - Component.onCompleted: { - if (source) -- Qt.callLater(() => one.update()); -+ Qt.callLater(() => { -+ one.update(); -+ completed = true; -+ }); - } - - Loader { -+ asynchronous: true - anchors.fill: parent - -- active: !root.source -+ active: root.completed && !root.source - - sourceComponent: StyledRect { - color: Colours.palette.m3surfaceContainer - - Row { - anchors.centerIn: parent -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - MaterialIcon { - text: "sentiment_stressed" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.extraLarge * 5 -+ font.pointSize: Tokens.font.size.extraLarge * 5 - } - - Column { - anchors.verticalCenter: parent.verticalCenter -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Wallpaper missing?") - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.extraLarge * 2 -+ font.pointSize: Tokens.font.size.extraLarge * 2 - font.bold: true - } - - StyledRect { -- implicitWidth: selectWallText.implicitWidth + Appearance.padding.large * 2 -- implicitHeight: selectWallText.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: selectWallText.implicitWidth + Tokens.padding.large * 2 -+ implicitHeight: selectWallText.implicitHeight + Tokens.padding.small * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3primary - - FileDialog { -@@ -78,10 +81,7 @@ Item { - StateLayer { - radius: parent.radius - color: Colours.palette.m3onPrimary -- -- function onClicked(): void { -- dialog.open(); -- } -+ onClicked: dialog.open() - } - - StyledText { -@@ -91,7 +91,7 @@ Item { - - text: qsTr("Set it now!") - color: Colours.palette.m3onPrimary -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml -index cb384e39..a2ed060e 100644 ---- a/modules/bar/Bar.qml -+++ b/modules/bar/Bar.qml -@@ -1,30 +1,32 @@ - pragma ComponentBehavior: Bound - --import qs.services --import qs.config - import "popouts" as BarPopouts - import "components" - import "components/workspaces" --import Quickshell - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root - - required property ShellScreen screen -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - required property BarPopouts.Wrapper popouts -- readonly property int vPadding: Appearance.padding.large -+ required property bool fullscreen -+ readonly property int vPadding: Tokens.padding.large - - function closeTray(): void { - if (!Config.bar.tray.compact) - return; - - for (let i = 0; i < repeater.count; i++) { -- const item = repeater.itemAt(i); -- if (item?.enabled && item.id === "tray") { -- item.item.expanded = false; -+ const loader = repeater.itemAt(i) as WrappedLoader; -+ if (loader?.enabled && loader.id === "tray") { -+ (loader.item as Tray).expanded = false; - } - } - } -@@ -42,11 +44,9 @@ ColumnLayout { - - const id = ch.id; - const top = ch.y; -- const item = ch.item; -- const itemHeight = item.implicitHeight; - - if (id === "statusIcons" && Config.bar.popouts.statusIcons) { -- const items = item.items; -+ const items = (ch.item as StatusIcons).items; - const icon = items.childAt(items.width / 2, mapToItem(items, 0, y).y); - if (icon) { - popouts.currentName = icon.name; -@@ -54,9 +54,10 @@ ColumnLayout { - popouts.hasCurrent = true; - } - } else if (id === "tray" && Config.bar.popouts.tray) { -- if (!Config.bar.tray.compact || (item.expanded && !item.expandIcon.contains(mapToItem(item.expandIcon, item.implicitWidth / 2, y)))) { -- const index = Math.floor(((y - top - item.padding * 2 + item.spacing) / item.layout.implicitHeight) * item.items.count); -- const trayItem = item.items.itemAt(index); -+ const tray = ch.item as Tray; -+ if (!Config.bar.tray.compact || (tray.expanded && !tray.expandIcon.contains(mapToItem(tray.expandIcon, tray.implicitWidth / 2, y)))) { -+ const index = Math.floor(((y - top - tray.padding * 2 + tray.spacing) / tray.layout.implicitHeight) * tray.items.count); -+ const trayItem = tray.items.itemAt(index); - if (trayItem) { - popouts.currentName = `traymenu${index}`; - popouts.currentCenter = Qt.binding(() => trayItem.mapToItem(root, 0, trayItem.implicitHeight / 2).y); -@@ -66,11 +67,11 @@ ColumnLayout { - } - } else { - popouts.hasCurrent = false; -- item.expanded = true; -+ tray.expanded = true; - } -- } else if (id === "activeWindow" && Config.bar.popouts.activeWindow) { -+ } else if (id === "activeWindow" && Config.bar.popouts.activeWindow && Config.bar.activeWindow.showOnHover) { - popouts.currentName = id.toLowerCase(); -- popouts.currentCenter = item.mapToItem(root, 0, itemHeight / 2).y; -+ popouts.currentCenter = (ch.item as Item).mapToItem(root, 0, (ch.item as Item).implicitHeight / 2).y ?? 0; - popouts.hasCurrent = true; - } - } -@@ -79,11 +80,11 @@ ColumnLayout { - const ch = childAt(width / 2, y) as WrappedLoader; - if (ch?.id === "workspaces" && Config.bar.scrollActions.workspaces) { - // Workspace scroll -- const mon = (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); -+ const mon = (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor); - const specialWs = mon?.lastIpcObject.specialWorkspace.name; - if (specialWs?.length > 0) - Hypr.dispatch(`togglespecialworkspace ${specialWs.slice(8)}`); -- else if (angleDelta.y < 0 || (Config.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) -+ else if (angleDelta.y < 0 || (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? mon.activeWorkspace?.id : Hypr.activeWsId) > 1) - Hypr.dispatch(`workspace r${angleDelta.y > 0 ? "-" : "+"}1`); - } else if (y < screen.height / 2 && Config.bar.scrollActions.volume) { - // Volume scroll on top half -@@ -95,13 +96,13 @@ ColumnLayout { - // Brightness scroll on bottom half - const monitor = Brightness.getMonitorForScreen(screen); - if (angleDelta.y > 0) -- monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); -+ monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); - else if (angleDelta.y < 0) -- monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); -+ monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); - } - } - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Repeater { - id: repeater -@@ -128,12 +129,15 @@ ColumnLayout { - delegate: WrappedLoader { - sourceComponent: Workspaces { - screen: root.screen -+ fullscreen: root.fullscreen - } - } - } - DelegateChoice { - roleValue: "activeWindow" - delegate: WrappedLoader { -+ Layout.fillWidth: true -+ visible: !root.fullscreen - sourceComponent: ActiveWindow { - bar: root - monitor: Brightness.getMonitorForScreen(root.screen) -@@ -143,18 +147,21 @@ ColumnLayout { - DelegateChoice { - roleValue: "tray" - delegate: WrappedLoader { -+ visible: !root.fullscreen - sourceComponent: Tray {} - } - } - DelegateChoice { - roleValue: "clock" - delegate: WrappedLoader { -+ visible: !root.fullscreen - sourceComponent: Clock {} - } - } - DelegateChoice { - roleValue: "statusIcons" - delegate: WrappedLoader { -+ visible: !root.fullscreen - sourceComponent: StatusIcons {} - } - } -@@ -170,7 +177,7 @@ ColumnLayout { - } - - component WrappedLoader: Loader { -- required property bool enabled -+ required enabled - required property string id - required property int index - -@@ -193,6 +200,7 @@ ColumnLayout { - return null; - } - -+ asynchronous: true - Layout.alignment: Qt.AlignHCenter - - // Cursed ahh thing to add padding to first and last enabled components -diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml -index 29961b62..c57dcd64 100644 ---- a/modules/bar/BarWrapper.qml -+++ b/modules/bar/BarWrapper.qml -@@ -1,39 +1,44 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config --import "popouts" as BarPopouts --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.utils -+import qs.modules.bar.popouts as BarPopouts - - Item { - id: root - - required property ShellScreen screen -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - required property BarPopouts.Wrapper popouts -- required property bool disabled -+ required property bool fullscreen -+ -+ readonly property bool disabled: Strings.testRegexList(Config.bar.excludedScreens, screen.name) - -- readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) -- readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 -- readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness -- readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered) -+ readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) -+ readonly property int padding: Math.max(Tokens.padding.smaller, Config.border.thickness) -+ readonly property int contentWidth: Tokens.sizes.bar.innerWidth + padding * 2 -+ readonly property int exclusiveZone: shouldBeVisible ? contentWidth : Config.border.thickness -+ readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) - property bool isHovered - - function closeTray(): void { -- content.item?.closeTray(); -+ (content.item as Bar)?.closeTray(); - } - - function checkPopout(y: real): void { -- content.item?.checkPopout(y); -+ (content.item as Bar)?.checkPopout(y); - } - - function handleWheel(y: real, angleDelta: point): void { -- content.item?.handleWheel(y, angleDelta); -+ (content.item as Bar)?.handleWheel(y, angleDelta); - } - -- visible: width > Config.border.thickness -- implicitWidth: Config.border.thickness -+ clip: true -+ visible: width > 0 -+ implicitWidth: fullscreen ? 0 : Config.border.thickness - - states: State { - name: "visible" -@@ -52,8 +57,7 @@ Item { - Anim { - target: root - property: "implicitWidth" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - }, - Transition { -@@ -63,7 +67,7 @@ Item { - Anim { - target: root - property: "implicitWidth" -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - ] -@@ -81,7 +85,8 @@ Item { - width: root.contentWidth - screen: root.screen - visibilities: root.visibilities -- popouts: root.popouts -+ popouts: root.popouts // qmllint disable incompatible-type -+ fullscreen: root.fullscreen - } - } - } -diff --git a/modules/bar/components/ActiveWindow.qml b/modules/bar/components/ActiveWindow.qml -index 0c9b21e6..f39aa4c3 100644 ---- a/modules/bar/components/ActiveWindow.qml -+++ b/modules/bar/components/ActiveWindow.qml -@@ -1,10 +1,10 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services - import qs.utils --import qs.config --import QtQuick - - Item { - id: root -@@ -13,6 +13,19 @@ Item { - required property Brightness.Monitor monitor - property color colour: Colours.palette.m3primary - -+ readonly property string windowTitle: { -+ const title = Hypr.activeToplevel?.title; -+ if (!title) -+ return qsTr("Desktop"); -+ if (Config.bar.activeWindow.compact) { -+ // " - " (standard hyphen), " — " (em dash), " – " (en dash) -+ const parts = title.split(/\s+[\-\u2013\u2014]\s+/); -+ if (parts.length > 1) -+ return parts[parts.length - 1].trim(); -+ } -+ return title; -+ } -+ - readonly property int maxHeight: { - const otherModules = bar.children.filter(c => c.id && c.item !== this && c.id !== "spacer"); - const otherHeight = otherModules.reduce((acc, curr) => acc + (curr.item.nonAnimHeight ?? curr.height), 0); -@@ -25,6 +38,32 @@ Item { - implicitWidth: Math.max(icon.implicitWidth, current.implicitHeight) - implicitHeight: icon.implicitHeight + current.implicitWidth + current.anchors.topMargin - -+ Loader { -+ asynchronous: true -+ anchors.fill: parent -+ active: !Config.bar.activeWindow.showOnHover -+ -+ sourceComponent: MouseArea { -+ cursorShape: Qt.PointingHandCursor -+ hoverEnabled: true -+ onPositionChanged: { -+ const popouts = root.bar.popouts; -+ if (popouts.hasCurrent && popouts.currentName !== "activewindow") -+ popouts.hasCurrent = false; -+ } -+ onClicked: { -+ const popouts = root.bar.popouts; -+ if (popouts.hasCurrent) { -+ popouts.hasCurrent = false; -+ } else { -+ popouts.currentName = "activewindow"; -+ popouts.currentCenter = root.mapToItem(root.bar, 0, root.implicitHeight / 2).y; -+ popouts.hasCurrent = true; -+ } -+ } -+ } -+ } -+ - MaterialIcon { - id: icon - -@@ -46,9 +85,9 @@ Item { - TextMetrics { - id: metrics - -- text: Hypr.activeToplevel?.title ?? qsTr("Desktop") -- font.pointSize: Appearance.font.size.smaller -- font.family: Appearance.font.family.mono -+ text: root.windowTitle -+ font.pointSize: root.Tokens.font.size.smaller -+ font.family: root.Tokens.font.family.mono - elide: Qt.ElideRight - elideWidth: root.maxHeight - icon.height - -@@ -62,8 +101,7 @@ Item { - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - -@@ -72,7 +110,7 @@ Item { - - anchors.horizontalCenter: icon.horizontalCenter - anchors.top: icon.bottom -- anchors.topMargin: Appearance.spacing.small -+ anchors.topMargin: Tokens.spacing.small - - font.pointSize: metrics.font.pointSize - font.family: metrics.font.family -@@ -81,10 +119,10 @@ Item { - - transform: [ - Translate { -- x: Config.bar.activeWindow.inverted ? -implicitWidth + text.implicitHeight : 0 -+ x: root.Config.bar.activeWindow.inverted ? -text.implicitWidth + text.implicitHeight : 0 - }, - Rotation { -- angle: Config.bar.activeWindow.inverted ? 270 : 90 -+ angle: root.Config.bar.activeWindow.inverted ? 270 : 90 - origin.x: text.implicitHeight / 2 - origin.y: text.implicitHeight / 2 - } -diff --git a/modules/bar/components/Clock.qml b/modules/bar/components/Clock.qml -index 801e93d7..ffe599af 100644 ---- a/modules/bar/components/Clock.qml -+++ b/modules/bar/components/Clock.qml -@@ -1,38 +1,71 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import QtQuick - --Column { -+StyledRect { - id: root - -- property color colour: Colours.palette.m3tertiary -+ readonly property color colour: Colours.palette.m3tertiary -+ readonly property int padding: Config.bar.clock.background ? Tokens.padding.normal : Tokens.padding.small -+ -+ implicitWidth: Tokens.sizes.bar.innerWidth -+ implicitHeight: layout.implicitHeight + root.padding * 2 -+ -+ color: Qt.alpha(Colours.tPalette.m3surfaceContainer, Config.bar.clock.background ? Colours.tPalette.m3surfaceContainer.a : 0) -+ radius: Tokens.rounding.full - -- spacing: Appearance.spacing.small -+ Column { -+ id: layout - -- Loader { -- anchors.horizontalCenter: parent.horizontalCenter -+ anchors.centerIn: parent -+ spacing: Tokens.spacing.small -+ -+ Loader { -+ asynchronous: true -+ anchors.horizontalCenter: parent.horizontalCenter -+ -+ active: Config.bar.clock.showIcon -+ visible: active -+ -+ sourceComponent: MaterialIcon { -+ text: "calendar_month" -+ color: root.colour -+ } -+ } - -- active: Config.bar.clock.showIcon -- visible: active -+ StyledText { -+ anchors.horizontalCenter: parent.horizontalCenter - -- sourceComponent: MaterialIcon { -- text: "calendar_month" -+ visible: Config.bar.clock.showDate -+ -+ horizontalAlignment: StyledText.AlignHCenter -+ text: Time.format("ddd\nd") -+ font.pointSize: Tokens.font.size.smaller -+ font.family: Tokens.font.family.sans - color: root.colour - } -- } - -- StyledText { -- id: text -+ Rectangle { -+ anchors.horizontalCenter: parent.horizontalCenter -+ visible: Config.bar.clock.showDate -+ height: visible ? 1 : 0 - -- anchors.horizontalCenter: parent.horizontalCenter -+ width: parent.width * 0.8 -+ color: root.colour -+ opacity: 0.2 -+ } - -- horizontalAlignment: StyledText.AlignHCenter -- text: Time.format(Config.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") -- font.pointSize: Appearance.font.size.smaller -- font.family: Appearance.font.family.mono -- color: root.colour -+ StyledText { -+ anchors.horizontalCenter: parent.horizontalCenter -+ -+ horizontalAlignment: StyledText.AlignHCenter -+ text: Time.format(GlobalConfig.services.useTwelveHourClock ? "hh\nmm\nA" : "hh\nmm") -+ font.pointSize: Tokens.font.size.smaller -+ font.family: Tokens.font.family.mono -+ color: root.colour -+ } - } - } -diff --git a/modules/bar/components/OsIcon.qml b/modules/bar/components/OsIcon.qml -index 2bc38644..94d1a1c4 100644 ---- a/modules/bar/components/OsIcon.qml -+++ b/modules/bar/components/OsIcon.qml -@@ -1,12 +1,16 @@ -+import QtQuick -+import Caelestia.Config -+import qs.components - import qs.components.effects - import qs.services --import qs.config - import qs.utils --import QtQuick - - Item { - id: root - -+ implicitWidth: Math.round(Tokens.font.size.large * 1.2) -+ implicitHeight: Math.round(Tokens.font.size.large * 1.2) -+ - MouseArea { - anchors.fill: parent - cursorShape: Qt.PointingHandCursor -@@ -16,13 +20,28 @@ Item { - } - } - -- ColouredIcon { -+ Loader { -+ asynchronous: true - anchors.centerIn: parent -- source: SysInfo.osLogo -- implicitSize: Appearance.font.size.large * 1.2 -- colour: Colours.palette.m3tertiary -+ sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon -+ } -+ -+ Component { -+ id: caelestiaLogo -+ -+ Logo { -+ implicitWidth: Math.round(Tokens.font.size.large * 1.6) -+ implicitHeight: Math.round(Tokens.font.size.large * 1.6) -+ } - } - -- implicitWidth: Appearance.font.size.large * 1.2 -- implicitHeight: Appearance.font.size.large * 1.2 -+ Component { -+ id: distroIcon -+ -+ ColouredIcon { -+ source: SysInfo.osLogo -+ implicitSize: Math.round(Tokens.font.size.large * 1.2) -+ colour: Colours.palette.m3tertiary -+ } -+ } - } -diff --git a/modules/bar/components/Power.qml b/modules/bar/components/Power.qml -index 917bdf7f..681a805d 100644 ---- a/modules/bar/components/Power.qml -+++ b/modules/bar/components/Power.qml -@@ -1,15 +1,14 @@ -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell --import QtQuick - - Item { - id: root - -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - -- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 - implicitHeight: icon.implicitHeight - - StateLayer { -@@ -17,13 +16,9 @@ Item { - anchors.fill: undefined - anchors.centerIn: parent - implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 -- -- radius: Appearance.rounding.full -- -- function onClicked(): void { -- root.visibilities.session = !root.visibilities.session; -- } -+ implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 -+ radius: Tokens.rounding.full -+ onClicked: root.visibilities.session = !root.visibilities.session - } - - MaterialIcon { -@@ -35,6 +30,6 @@ Item { - text: "power_settings_new" - color: Colours.palette.m3error - font.bold: true -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - } -diff --git a/modules/bar/components/Settings.qml b/modules/bar/components/Settings.qml -deleted file mode 100644 -index 5d562cef..00000000 ---- a/modules/bar/components/Settings.qml -+++ /dev/null -@@ -1,41 +0,0 @@ --import qs.components --import qs.modules.controlcenter --import qs.services --import qs.config --import Quickshell --import QtQuick -- --Item { -- id: root -- -- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 -- implicitHeight: icon.implicitHeight -- -- StateLayer { -- // Cursed workaround to make the height larger than the parent -- anchors.fill: undefined -- anchors.centerIn: parent -- implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 -- -- radius: Appearance.rounding.full -- -- function onClicked(): void { -- WindowFactory.create(null, { -- active: "network" -- }); -- } -- } -- -- MaterialIcon { -- id: icon -- -- anchors.centerIn: parent -- anchors.horizontalCenterOffset: -1 -- -- text: "settings" -- color: Colours.palette.m3onSurface -- font.bold: true -- font.pointSize: Appearance.font.size.normal -- } --} -diff --git a/modules/bar/components/SettingsIcon.qml b/modules/bar/components/SettingsIcon.qml -deleted file mode 100644 -index 5d562cef..00000000 ---- a/modules/bar/components/SettingsIcon.qml -+++ /dev/null -@@ -1,41 +0,0 @@ --import qs.components --import qs.modules.controlcenter --import qs.services --import qs.config --import Quickshell --import QtQuick -- --Item { -- id: root -- -- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 -- implicitHeight: icon.implicitHeight -- -- StateLayer { -- // Cursed workaround to make the height larger than the parent -- anchors.fill: undefined -- anchors.centerIn: parent -- implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 -- -- radius: Appearance.rounding.full -- -- function onClicked(): void { -- WindowFactory.create(null, { -- active: "network" -- }); -- } -- } -- -- MaterialIcon { -- id: icon -- -- anchors.centerIn: parent -- anchors.horizontalCenterOffset: -1 -- -- text: "settings" -- color: Colours.palette.m3onSurface -- font.bold: true -- font.pointSize: Appearance.font.size.normal -- } --} -diff --git a/modules/bar/components/StatusIcons.qml b/modules/bar/components/StatusIcons.qml -index ca7dc2e3..900e5574 100644 ---- a/modules/bar/components/StatusIcons.qml -+++ b/modules/bar/components/StatusIcons.qml -@@ -1,14 +1,14 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.utils --import qs.config -+import QtQuick -+import QtQuick.Layouts - import Quickshell - import Quickshell.Bluetooth - import Quickshell.Services.UPower --import QtQuick --import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services -+import qs.utils - - StyledRect { - id: root -@@ -17,11 +17,11 @@ StyledRect { - readonly property alias items: iconColumn - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - clip: true -- implicitWidth: Config.bar.sizes.innerWidth -- implicitHeight: iconColumn.implicitHeight + Appearance.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) -+ implicitWidth: Tokens.sizes.bar.innerWidth -+ implicitHeight: iconColumn.implicitHeight + Tokens.padding.normal * 2 - (Config.bar.status.showLockStatus && !Hypr.capsLock && !Hypr.numLock ? iconColumn.spacing : 0) - - ColumnLayout { - id: iconColumn -@@ -29,9 +29,9 @@ StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom -- anchors.bottomMargin: Appearance.padding.normal -+ anchors.bottomMargin: Tokens.padding.normal - -- spacing: Appearance.spacing.smaller / 2 -+ spacing: Tokens.spacing.smaller / 2 - - // Lock keys status - WrappedLoader { -@@ -136,7 +136,7 @@ StyledRect { - animate: true - text: Hypr.kbLayout - color: root.colour -- font.family: Appearance.font.family.mono -+ font.family: Tokens.font.family.mono - } - } - -@@ -172,15 +172,15 @@ StyledRect { - active: Config.bar.status.showBluetooth - - sourceComponent: ColumnLayout { -- spacing: Appearance.spacing.smaller / 2 -+ spacing: Tokens.spacing.smaller / 2 - - // Bluetooth icon - MaterialIcon { - animate: true - text: { -- if (!Bluetooth.defaultAdapter?.enabled) -+ if (!Bluetooth.defaultAdapter?.enabled) // qmllint disable unresolved-type - return "bluetooth_disabled"; -- if (Bluetooth.devices.values.some(d => d.connected)) -+ if (Bluetooth.devices.values.some(d => d.connected)) // qmllint disable unresolved-type - return "bluetooth_connected"; - return "bluetooth"; - } -@@ -190,7 +190,7 @@ StyledRect { - // Connected bluetooth devices - Repeater { - model: ScriptModel { -- values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) -+ values: Bluetooth.devices.values.filter(d => d.state !== BluetoothDeviceState.Disconnected) // qmllint disable unresolved-type - } - - MaterialIcon { -@@ -204,21 +204,21 @@ StyledRect { - fill: 1 - - SequentialAnimation on opacity { -- running: device.modelData?.state !== BluetoothDeviceState.Connected -+ running: device.modelData?.state !== BluetoothDeviceState.Connected // qmllint disable unresolved-type - alwaysRunToEnd: true - loops: Animation.Infinite - - Anim { - from: 1 - to: 0 -- duration: Appearance.anim.durations.large -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ duration: Tokens.anim.durations.large -+ easing: Tokens.anim.standardAccel - } - Anim { - from: 0 - to: 1 -- duration: Appearance.anim.durations.large -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ duration: Tokens.anim.durations.large -+ easing: Tokens.anim.standardDecel - } - } - } -@@ -264,6 +264,7 @@ StyledRect { - component WrappedLoader: Loader { - required property string name - -+ asynchronous: true - Layout.alignment: Qt.AlignHCenter - visible: active - } -diff --git a/modules/bar/components/Tray.qml b/modules/bar/components/Tray.qml -index 96956f6f..85a920c1 100644 ---- a/modules/bar/components/Tray.qml -+++ b/modules/bar/components/Tray.qml -@@ -1,10 +1,11 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Quickshell.Services.SystemTray -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell.Services.SystemTray --import QtQuick - - StyledRect { - id: root -@@ -13,8 +14,8 @@ StyledRect { - readonly property alias items: items - readonly property alias expandIcon: expandIcon - -- readonly property int padding: Config.bar.tray.background ? Appearance.padding.normal : Appearance.padding.small -- readonly property int spacing: Config.bar.tray.background ? Appearance.spacing.small : 0 -+ readonly property int padding: Config.bar.tray.background ? Tokens.padding.normal : Tokens.padding.small -+ readonly property int spacing: Config.bar.tray.background ? Tokens.spacing.small : 0 - - property bool expanded - -@@ -27,11 +28,11 @@ StyledRect { - clip: true - visible: height > 0 - -- implicitWidth: Config.bar.sizes.innerWidth -+ implicitWidth: Tokens.sizes.bar.innerWidth - implicitHeight: nonAnimHeight - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (Config.bar.tray.background && items.count > 0) ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - Column { - id: layout -@@ -39,7 +40,7 @@ StyledRect { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.topMargin: root.padding -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - opacity: root.expanded || !Config.bar.tray.compact ? 1 : 0 - -@@ -48,7 +49,7 @@ StyledRect { - properties: "scale" - from: 0 - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - } - -@@ -56,7 +57,7 @@ StyledRect { - Anim { - properties: "scale" - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - Anim { - properties: "x,y" -@@ -66,7 +67,9 @@ StyledRect { - Repeater { - id: items - -- model: SystemTray.items -+ model: ScriptModel { -+ values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) -+ } - - TrayItem {} - } -@@ -79,23 +82,25 @@ StyledRect { - Loader { - id: expandIcon - -+ asynchronous: true -+ - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - -- active: Config.bar.tray.compact -+ active: Config.bar.tray.compact && items.count > 0 - - sourceComponent: Item { - implicitWidth: expandIconInner.implicitWidth -- implicitHeight: expandIconInner.implicitHeight - Appearance.padding.small * 2 -+ implicitHeight: expandIconInner.implicitHeight - Tokens.padding.small * 2 - - MaterialIcon { - id: expandIconInner - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom -- anchors.bottomMargin: Config.bar.tray.background ? Appearance.padding.small : -Appearance.padding.small -+ anchors.bottomMargin: Config.bar.tray.background ? Tokens.padding.small : -Tokens.padding.small - text: "expand_less" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - rotation: root.expanded ? 180 : 0 - - Behavior on rotation { -@@ -111,8 +116,7 @@ StyledRect { - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -diff --git a/modules/bar/components/TrayItem.qml b/modules/bar/components/TrayItem.qml -index 99119073..fefb532c 100644 ---- a/modules/bar/components/TrayItem.qml -+++ b/modules/bar/components/TrayItem.qml -@@ -1,11 +1,11 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell.Services.SystemTray -+import Caelestia.Config - import qs.components.effects - import qs.services --import qs.config - import qs.utils --import Quickshell.Services.SystemTray --import QtQuick - - MouseArea { - id: root -@@ -13,8 +13,8 @@ MouseArea { - required property SystemTrayItem modelData - - acceptedButtons: Qt.LeftButton | Qt.RightButton -- implicitWidth: Appearance.font.size.small * 2 -- implicitHeight: Appearance.font.size.small * 2 -+ implicitWidth: Tokens.font.size.small * 2 -+ implicitHeight: Tokens.font.size.small * 2 - - onClicked: event => { - if (event.button === Qt.LeftButton) -diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml -index dae54b37..ebc0caf2 100644 ---- a/modules/bar/components/workspaces/ActiveIndicator.qml -+++ b/modules/bar/components/workspaces/ActiveIndicator.qml -@@ -1,8 +1,10 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services --import qs.config --import QtQuick - - StyledRect { - id: root -@@ -10,6 +12,7 @@ StyledRect { - required property int activeWsId - required property Repeater workspaces - required property Item mask -+ required property bool fullscreen - - readonly property int currentWsIdx: { - let i = activeWsId - 1; -@@ -20,13 +23,12 @@ StyledRect { - - property real leading: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 - property real trailing: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.y ?? 0 : 0 -- property real currentSize: workspaces.count > 0 ? workspaces.itemAt(currentWsIdx)?.size ?? 0 : 0 -+ property real currentSize: workspaces.count > 0 ? (workspaces.itemAt(currentWsIdx) as Workspace)?.size ?? 0 : 0 - property real offset: Math.min(leading, trailing) - property real size: { - const s = Math.abs(leading - trailing) + currentSize; - if (Config.bar.workspaces.activeTrail && lastWs > currentWsIdx) { -- const ws = workspaces.itemAt(lastWs); -- // console.log(ws, lastWs); -+ const ws = workspaces.itemAt(lastWs) as Workspace; - return ws ? Math.min(ws.y + ws.size - offset, s) : 0; - } - return s; -@@ -42,9 +44,9 @@ StyledRect { - - clip: true - y: offset + mask.y -- implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 -+ implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 - implicitHeight: size -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3primary - - Colouriser { -@@ -61,38 +63,38 @@ StyledRect { - } - - Behavior on leading { -- enabled: Config.bar.workspaces.activeTrail -+ enabled: root.Config.bar.workspaces.activeTrail - - EAnim {} - } - - Behavior on trailing { -- enabled: Config.bar.workspaces.activeTrail -+ enabled: root.Config.bar.workspaces.activeTrail - - EAnim { -- duration: Appearance.anim.durations.normal * 2 -+ duration: Tokens.anim.durations.normal * 2 - } - } - - Behavior on currentSize { -- enabled: Config.bar.workspaces.activeTrail -+ enabled: root.Config.bar.workspaces.activeTrail - - EAnim {} - } - - Behavior on offset { -- enabled: !Config.bar.workspaces.activeTrail -+ enabled: !root.Config.bar.workspaces.activeTrail - - EAnim {} - } - - Behavior on size { -- enabled: !Config.bar.workspaces.activeTrail -+ enabled: !root.Config.bar.workspaces.activeTrail - - EAnim {} - } - - component EAnim: Anim { -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } -diff --git a/modules/bar/components/workspaces/OccupiedBg.qml b/modules/bar/components/workspaces/OccupiedBg.qml -index 56b215e6..2bd3c8cb 100644 ---- a/modules/bar/components/workspaces/OccupiedBg.qml -+++ b/modules/bar/components/workspaces/OccupiedBg.qml -@@ -1,10 +1,10 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell --import QtQuick - - Item { - id: root -@@ -52,8 +52,8 @@ Item { - - required property var modelData - -- readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null -- readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null -+ readonly property Workspace start: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.start)) ?? null : null // qmllint disable incompatible-type -+ readonly property Workspace end: root.workspaces.count > 0 ? root.workspaces.itemAt(getWsIdx(modelData.end)) ?? null : null // qmllint disable incompatible-type - - function getWsIdx(ws: int): int { - let i = ws - 1; -@@ -65,18 +65,18 @@ Item { - anchors.horizontalCenter: root.horizontalCenter - - y: (start?.y ?? 0) - 1 -- implicitWidth: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 + 2 -+ implicitWidth: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 + 2 - implicitHeight: start && end ? end.y + end.size - start.y + 2 : 0 - - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - scale: 0 - Component.onCompleted: scale = 1 - - Behavior on scale { - Anim { -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - } - -@@ -90,14 +90,14 @@ Item { - } - } - -- component Pill: QtObject { -- property int start -- property int end -- } -- - Component { - id: pillComp - - Pill {} - } -+ -+ component Pill: QtObject { -+ property int start -+ property int end -+ } - } -diff --git a/modules/bar/components/workspaces/SpecialWorkspaces.qml b/modules/bar/components/workspaces/SpecialWorkspaces.qml -index ad85af89..3ed382e9 100644 ---- a/modules/bar/components/workspaces/SpecialWorkspaces.qml -+++ b/modules/bar/components/workspaces/SpecialWorkspaces.qml -@@ -1,21 +1,21 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Hyprland -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services - import qs.utils --import qs.config --import Quickshell --import Quickshell.Hyprland --import QtQuick --import QtQuick.Layouts - - Item { - id: root - - required property ShellScreen screen - readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) -- readonly property string activeSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name ?? "" -+ readonly property string activeSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? monitor : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name ?? "" - - layer.enabled: true - layer.effect: OpacityMask { -@@ -31,7 +31,7 @@ Item { - - Rectangle { - anchors.fill: parent -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - gradient: Gradient { - orientation: Gradient.Vertical -@@ -60,7 +60,7 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - implicitHeight: parent.height / 2 - opacity: view.contentY > 0 ? 0 : 1 - -@@ -74,9 +74,9 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - implicitHeight: parent.height / 2 -- opacity: view.contentY < view.contentHeight - parent.height + Appearance.padding.small ? 0 : 1 -+ opacity: view.contentY < view.contentHeight - parent.height + Tokens.padding.small ? 0 : 1 - - Behavior on opacity { - Anim {} -@@ -88,14 +88,14 @@ Item { - id: view - - anchors.fill: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - interactive: false - - currentIndex: model.values.findIndex(w => w.name === root.activeSpecial) - onCurrentIndexChanged: currentIndex = Qt.binding(() => model.values.findIndex(w => w.name === root.activeSpecial)) - - model: ScriptModel { -- values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!Config.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) -+ values: Hypr.workspaces.values.filter(w => w.name.startsWith("special:") && (!GlobalConfig.bar.workspaces.perMonitorWorkspaces || w.monitor === root.monitor)) - } - - preferredHighlightBegin: 0 -@@ -105,150 +105,21 @@ Item { - highlightFollowsCurrentItem: false - highlight: Item { - y: view.currentItem?.y ?? 0 -- implicitHeight: view.currentItem?.size ?? 0 -+ implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 - - Behavior on y { - Anim {} - } - } - -- delegate: ColumnLayout { -- id: ws -- -- required property HyprlandWorkspace modelData -- readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Appearance.padding.small : 0) -- property int wsId -- property string icon -- property bool hasWindows -- -- anchors.left: view.contentItem.left -- anchors.right: view.contentItem.right -- -- spacing: 0 -- -- Component.onCompleted: { -- wsId = modelData.id; -- icon = Icons.getSpecialWsIcon(modelData.name); -- hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; -- } -- -- // Hacky thing cause modelData gets destroyed before the remove anim finishes -- Connections { -- target: ws.modelData -- -- function onIdChanged(): void { -- if (ws.modelData) -- ws.wsId = ws.modelData.id; -- } -- -- function onNameChanged(): void { -- if (ws.modelData) -- ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); -- } -- -- function onLastIpcObjectChanged(): void { -- if (ws.modelData) -- ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; -- } -- } -- -- Connections { -- target: Config.bar.workspaces -- -- function onShowWindowsOnSpecialWorkspacesChanged(): void { -- if (ws.modelData) -- ws.hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; -- } -- } -- -- Loader { -- id: label -- -- Layout.alignment: Qt.AlignHCenter | Qt.AlignTop -- Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 -- -- sourceComponent: ws.icon.length === 1 ? letterComp : iconComp -- -- Component { -- id: iconComp -- -- MaterialIcon { -- fill: 1 -- text: ws.icon -- verticalAlignment: Qt.AlignVCenter -- } -- } -- -- Component { -- id: letterComp -- -- StyledText { -- text: ws.icon -- verticalAlignment: Qt.AlignVCenter -- } -- } -- } -- -- Loader { -- id: windows -- -- Layout.alignment: Qt.AlignHCenter -- Layout.fillHeight: true -- Layout.preferredHeight: implicitHeight -- -- visible: active -- active: ws.hasWindows -- -- sourceComponent: Column { -- spacing: 0 -- -- add: Transition { -- Anim { -- properties: "scale" -- from: 0 -- to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -- } -- } -- -- move: Transition { -- Anim { -- properties: "scale" -- to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -- } -- Anim { -- properties: "x,y" -- } -- } -- -- Repeater { -- model: ScriptModel { -- values: Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId) -- } -- -- MaterialIcon { -- required property var modelData -- -- grade: 0 -- text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") -- color: Colours.palette.m3onSurfaceVariant -- } -- } -- } -- -- Behavior on Layout.preferredHeight { -- Anim {} -- } -- } -- } -+ delegate: SpecialWsDelegate {} - - add: Transition { - Anim { - properties: "scale" - from: 0 - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - } - -@@ -256,12 +127,12 @@ Item { - Anim { - property: "scale" - to: 0.5 -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - Anim { - property: "opacity" - to: 0 -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - -@@ -269,7 +140,7 @@ Item { - Anim { - properties: "scale" - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - Anim { - properties: "x,y" -@@ -280,7 +151,7 @@ Item { - Anim { - properties: "scale" - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - Anim { - properties: "x,y" -@@ -289,6 +160,7 @@ Item { - } - - Loader { -+ asynchronous: true - active: Config.bar.workspaces.activeIndicator - anchors.fill: parent - -@@ -300,10 +172,10 @@ Item { - anchors.right: parent.right - - y: (view.currentItem?.y ?? 0) - view.contentY -- implicitHeight: view.currentItem?.size ?? 0 -+ implicitHeight: (view.currentItem as SpecialWsDelegate)?.size ?? 0 - - color: Colours.palette.m3tertiary -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - Colouriser { - source: view -@@ -320,13 +192,13 @@ Item { - - Behavior on y { - Anim { -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - - Behavior on implicitHeight { - Anim { -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - } -@@ -341,7 +213,7 @@ Item { - drag.target: view.contentItem - drag.axis: Drag.YAxis - drag.maximumY: 0 -- drag.minimumY: Math.min(0, view.height - view.contentHeight - Appearance.padding.small) -+ drag.minimumY: Math.min(0, view.height - view.contentHeight - Tokens.padding.small) - - onPressed: event => startY = event.y - -@@ -349,11 +221,150 @@ Item { - if (Math.abs(event.y - startY) > drag.threshold) - return; - -- const ws = view.itemAt(event.x, event.y); -+ const ws = view.itemAt(event.x, event.y) as SpecialWsDelegate; - if (ws?.modelData) - Hypr.dispatch(`togglespecialworkspace ${ws.modelData.name.slice(8)}`); - else - Hypr.dispatch("togglespecialworkspace special"); - } - } -+ -+ component SpecialWsDelegate: ColumnLayout { -+ id: ws -+ -+ required property HyprlandWorkspace modelData -+ readonly property int size: label.Layout.preferredHeight + (hasWindows ? windows.implicitHeight + Tokens.padding.small : 0) -+ property int wsId -+ property string icon -+ property bool hasWindows -+ -+ anchors.left: view.contentItem.left -+ anchors.right: view.contentItem.right -+ -+ spacing: 0 -+ -+ Component.onCompleted: { -+ wsId = modelData.id; -+ icon = Icons.getSpecialWsIcon(modelData.name); -+ hasWindows = Config.bar.workspaces.showWindowsOnSpecialWorkspaces && modelData.lastIpcObject.windows > 0; -+ } -+ -+ // Hacky thing cause modelData gets destroyed before the remove anim finishes -+ Connections { -+ function onIdChanged(): void { -+ if (ws.modelData) -+ ws.wsId = ws.modelData.id; -+ } -+ -+ function onNameChanged(): void { -+ if (ws.modelData) -+ ws.icon = Icons.getSpecialWsIcon(ws.modelData.name); -+ } -+ -+ function onLastIpcObjectChanged(): void { -+ if (ws.modelData) -+ ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; -+ } -+ -+ target: ws.modelData -+ } -+ -+ Connections { -+ function onShowWindowsOnSpecialWorkspacesChanged(): void { -+ if (ws.modelData) -+ ws.hasWindows = root.Config.bar.workspaces.showWindowsOnSpecialWorkspaces && ws.modelData.lastIpcObject.windows > 0; -+ } -+ -+ target: root.Config.bar.workspaces -+ } -+ -+ Loader { -+ id: label -+ -+ asynchronous: true -+ -+ Layout.alignment: Qt.AlignHCenter | Qt.AlignTop -+ Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 -+ -+ sourceComponent: ws.icon.length === 1 ? letterComp : iconComp -+ -+ Component { -+ id: iconComp -+ -+ MaterialIcon { -+ fill: 1 -+ text: ws.icon -+ verticalAlignment: Qt.AlignVCenter -+ } -+ } -+ -+ Component { -+ id: letterComp -+ -+ StyledText { -+ text: ws.icon -+ verticalAlignment: Qt.AlignVCenter -+ } -+ } -+ } -+ -+ Loader { -+ id: windows -+ -+ asynchronous: true -+ -+ Layout.alignment: Qt.AlignHCenter -+ Layout.fillHeight: true -+ Layout.preferredHeight: implicitHeight -+ -+ visible: active -+ active: ws.hasWindows -+ -+ sourceComponent: Column { -+ spacing: 0 -+ -+ add: Transition { -+ Anim { -+ properties: "scale" -+ from: 0 -+ to: 1 -+ easing: Tokens.anim.standardDecel -+ } -+ } -+ -+ move: Transition { -+ Anim { -+ properties: "scale" -+ to: 1 -+ easing: Tokens.anim.standardDecel -+ } -+ Anim { -+ properties: "x,y" -+ } -+ } -+ -+ Repeater { -+ model: ScriptModel { -+ values: { -+ const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws.wsId); -+ const maxIcons = root.Config.bar.workspaces.maxWindowIcons; -+ return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; -+ } -+ } -+ -+ MaterialIcon { -+ required property var modelData -+ -+ grade: 0 -+ text: Icons.getAppCategoryIcon(modelData.lastIpcObject.class, "terminal") -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ } -+ } -+ -+ Behavior on Layout.preferredHeight { -+ Anim {} -+ } -+ } -+ } - } -diff --git a/modules/bar/components/workspaces/Workspace.qml b/modules/bar/components/workspaces/Workspace.qml -index 3c8238b5..bd581aab 100644 ---- a/modules/bar/components/workspaces/Workspace.qml -+++ b/modules/bar/components/workspaces/Workspace.qml -@@ -1,10 +1,12 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.services - import qs.utils --import qs.config --import Quickshell --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root -@@ -16,7 +18,7 @@ ColumnLayout { - - readonly property bool isWorkspace: true // Flag for finding workspace children - // Unanimated prop for others to use as reference -- readonly property int size: implicitHeight + (hasWindows ? Appearance.padding.small : 0) -+ readonly property int size: implicitHeight + (hasWindows ? Tokens.padding.small : 0) - - readonly property int ws: groupOffset + index + 1 - readonly property bool isOccupied: occupied[ws] ?? false -@@ -31,7 +33,7 @@ ColumnLayout { - id: indicator - - Layout.alignment: Qt.AlignHCenter | Qt.AlignTop -- Layout.preferredHeight: Config.bar.sizes.innerWidth - Appearance.padding.small * 2 -+ Layout.preferredHeight: Tokens.sizes.bar.innerWidth - Tokens.padding.small * 2 - - animate: true - text: { -@@ -55,9 +57,11 @@ ColumnLayout { - Loader { - id: windows - -+ asynchronous: true -+ - Layout.alignment: Qt.AlignHCenter - Layout.fillHeight: true -- Layout.topMargin: -Config.bar.sizes.innerWidth / 10 -+ Layout.topMargin: -Tokens.sizes.bar.innerWidth / 10 - - visible: active - active: root.hasWindows -@@ -70,7 +74,7 @@ ColumnLayout { - properties: "scale" - from: 0 - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - } - -@@ -78,7 +82,7 @@ ColumnLayout { - Anim { - properties: "scale" - to: 1 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - Anim { - properties: "x,y" -@@ -87,7 +91,12 @@ ColumnLayout { - - Repeater { - model: ScriptModel { -- values: Hypr.toplevels.values.filter(c => c.workspace?.id === root.ws) -+ values: { -+ const ws = root.ws; -+ const windows = Hypr.toplevels.values.filter(c => c.workspace?.id === ws); -+ const maxIcons = root.Config.bar.workspaces.maxWindowIcons; -+ return maxIcons > 0 ? windows.slice(0, maxIcons) : windows; -+ } - } - - MaterialIcon { -diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml -index bfa80ab6..2030bf80 100644 ---- a/modules/bar/components/workspaces/Workspaces.qml -+++ b/modules/bar/components/workspaces/Workspaces.qml -@@ -1,39 +1,43 @@ - pragma ComponentBehavior: Bound - --import qs.services --import qs.config --import qs.components --import Quickshell - import QtQuick --import QtQuick.Layouts - import QtQuick.Effects -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledClippingRect { - id: root - - required property ShellScreen screen -+ required property bool fullscreen - -- readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject?.specialWorkspace?.name !== "" -- readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId -+ readonly property bool onSpecial: (GlobalConfig.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" -+ readonly property int activeWsId: GlobalConfig.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId - -- readonly property var occupied: Hypr.workspaces.values.reduce((acc, curr) => { -- acc[curr.id] = curr.lastIpcObject.windows > 0; -- return acc; -- }, {}) -+ readonly property var occupied: { -+ const occ = {}; -+ for (const ws of Hypr.workspaces.values) -+ occ[ws.id] = ws.lastIpcObject.windows > 0; -+ return occ; -+ } - readonly property int groupOffset: Math.floor((activeWsId - 1) / Config.bar.workspaces.shown) * Config.bar.workspaces.shown - - property real blur: onSpecial ? 1 : 0 - -- implicitWidth: Config.bar.sizes.innerWidth -- implicitHeight: layout.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: Tokens.sizes.bar.innerWidth -+ implicitHeight: layout.implicitHeight + Tokens.padding.small * 2 - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - Item { - anchors.fill: parent - scale: root.onSpecial ? 0.8 : 1 - opacity: root.onSpecial ? 0.5 : 1 -+ visible: !root.fullscreen - - layer.enabled: root.blur > 0 - layer.effect: MultiEffect { -@@ -43,10 +47,11 @@ StyledClippingRect { - } - - Loader { -+ asynchronous: true - active: Config.bar.workspaces.occupiedBg - - anchors.fill: parent -- anchors.margins: Appearance.padding.small -+ anchors.margins: Tokens.padding.small - - sourceComponent: OccupiedBg { - workspaces: workspaces -@@ -59,7 +64,7 @@ StyledClippingRect { - id: layout - - anchors.centerIn: parent -- spacing: Math.floor(Appearance.spacing.small / 2) -+ spacing: Math.floor(Tokens.spacing.small / 2) - - Repeater { - id: workspaces -@@ -75,6 +80,7 @@ StyledClippingRect { - } - - Loader { -+ asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter - active: Config.bar.workspaces.activeIndicator - -@@ -82,13 +88,14 @@ StyledClippingRect { - activeWsId: root.activeWsId - workspaces: workspaces - mask: layout -+ fullscreen: root.fullscreen - } - } - - MouseArea { - anchors.fill: layout - onClicked: event => { -- const ws = layout.childAt(event.x, event.y).ws; -+ const ws = (layout.childAt(event.x, event.y) as Workspace)?.ws; - if (Hypr.activeWsId !== ws) - Hypr.dispatch(`workspace ${ws}`); - else -@@ -108,8 +115,10 @@ StyledClippingRect { - Loader { - id: specialWs - -+ asynchronous: true -+ - anchors.fill: parent -- anchors.margins: Appearance.padding.small -+ anchors.margins: Tokens.padding.small - - active: opacity > 0 - -@@ -131,7 +140,7 @@ StyledClippingRect { - - Behavior on blur { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -diff --git a/modules/bar/popouts/ActiveWindow.qml b/modules/bar/popouts/ActiveWindow.qml -index adf7b774..12246645 100644 ---- a/modules/bar/popouts/ActiveWindow.qml -+++ b/modules/bar/popouts/ActiveWindow.qml -@@ -1,36 +1,37 @@ -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Wayland -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components - import qs.services - import qs.utils --import qs.config --import Quickshell.Widgets --import Quickshell.Wayland --import QtQuick --import QtQuick.Layouts - - Item { - id: root - -- required property Item wrapper -+ required property PopoutState popouts - -- implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Appearance.padding.large * 2 -+ implicitWidth: Hypr.activeToplevel ? child.implicitWidth : -Tokens.padding.large * 2 - implicitHeight: child.implicitHeight - - Column { - id: child - - anchors.centerIn: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - RowLayout { - id: detailsRow - - anchors.left: parent.left - anchors.right: parent.right -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - IconImage { - id: icon - -+ asynchronous: true - Layout.alignment: Qt.AlignVCenter - implicitSize: details.implicitHeight - source: Icons.getAppIcon(Hypr.activeToplevel?.lastIpcObject.class ?? "", "image-missing") -@@ -45,7 +46,7 @@ Item { - StyledText { - Layout.fillWidth: true - text: Hypr.activeToplevel?.title ?? "" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - -@@ -58,17 +59,14 @@ Item { - } - - Item { -- implicitWidth: expandIcon.implicitHeight + Appearance.padding.small * 2 -- implicitHeight: expandIcon.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: expandIcon.implicitHeight + Tokens.padding.small * 2 -+ implicitHeight: expandIcon.implicitHeight + Tokens.padding.small * 2 - - Layout.alignment: Qt.AlignVCenter - - StateLayer { -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -- root.wrapper.detach("winfo"); -- } -+ radius: Tokens.rounding.normal -+ onClicked: root.popouts.detachRequested("winfo") - } - - MaterialIcon { -@@ -79,23 +77,23 @@ Item { - - text: "chevron_right" - -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } - - ClippingWrapperRectangle { - color: "transparent" -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - - ScreencopyView { - id: preview - -- captureSource: Hypr.activeToplevel?.wayland ?? null -+ captureSource: Hypr.activeToplevel?.wayland ?? null // qmllint disable unresolved-type - live: visible - -- constraintSize.width: Config.bar.sizes.windowPreviewSize -- constraintSize.height: Config.bar.sizes.windowPreviewSize -+ constraintSize.width: Tokens.sizes.bar.windowPreviewSize -+ constraintSize.height: Tokens.sizes.bar.windowPreviewSize - } - } - } -diff --git a/modules/bar/popouts/Audio.qml b/modules/bar/popouts/Audio.qml -index 58b29ba8..e8dc9203 100644 ---- a/modules/bar/popouts/Audio.qml -+++ b/modules/bar/popouts/Audio.qml -@@ -1,23 +1,21 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Controls -+import QtQuick.Layouts -+import Quickshell.Services.Pipewire -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config --import Quickshell --import Quickshell.Services.Pipewire --import QtQuick --import QtQuick.Layouts --import QtQuick.Controls --import "../../controlcenter/network" - - Item { - id: root - -- required property var wrapper -+ required property PopoutState popouts - -- implicitWidth: layout.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: layout.implicitHeight + Appearance.padding.normal * 2 -+ implicitWidth: layout.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: layout.implicitHeight + Tokens.padding.normal * 2 - - ButtonGroup { - id: sinks -@@ -32,7 +30,7 @@ Item { - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Output device") -@@ -55,7 +53,7 @@ Item { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.smaller -+ Layout.topMargin: Tokens.spacing.smaller - text: qsTr("Input device") - font.weight: 500 - } -@@ -74,15 +72,15 @@ Item { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.smaller -- Layout.bottomMargin: -Appearance.spacing.small / 2 -+ Layout.topMargin: Tokens.spacing.smaller -+ Layout.bottomMargin: -Tokens.spacing.small / 2 - text: qsTr("Volume (%1)").arg(Audio.muted ? qsTr("Muted") : `${Math.round(Audio.volume * 100)}%`) - font.weight: 500 - } - - CustomMouseArea { - Layout.fillWidth: true -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - - onWheel: event => { - if (event.angleDelta.y > 0) -@@ -107,14 +105,14 @@ Item { - - IconTextButton { - Layout.fillWidth: true -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer -- verticalPadding: Appearance.padding.small -+ verticalPadding: Tokens.padding.small - text: qsTr("Open settings") - icon: "settings" - -- onClicked: root.wrapper.detach("audio") -+ onClicked: root.popouts.detachRequested("audio") - } - } - } -diff --git a/modules/bar/popouts/Background.qml b/modules/bar/popouts/Background.qml -deleted file mode 100644 -index 075b6988..00000000 ---- a/modules/bar/popouts/Background.qml -+++ /dev/null -@@ -1,73 +0,0 @@ --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Shapes -- --ShapePath { -- id: root -- -- required property Wrapper wrapper -- required property bool invertBottomRounding -- readonly property real rounding: wrapper.isDetached ? Appearance.rounding.normal : Config.border.rounding -- readonly property bool flatten: wrapper.width < rounding * 2 -- readonly property real roundingX: flatten ? wrapper.width / 2 : rounding -- property real ibr: invertBottomRounding ? -1 : 1 -- -- property real sideRounding: startX > 0 ? -1 : 1 -- -- strokeWidth: -1 -- fillColor: Colours.palette.m3surface -- -- PathArc { -- relativeX: root.roundingX -- relativeY: root.rounding * root.sideRounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise -- } -- PathLine { -- relativeX: root.wrapper.width - root.roundingX * 2 -- relativeY: 0 -- } -- PathArc { -- relativeX: root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- } -- PathLine { -- relativeX: 0 -- relativeY: root.wrapper.height - root.rounding * 2 -- } -- PathArc { -- relativeX: -root.roundingX * root.ibr -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- direction: root.ibr < 0 ? PathArc.Counterclockwise : PathArc.Clockwise -- } -- PathLine { -- relativeX: -(root.wrapper.width - root.roundingX - root.roundingX * root.ibr) -- relativeY: 0 -- } -- PathArc { -- relativeX: -root.roundingX -- relativeY: root.rounding * root.sideRounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- direction: root.sideRounding < 0 ? PathArc.Clockwise : PathArc.Counterclockwise -- } -- -- Behavior on fillColor { -- CAnim {} -- } -- -- Behavior on ibr { -- Anim {} -- } -- -- Behavior on sideRounding { -- Anim {} -- } --} -diff --git a/modules/bar/popouts/Battery.qml b/modules/bar/popouts/Battery.qml -index ac975e1b..93d2012b 100644 ---- a/modules/bar/popouts/Battery.qml -+++ b/modules/bar/popouts/Battery.qml -@@ -1,16 +1,16 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell.Services.UPower -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell.Services.UPower --import QtQuick - - Column { - id: root - -- spacing: Appearance.spacing.normal -- width: Config.bar.sizes.batteryWidth -+ spacing: Tokens.spacing.normal -+ width: Tokens.sizes.bar.batteryWidth - - StyledText { - text: UPower.displayDevice.isLaptopBattery ? qsTr("Remaining: %1%").arg(Math.round(UPower.displayDevice.percentage * 100)) : qsTr("No battery detected") -@@ -37,18 +37,19 @@ Column { - } - - Loader { -+ asynchronous: true - anchors.horizontalCenter: parent.horizontalCenter - - active: PowerProfiles.degradationReason !== PerformanceDegradationReason.None - -- height: active ? (item?.implicitHeight ?? 0) : 0 -+ height: active ? ((item as Item)?.implicitHeight ?? 0) : 0 - - sourceComponent: StyledRect { -- implicitWidth: child.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: child.implicitHeight + Appearance.padding.smaller * 2 -+ implicitWidth: child.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: child.implicitHeight + Tokens.padding.smaller * 2 - - color: Colours.palette.m3error -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - Column { - id: child -@@ -57,7 +58,7 @@ Column { - - Row { - anchors.horizontalCenter: parent.horizontalCenter -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - MaterialIcon { - anchors.verticalCenter: parent.verticalCenter -@@ -71,7 +72,7 @@ Column { - anchors.verticalCenter: parent.verticalCenter - text: qsTr("Performance Degraded") - color: Colours.palette.m3onError -- font.family: Appearance.font.family.mono -+ font.family: Tokens.font.family.mono - font.weight: 500 - } - -@@ -108,17 +109,17 @@ Column { - - anchors.horizontalCenter: parent.horizontalCenter - -- implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Appearance.padding.normal * 2 + Appearance.spacing.large * 2 -- implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Appearance.padding.small * 2 -+ implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + Tokens.padding.normal * 2 + Tokens.spacing.large * 2 -+ implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + Tokens.padding.small * 2 - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - StyledRect { - id: indicator - - color: Colours.palette.m3primary -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - state: profiles.current - - states: [ -@@ -146,10 +147,8 @@ Column { - ] - - transitions: Transition { -- AnchorAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ AnchorAnim { -+ type: AnchorAnim.Emphasized - } - } - } -@@ -159,7 +158,7 @@ Column { - - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left -- anchors.leftMargin: Appearance.padding.small -+ anchors.leftMargin: Tokens.padding.small - - profile: PowerProfile.PowerSaver - icon: "energy_savings_leaf" -@@ -179,7 +178,7 @@ Column { - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right -- anchors.rightMargin: Appearance.padding.small -+ anchors.rightMargin: Tokens.padding.small - - profile: PowerProfile.Performance - icon: "rocket_launch" -@@ -200,16 +199,13 @@ Column { - required property string icon - required property int profile - -- implicitWidth: icon.implicitHeight + Appearance.padding.small * 2 -- implicitHeight: icon.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: icon.implicitHeight + Tokens.padding.small * 2 -+ implicitHeight: icon.implicitHeight + Tokens.padding.small * 2 - - StateLayer { -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: profiles.current === parent.icon ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface -- -- function onClicked(): void { -- PowerProfiles.profile = parent.profile; -- } -+ onClicked: PowerProfiles.profile = parent.profile - } - - MaterialIcon { -@@ -218,7 +214,7 @@ Column { - anchors.centerIn: parent - - text: parent.icon -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - color: profiles.current === text ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - fill: profiles.current === text ? 1 : 0 - -diff --git a/modules/bar/popouts/Bluetooth.qml b/modules/bar/popouts/Bluetooth.qml -index 676da82f..baca115e 100644 ---- a/modules/bar/popouts/Bluetooth.qml -+++ b/modules/bar/popouts/Bluetooth.qml -@@ -1,35 +1,35 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Bluetooth -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config - import qs.utils --import Quickshell --import Quickshell.Bluetooth --import QtQuick --import QtQuick.Layouts --import "../../controlcenter/network" - - ColumnLayout { - id: root - -- required property Item wrapper -+ required property PopoutState popouts - -- spacing: Appearance.spacing.small -+ width: 300 -+ spacing: Tokens.spacing.small - - StyledText { -- Layout.topMargin: Appearance.padding.normal -- Layout.rightMargin: Appearance.padding.small -+ Layout.topMargin: Tokens.padding.normal -+ Layout.rightMargin: Tokens.padding.small - text: qsTr("Bluetooth") - font.weight: 500 - } - - Toggle { - label: qsTr("Enabled") -- checked: Bluetooth.defaultAdapter?.enabled ?? false -+ checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type - toggle.onToggled: { -- const adapter = Bluetooth.defaultAdapter; -+ const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type - if (adapter) - adapter.enabled = checked; - } -@@ -37,19 +37,19 @@ ColumnLayout { - - Toggle { - label: qsTr("Discovering") -- checked: Bluetooth.defaultAdapter?.discovering ?? false -+ checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type - toggle.onToggled: { -- const adapter = Bluetooth.defaultAdapter; -+ const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type - if (adapter) - adapter.discovering = checked; - } - } - - StyledText { -- Layout.topMargin: Appearance.spacing.small -- Layout.rightMargin: Appearance.padding.small -+ Layout.topMargin: Tokens.spacing.small -+ Layout.rightMargin: Tokens.padding.small - text: { -- const devices = Bluetooth.devices.values; -+ const devices = Bluetooth.devices.values; // qmllint disable unresolved-type - let available = qsTr("%1 device%2 available").arg(devices.length).arg(devices.length === 1 ? "" : "s"); - const connected = devices.filter(d => d.connected).length; - if (connected > 0) -@@ -57,23 +57,23 @@ ColumnLayout { - return available; - } - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - Repeater { - model: ScriptModel { -- values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) -+ values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired) || a.name.localeCompare(b.name)).slice(0, 5) // qmllint disable unresolved-type - } - - RowLayout { - id: device - - required property BluetoothDevice modelData -- readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting -+ readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type - - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- spacing: Appearance.spacing.small -+ Layout.rightMargin: Tokens.padding.small -+ spacing: Tokens.spacing.small - - opacity: 0 - scale: 0.7 -@@ -96,20 +96,27 @@ ColumnLayout { - } - - StyledText { -- Layout.leftMargin: Appearance.spacing.small / 2 -- Layout.rightMargin: Appearance.spacing.small / 2 -+ Layout.leftMargin: Tokens.spacing.small / 2 -+ Layout.rightMargin: Tokens.spacing.small / 2 - Layout.fillWidth: true - text: device.modelData.name -+ elide: Text.ElideRight -+ } -+ -+ MaterialIcon { -+ visible: device.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type -+ text: Icons.getBatteryIcon(device.modelData.batteryAvailable ? device.modelData.battery * 100 : -1) -+ color: device.modelData.battery < 0.2 ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant - } - - StyledRect { - id: connectBtn - - implicitWidth: implicitHeight -- implicitHeight: connectIcon.implicitHeight + Appearance.padding.small -+ implicitHeight: connectIcon.implicitHeight + Tokens.padding.small - -- radius: Appearance.rounding.full -- color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) -+ radius: Tokens.rounding.full -+ color: Qt.alpha(Colours.palette.m3primary, device.modelData.state === BluetoothDeviceState.Connected ? 1 : 0) // qmllint disable unresolved-type - - CircularIndicator { - anchors.fill: parent -@@ -117,12 +124,9 @@ ColumnLayout { - } - - StateLayer { -- color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface -+ color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type - disabled: device.loading -- -- function onClicked(): void { -- device.modelData.connected = !device.modelData.connected; -- } -+ onClicked: device.modelData.connected = !device.modelData.connected - } - - MaterialIcon { -@@ -131,7 +135,7 @@ ColumnLayout { - anchors.centerIn: parent - animate: true - text: device.modelData.connected ? "link_off" : "link" -- color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface -+ color: device.modelData.state === BluetoothDeviceState.Connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface // qmllint disable unresolved-type - - opacity: device.loading ? 0 : 1 - -@@ -142,17 +146,16 @@ ColumnLayout { - } - - Loader { -+ visible: status === Loader.Ready -+ asynchronous: true - active: device.modelData.bonded - sourceComponent: Item { - implicitWidth: connectBtn.implicitWidth - implicitHeight: connectBtn.implicitHeight - - StateLayer { -- radius: Appearance.rounding.full -- -- function onClicked(): void { -- device.modelData.forget(); -- } -+ radius: Tokens.rounding.full -+ onClicked: device.modelData.forget() - } - - MaterialIcon { -@@ -166,14 +169,14 @@ ColumnLayout { - - IconTextButton { - Layout.fillWidth: true -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer -- verticalPadding: Appearance.padding.small -+ verticalPadding: Tokens.padding.small - text: qsTr("Open settings") - icon: "settings" - -- onClicked: root.wrapper.detach("bluetooth") -+ onClicked: root.popouts.detachRequested("bluetooth") - } - - component Toggle: RowLayout { -@@ -182,8 +185,8 @@ ColumnLayout { - property alias toggle: toggle - - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- spacing: Appearance.spacing.normal -+ Layout.rightMargin: Tokens.padding.small -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -diff --git a/modules/bar/popouts/ClipWrapper.qml b/modules/bar/popouts/ClipWrapper.qml -new file mode 100644 -index 00000000..ab80d2cc ---- /dev/null -+++ b/modules/bar/popouts/ClipWrapper.qml -@@ -0,0 +1,67 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import Quickshell -+import qs.components -+import qs.modules.bar.popouts // Need to import this module so the Wrapper type is the same as others -+ -+Item { -+ id: root -+ -+ required property ShellScreen screen -+ required property real borderThickness -+ -+ readonly property alias content: content -+ property real offsetScale: x > 0 || content.hasCurrent ? 0 : 1 -+ -+ visible: width > 0 && height > 0 -+ clip: true -+ -+ implicitWidth: content.implicitWidth * (1 - offsetScale) -+ implicitHeight: content.implicitHeight -+ -+ x: content.isDetached ? (parent.width - content.nonAnimWidth) / 2 : 0 -+ y: { -+ if (content.isDetached) -+ return (parent.height - content.nonAnimHeight) / 2; -+ -+ const off = content.currentCenter - borderThickness - content.nonAnimHeight / 2; -+ const diff = parent.height - Math.floor(off + content.nonAnimHeight); -+ if (diff < 0) -+ return off + diff; -+ return Math.max(off, 0); -+ } -+ -+ Behavior on offsetScale { -+ Anim { -+ type: Anim.DefaultSpatial -+ } -+ } -+ -+ Behavior on x { -+ Anim { -+ duration: content.animLength -+ easing: content.animCurve -+ } -+ } -+ -+ Behavior on y { -+ enabled: root.offsetScale < 1 -+ -+ Anim { -+ duration: content.animLength -+ easing: content.animCurve -+ } -+ } -+ -+ Wrapper { -+ id: content -+ -+ screen: root.screen -+ offsetScale: root.offsetScale -+ -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.left: parent.left -+ anchors.leftMargin: (-implicitWidth - 5) * root.offsetScale -+ } -+} -diff --git a/modules/bar/popouts/Content.qml b/modules/bar/popouts/Content.qml -index 40768444..42c5a766 100644 ---- a/modules/bar/popouts/Content.qml -+++ b/modules/bar/popouts/Content.qml -@@ -1,43 +1,41 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config -+import "./kblayout" -+import QtQuick - import Quickshell - import Quickshell.Services.SystemTray --import QtQuick -- --import "./kblayout" -+import Caelestia.Config -+import qs.components - - Item { - id: root - -- required property Item wrapper -+ required property PopoutState popouts - readonly property Popout currentPopout: content.children.find(c => c.shouldBeActive) ?? null - readonly property Item current: currentPopout?.item ?? null - -- anchors.centerIn: parent -- -- implicitWidth: (currentPopout?.implicitWidth ?? 0) + Appearance.padding.large * 2 -- implicitHeight: (currentPopout?.implicitHeight ?? 0) + Appearance.padding.large * 2 -+ implicitWidth: (currentPopout?.implicitWidth ?? 0) + Tokens.padding.large * 2 -+ implicitHeight: (currentPopout?.implicitHeight ?? 0) + Tokens.padding.large * 2 - - Item { - id: content - - anchors.fill: parent -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - - Popout { - name: "activewindow" - sourceComponent: ActiveWindow { -- wrapper: root.wrapper -+ popouts: root.popouts - } - } - - Popout { - id: networkPopout -+ - name: "network" - sourceComponent: Network { -- wrapper: root.wrapper -+ popouts: root.popouts - view: "wireless" - } - } -@@ -45,60 +43,64 @@ Item { - Popout { - name: "ethernet" - sourceComponent: Network { -- wrapper: root.wrapper -+ popouts: root.popouts - view: "ethernet" - } - } - - Popout { - id: passwordPopout -+ - name: "wirelesspassword" - sourceComponent: WirelessPassword { - id: passwordComponent -- wrapper: root.wrapper -- network: networkPopout.item?.passwordNetwork ?? null -+ -+ popouts: root.popouts -+ network: (networkPopout.item as Network)?.passwordNetwork ?? null - } - - Connections { -- target: root.wrapper - function onCurrentNameChanged() { - // Update network immediately when password popout becomes active -- if (root.wrapper.currentName === "wirelesspassword") { -+ if (root.popouts.currentName === "wirelesspassword") { - // Set network immediately if available -- if (networkPopout.item && networkPopout.item.passwordNetwork) { -+ if ((networkPopout.item as Network)?.passwordNetwork) { - if (passwordPopout.item) { -- passwordPopout.item.network = networkPopout.item.passwordNetwork; -+ (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; - } - } - // Also try after a short delay in case networkPopout.item wasn't ready - Qt.callLater(() => { -- if (passwordPopout.item && networkPopout.item && networkPopout.item.passwordNetwork) { -- passwordPopout.item.network = networkPopout.item.passwordNetwork; -+ if (passwordPopout.item && (networkPopout.item as Network)?.passwordNetwork) { -+ (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; - } - }, 100); - } - } -+ -+ target: root.popouts - } - - Connections { -- target: networkPopout - function onItemChanged() { - // When network popout loads, update password popout if it's active -- if (root.wrapper.currentName === "wirelesspassword" && passwordPopout.item) { -+ if (root.popouts.currentName === "wirelesspassword" && passwordPopout.item) { - Qt.callLater(() => { -- if (networkPopout.item && networkPopout.item.passwordNetwork) { -- passwordPopout.item.network = networkPopout.item.passwordNetwork; -+ if ((networkPopout.item as Network)?.passwordNetwork) { -+ (passwordPopout.item as WirelessPassword).network = (networkPopout.item as Network).passwordNetwork; - } - }); - } - } -+ -+ target: networkPopout - } - } - - Popout { - name: "bluetooth" - sourceComponent: Bluetooth { -- wrapper: root.wrapper -+ popouts: root.popouts - } - } - -@@ -110,15 +112,13 @@ Item { - Popout { - name: "audio" - sourceComponent: Audio { -- wrapper: root.wrapper -+ popouts: root.popouts - } - } - - Popout { - name: "kblayout" -- sourceComponent: KbLayout { -- wrapper: root.wrapper -- } -+ sourceComponent: KbLayout {} - } - - Popout { -@@ -128,7 +128,7 @@ Item { - - Repeater { - model: ScriptModel { -- values: [...SystemTray.items.values] -+ values: SystemTray.items.values.filter(i => !GlobalConfig.bar.tray.hiddenIcons.includes(i.id)) - } - - Popout { -@@ -141,22 +141,22 @@ Item { - sourceComponent: trayMenuComp - - Connections { -- target: root.wrapper -- - function onHasCurrentChanged(): void { -- if (root.wrapper.hasCurrent && trayMenu.shouldBeActive) { -+ if (root.popouts.hasCurrent && trayMenu.shouldBeActive) { - trayMenu.sourceComponent = null; - trayMenu.sourceComponent = trayMenuComp; - } - } -+ -+ target: root.popouts - } - - Component { - id: trayMenuComp - - TrayMenu { -- popouts: root.wrapper -- trayItem: trayMenu.modelData.menu -+ popouts: root.popouts -+ trayItem: trayMenu.modelData.menu // qmllint disable unresolved-type - } - } - } -@@ -167,10 +167,9 @@ Item { - id: popout - - required property string name -- readonly property bool shouldBeActive: root.wrapper.currentName === name -+ readonly property bool shouldBeActive: root.popouts.currentName === name - -- anchors.verticalCenter: parent.verticalCenter -- anchors.right: parent.right -+ anchors.centerIn: parent - - opacity: 0 - scale: 0.8 -@@ -195,7 +194,7 @@ Item { - SequentialAnimation { - Anim { - properties: "opacity,scale" -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - PropertyAction { - target: popout -diff --git a/modules/bar/popouts/LockStatus.qml b/modules/bar/popouts/LockStatus.qml -index 7d74530e..caab7487 100644 ---- a/modules/bar/popouts/LockStatus.qml -+++ b/modules/bar/popouts/LockStatus.qml -@@ -1,10 +1,10 @@ -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import QtQuick.Layouts - - ColumnLayout { -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Capslock: %1").arg(Hypr.capsLock ? "Enabled" : "Disabled") -diff --git a/modules/bar/popouts/Network.qml b/modules/bar/popouts/Network.qml -index 5b32e4a6..63b69b57 100644 ---- a/modules/bar/popouts/Network.qml -+++ b/modules/bar/popouts/Network.qml -@@ -1,33 +1,33 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - -- required property Item wrapper -+ required property PopoutState popouts - - property string connectingToSsid: "" - property string view: "wireless" // "wireless" or "ethernet" - property var passwordNetwork: null - property bool showPasswordDialog: false - -- spacing: Appearance.spacing.small -- width: Config.bar.sizes.networkWidth -+ spacing: Tokens.spacing.small -+ width: Tokens.sizes.bar.networkWidth - - // Wireless section - StyledText { - visible: root.view === "wireless" - Layout.preferredHeight: visible ? implicitHeight : 0 -- Layout.topMargin: visible ? Appearance.padding.normal : 0 -- Layout.rightMargin: Appearance.padding.small -+ Layout.topMargin: visible ? Tokens.padding.normal : 0 -+ Layout.rightMargin: Tokens.padding.small - text: qsTr("Wireless") - font.weight: 500 - } -@@ -43,11 +43,11 @@ ColumnLayout { - StyledText { - visible: root.view === "wireless" - Layout.preferredHeight: visible ? implicitHeight : 0 -- Layout.topMargin: visible ? Appearance.spacing.small : 0 -- Layout.rightMargin: Appearance.padding.small -- text: qsTr("%1 networks available").arg(Nmcli.networks.length) -+ Layout.topMargin: visible ? Tokens.spacing.small : 0 -+ Layout.rightMargin: Tokens.padding.small -+ text: qsTr("%1 networks available").arg(Nmcli.networks.length) // qmllint disable missing-property - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - Repeater { -@@ -70,8 +70,8 @@ ColumnLayout { - visible: root.view === "wireless" - Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- spacing: Appearance.spacing.small -+ Layout.rightMargin: Tokens.padding.small -+ spacing: Tokens.spacing.small - - opacity: 0 - scale: 0.7 -@@ -97,12 +97,12 @@ ColumnLayout { - MaterialIcon { - visible: networkItem.modelData.isSecure - text: "lock" -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.leftMargin: Appearance.spacing.small / 2 -- Layout.rightMargin: Appearance.spacing.small / 2 -+ Layout.leftMargin: Tokens.spacing.small / 2 -+ Layout.rightMargin: Tokens.spacing.small / 2 - Layout.fillWidth: true - text: networkItem.modelData.ssid - elide: Text.ElideRight -@@ -112,9 +112,9 @@ ColumnLayout { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: wirelessConnectIcon.implicitHeight + Appearance.padding.small -+ implicitHeight: wirelessConnectIcon.implicitHeight + Tokens.padding.small - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primary, networkItem.modelData.active ? 1 : 0) - - CircularIndicator { -@@ -126,7 +126,7 @@ ColumnLayout { - color: networkItem.modelData.active ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: networkItem.loading || !Nmcli.wifiEnabled - -- function onClicked(): void { -+ onClicked: { - if (networkItem.modelData.active) { - Nmcli.disconnectFromNetwork(); - } else { -@@ -135,7 +135,7 @@ ColumnLayout { - // Password is required - show password dialog - root.passwordNetwork = network; - root.showPasswordDialog = true; -- root.wrapper.currentName = "wirelesspassword"; -+ root.popouts.currentName = "wirelesspassword"; - }); - - // Clear connecting state if connection succeeds immediately (saved profile) -@@ -165,27 +165,24 @@ ColumnLayout { - StyledRect { - visible: root.view === "wireless" - Layout.preferredHeight: visible ? implicitHeight : 0 -- Layout.topMargin: visible ? Appearance.spacing.small : 0 -+ Layout.topMargin: visible ? Tokens.spacing.small : 0 - Layout.fillWidth: true -- implicitHeight: rescanBtn.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: rescanBtn.implicitHeight + Tokens.padding.small * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3primaryContainer - - StateLayer { - color: Colours.palette.m3onPrimaryContainer - disabled: Nmcli.scanning || !Nmcli.wifiEnabled -- -- function onClicked(): void { -- Nmcli.rescanWifi(); -- } -+ onClicked: Nmcli.rescanWifi() - } - - RowLayout { - id: rescanBtn - - anchors.centerIn: parent -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - opacity: Nmcli.scanning ? 0 : 1 - - MaterialIcon { -@@ -210,9 +207,9 @@ ColumnLayout { - - CircularIndicator { - anchors.centerIn: parent -- strokeWidth: Appearance.padding.small / 2 -+ strokeWidth: Tokens.padding.small / 2 - bgColour: "transparent" -- implicitSize: parent.implicitHeight - Appearance.padding.smaller * 2 -+ implicitSize: parent.implicitHeight - Tokens.padding.smaller * 2 - running: Nmcli.scanning - } - } -@@ -221,8 +218,8 @@ ColumnLayout { - StyledText { - visible: root.view === "ethernet" - Layout.preferredHeight: visible ? implicitHeight : 0 -- Layout.topMargin: visible ? Appearance.padding.normal : 0 -- Layout.rightMargin: Appearance.padding.small -+ Layout.topMargin: visible ? Tokens.padding.normal : 0 -+ Layout.rightMargin: Tokens.padding.small - text: qsTr("Ethernet") - font.weight: 500 - } -@@ -230,11 +227,11 @@ ColumnLayout { - StyledText { - visible: root.view === "ethernet" - Layout.preferredHeight: visible ? implicitHeight : 0 -- Layout.topMargin: visible ? Appearance.spacing.small : 0 -- Layout.rightMargin: Appearance.padding.small -+ Layout.topMargin: visible ? Tokens.spacing.small : 0 -+ Layout.rightMargin: Tokens.padding.small - text: qsTr("%1 devices available").arg(Nmcli.ethernetDevices.length) - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - Repeater { -@@ -256,8 +253,8 @@ ColumnLayout { - visible: root.view === "ethernet" - Layout.preferredHeight: visible ? implicitHeight : 0 - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- spacing: Appearance.spacing.small -+ Layout.rightMargin: Tokens.padding.small -+ spacing: Tokens.spacing.small - - opacity: 0 - scale: 0.7 -@@ -281,8 +278,8 @@ ColumnLayout { - } - - StyledText { -- Layout.leftMargin: Appearance.spacing.small / 2 -- Layout.rightMargin: Appearance.spacing.small / 2 -+ Layout.leftMargin: Tokens.spacing.small / 2 -+ Layout.rightMargin: Tokens.spacing.small / 2 - Layout.fillWidth: true - text: ethernetItem.modelData.interface || qsTr("Unknown") - elide: Text.ElideRight -@@ -292,9 +289,9 @@ ColumnLayout { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: connectIcon.implicitHeight + Appearance.padding.small -+ implicitHeight: connectIcon.implicitHeight + Tokens.padding.small - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primary, ethernetItem.modelData.connected ? 1 : 0) - - CircularIndicator { -@@ -306,7 +303,7 @@ ColumnLayout { - color: ethernetItem.modelData.connected ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - disabled: ethernetItem.loading - -- function onClicked(): void { -+ onClicked: { - if (ethernetItem.modelData.connected && ethernetItem.modelData.connection) { - Nmcli.disconnectEthernet(ethernetItem.modelData.connection, () => {}); - } else { -@@ -334,8 +331,6 @@ ColumnLayout { - } - - Connections { -- target: Nmcli -- - function onActiveChanged(): void { - if (Nmcli.active && root.connectingToSsid === Nmcli.active.ssid) { - root.connectingToSsid = ""; -@@ -343,8 +338,8 @@ ColumnLayout { - if (root.showPasswordDialog && root.passwordNetwork && Nmcli.active.ssid === root.passwordNetwork.ssid) { - root.showPasswordDialog = false; - root.passwordNetwork = null; -- if (root.wrapper.currentName === "wirelesspassword") { -- root.wrapper.currentName = "network"; -+ if (root.popouts.currentName === "wirelesspassword") { -+ root.popouts.currentName = "network"; - } - } - } -@@ -354,17 +349,20 @@ ColumnLayout { - if (!Nmcli.scanning) - scanIcon.rotation = 0; - } -+ -+ target: Nmcli - } - - Connections { -- target: root.wrapper - function onCurrentNameChanged(): void { - // Clear password network when leaving password dialog -- if (root.wrapper.currentName !== "wirelesspassword" && root.showPasswordDialog) { -+ if (root.popouts.currentName !== "wirelesspassword" && root.showPasswordDialog) { - root.showPasswordDialog = false; - root.passwordNetwork = null; - } - } -+ -+ target: root.popouts - } - - component Toggle: RowLayout { -@@ -373,8 +371,8 @@ ColumnLayout { - property alias toggle: toggle - - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- spacing: Appearance.spacing.normal -+ Layout.rightMargin: Tokens.padding.small -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -diff --git a/modules/bar/popouts/PopoutState.qml b/modules/bar/popouts/PopoutState.qml -new file mode 100644 -index 00000000..6be8169b ---- /dev/null -+++ b/modules/bar/popouts/PopoutState.qml -@@ -0,0 +1,8 @@ -+import QtQuick -+ -+QtObject { -+ property string currentName -+ property bool hasCurrent -+ -+ signal detachRequested(mode: string) -+} -diff --git a/modules/bar/popouts/TrayMenu.qml b/modules/bar/popouts/TrayMenu.qml -index 9b743db1..7975d1bf 100644 ---- a/modules/bar/popouts/TrayMenu.qml -+++ b/modules/bar/popouts/TrayMenu.qml -@@ -1,21 +1,21 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import Quickshell --import Quickshell.Widgets - import QtQuick - import QtQuick.Controls -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components -+import qs.services - - StackView { - id: root - -- required property Item popouts -+ required property PopoutState popouts - required property QsMenuHandle trayItem - -- implicitWidth: currentItem.implicitWidth -- implicitHeight: currentItem.implicitHeight -+ implicitWidth: currentItem?.implicitWidth ?? 0 -+ implicitHeight: currentItem?.implicitHeight ?? 0 - - initialItem: SubMenu { - handle: root.trayItem -@@ -26,6 +26,12 @@ StackView { - popEnter: NoAnim {} - popExit: NoAnim {} - -+ Component { -+ id: subMenuComp -+ -+ SubMenu {} -+ } -+ - component NoAnim: Transition { - NumberAnimation { - duration: 0 -@@ -39,8 +45,8 @@ StackView { - property bool isSubMenu - property bool shown - -- padding: Appearance.padding.smaller -- spacing: Appearance.spacing.small -+ padding: Tokens.padding.smaller -+ spacing: Tokens.spacing.small - - opacity: shown ? 1 : 0 - scale: shown ? 1 : 0.8 -@@ -72,15 +78,16 @@ StackView { - - required property QsMenuEntry modelData - -- implicitWidth: Config.bar.sizes.trayMenuWidth -+ implicitWidth: Tokens.sizes.bar.trayMenuWidth - implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: modelData.isSeparator ? Colours.palette.m3outlineVariant : "transparent" - - Loader { - id: children - -+ asynchronous: true - anchors.left: parent.left - anchors.right: parent.right - -@@ -90,14 +97,14 @@ StackView { - implicitHeight: label.implicitHeight - - StateLayer { -- anchors.margins: -Appearance.padding.small / 2 -- anchors.leftMargin: -Appearance.padding.smaller -- anchors.rightMargin: -Appearance.padding.smaller -+ anchors.margins: -Tokens.padding.small / 2 -+ anchors.leftMargin: -Tokens.padding.smaller -+ anchors.rightMargin: -Tokens.padding.smaller - - radius: item.radius - disabled: !item.modelData.enabled - -- function onClicked(): void { -+ onClicked: { - const entry = item.modelData; - if (entry.hasChildren) - root.push(subMenuComp.createObject(null, { -@@ -114,11 +121,13 @@ StackView { - Loader { - id: icon - -+ asynchronous: true - anchors.left: parent.left - - active: item.modelData.icon !== "" - - sourceComponent: IconImage { -+ asynchronous: true - implicitSize: label.implicitHeight - - source: item.modelData.icon -@@ -129,7 +138,7 @@ StackView { - id: label - - anchors.left: icon.right -- anchors.leftMargin: icon.active ? Appearance.spacing.smaller : 0 -+ anchors.leftMargin: icon.active ? Tokens.spacing.smaller : 0 - - text: labelMetrics.elidedText - color: item.modelData.enabled ? Colours.palette.m3onSurface : Colours.palette.m3outline -@@ -143,12 +152,13 @@ StackView { - font.family: label.font.family - - elide: Text.ElideRight -- elideWidth: Config.bar.sizes.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + Appearance.spacing.normal : 0) -+ elideWidth: root.Tokens.sizes.bar.trayMenuWidth - (icon.active ? icon.implicitWidth + label.anchors.leftMargin : 0) - (expand.active ? expand.implicitWidth + root.Tokens.spacing.normal : 0) - } - - Loader { - id: expand - -+ asynchronous: true - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - -@@ -165,11 +175,12 @@ StackView { - } - - Loader { -+ asynchronous: true - active: menu.isSubMenu - - sourceComponent: Item { - implicitWidth: back.implicitWidth -- implicitHeight: back.implicitHeight + Appearance.spacing.small / 2 -+ implicitHeight: back.implicitHeight + Tokens.spacing.small / 2 - - Item { - anchors.bottom: parent.bottom -@@ -178,20 +189,17 @@ StackView { - - StyledRect { - anchors.fill: parent -- anchors.margins: -Appearance.padding.small / 2 -- anchors.leftMargin: -Appearance.padding.smaller -- anchors.rightMargin: -Appearance.padding.smaller * 2 -+ anchors.margins: -Tokens.padding.small / 2 -+ anchors.leftMargin: -Tokens.padding.smaller -+ anchors.rightMargin: -Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3secondaryContainer - - StateLayer { - radius: parent.radius - color: Colours.palette.m3onSecondaryContainer -- -- function onClicked(): void { -- root.pop(); -- } -+ onClicked: root.pop() - } - } - -@@ -216,10 +224,4 @@ StackView { - } - } - } -- -- Component { -- id: subMenuComp -- -- SubMenu {} -- } - } -diff --git a/modules/bar/popouts/WirelessPassword.qml b/modules/bar/popouts/WirelessPassword.qml -index 96639e71..f6a635fe 100644 ---- a/modules/bar/popouts/WirelessPassword.qml -+++ b/modules/bar/popouts/WirelessPassword.qml -@@ -1,27 +1,100 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - -- required property Item wrapper -+ required property PopoutState popouts - property var network: null - property bool isClosing: false - -- readonly property bool shouldBeVisible: root.wrapper.currentName === "wirelesspassword" -+ readonly property bool shouldBeVisible: root.popouts.currentName === "wirelesspassword" -+ -+ function checkConnectionStatus(): void { -+ if (!root.shouldBeVisible || !connectButton.connecting) { -+ return; -+ } -+ -+ // Check if we're connected to the target network (case-insensitive SSID comparison) -+ const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); -+ -+ if (isConnected) { -+ // Successfully connected - give it a moment for network list to update -+ // Use Timer for actual delay -+ connectionSuccessTimer.start(); -+ return; -+ } -+ -+ // Check for connection failures - if pending connection was cleared but we're not connected -+ if (Nmcli.pendingConnection === null && connectButton.connecting) { -+ // Wait a bit more before giving up (allow time for connection to establish) -+ if (connectionMonitor.repeatCount > 10) { -+ connectionMonitor.stop(); -+ connectButton.connecting = false; -+ connectButton.hasError = true; -+ connectButton.enabled = true; -+ connectButton.text = qsTr("Connect"); -+ passwordContainer.passwordBuffer = ""; -+ // Delete the failed connection -+ if (root.network && root.network.ssid) { -+ Nmcli.forgetNetwork(root.network.ssid); -+ } -+ } -+ } -+ } -+ -+ function closeDialog(): void { -+ if (isClosing) { -+ return; -+ } -+ -+ isClosing = true; -+ passwordContainer.passwordBuffer = ""; -+ connectButton.connecting = false; -+ connectButton.hasError = false; -+ connectButton.text = qsTr("Connect"); -+ connectionMonitor.stop(); -+ -+ // Return to network popout -+ if (root.popouts.currentName === "wirelesspassword") { -+ root.popouts.currentName = "network"; -+ } -+ } -+ -+ spacing: Tokens.spacing.normal -+ implicitWidth: 400 -+ implicitHeight: content.implicitHeight + Tokens.padding.large * 2 -+ visible: shouldBeVisible || isClosing -+ enabled: shouldBeVisible && !isClosing -+ focus: enabled -+ -+ Component.onCompleted: { -+ if (shouldBeVisible) { -+ // Use Timer for actual delay to ensure dialog is fully rendered -+ focusTimer.start(); -+ } -+ } -+ -+ onShouldBeVisibleChanged: { -+ if (shouldBeVisible) { -+ // Use Timer for actual delay to ensure dialog is fully rendered -+ focusTimer.start(); -+ } -+ } -+ -+ Keys.onEscapePressed: closeDialog() - - Connections { -- target: root.wrapper - function onCurrentNameChanged() { -- if (root.wrapper.currentName === "wirelesspassword") { -+ if (root.popouts.currentName === "wirelesspassword") { - // Update network when popout becomes active - Qt.callLater(() => { - // Try to get network from parent Content's networkPopout -@@ -38,10 +111,13 @@ ColumnLayout { - }); - } - } -+ -+ target: root.popouts - } - - Timer { - id: focusTimer -+ - interval: 150 - onTriggered: { - root.forceActiveFocus(); -@@ -49,41 +125,16 @@ ColumnLayout { - } - } - -- spacing: Appearance.spacing.normal -- -- implicitWidth: 400 -- implicitHeight: content.implicitHeight + Appearance.padding.large * 2 -- -- visible: shouldBeVisible || isClosing -- enabled: shouldBeVisible && !isClosing -- focus: enabled -- -- Component.onCompleted: { -- if (shouldBeVisible) { -- // Use Timer for actual delay to ensure dialog is fully rendered -- focusTimer.start(); -- } -- } -- -- onShouldBeVisibleChanged: { -- if (shouldBeVisible) { -- // Use Timer for actual delay to ensure dialog is fully rendered -- focusTimer.start(); -- } -- } -- -- Keys.onEscapePressed: closeDialog() -- - StyledRect { - Layout.fillWidth: true - Layout.preferredWidth: 400 -- implicitHeight: content.implicitHeight + Appearance.padding.large * 2 -- -- radius: Appearance.rounding.normal -+ implicitHeight: content.implicitHeight + Tokens.padding.large * 2 -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - visible: root.shouldBeVisible || root.isClosing - opacity: root.shouldBeVisible && !root.isClosing ? 1 : 0 - scale: root.shouldBeVisible && !root.isClosing ? 1 : 0.7 -+ Keys.onEscapePressed: root.closeDialog() - - Behavior on opacity { - Anim {} -@@ -113,33 +164,32 @@ ColumnLayout { - } - } - -- Keys.onEscapePressed: root.closeDialog() -- - ColumnLayout { - id: content - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "lock" -- font.pointSize: Appearance.font.size.extraLarge * 2 -+ font.pointSize: Tokens.font.size.extraLarge * 2 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Enter password") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - StyledText { - id: networkNameText -+ - Layout.alignment: Qt.AlignHCenter - text: { - if (root.network) { -@@ -151,14 +201,15 @@ ColumnLayout { - return qsTr("Network: Unknown"); - } - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - Timer { -+ property int attempts: 0 -+ - interval: 50 - running: root.shouldBeVisible && (!root.network || !root.network.ssid) - repeat: true -- property int attempts: 0 - onTriggered: { - attempts++; - // Keep trying to get network from Network component -@@ -186,7 +237,7 @@ ColumnLayout { - id: statusText - - Layout.alignment: Qt.AlignHCenter -- Layout.topMargin: Appearance.spacing.small -+ Layout.topMargin: Tokens.spacing.small - visible: connectButton.connecting || connectButton.hasError - text: { - if (connectButton.hasError) { -@@ -198,23 +249,30 @@ ColumnLayout { - return ""; - } - color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - font.weight: 400 - wrapMode: Text.WordWrap -- Layout.maximumWidth: parent.width - Appearance.padding.large * 2 -+ Layout.maximumWidth: parent.width - Tokens.padding.large * 2 - } - - FocusScope { - id: passwordContainer -+ -+ property string passwordBuffer: "" -+ - objectName: "passwordContainer" -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - Layout.fillWidth: true -- implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) -- -+ implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) - focus: true - activeFocusOnTab: true - -- property string passwordBuffer: "" -+ Component.onCompleted: { -+ if (root.shouldBeVisible) { -+ // Use Timer for actual delay to ensure focus works correctly -+ passwordFocusTimer.start(); -+ } -+ } - - Keys.onPressed: event => { - // Ensure we have focus when receiving keyboard input -@@ -222,6 +280,11 @@ ColumnLayout { - forceActiveFocus(); - } - -+ if (event.key === Qt.Key_Escape) { -+ event.accepted = false; -+ closeDialog(); -+ } -+ - // Clear error when user starts typing - if (connectButton.hasError && event.text && event.text.length > 0) { - connectButton.hasError = false; -@@ -240,13 +303,16 @@ ColumnLayout { - } - event.accepted = true; - } else if (event.text && event.text.length > 0) { -+ if (event.key === Qt.Key_Tab) { -+ event.accepted = false; -+ return; -+ } - passwordBuffer += event.text; - event.accepted = true; - } - } - - Connections { -- target: root - function onShouldBeVisibleChanged(): void { - if (root.shouldBeVisible) { - // Use Timer for actual delay to ensure focus works correctly -@@ -255,26 +321,22 @@ ColumnLayout { - connectButton.hasError = false; - } - } -+ -+ target: root - } - - Timer { - id: passwordFocusTimer -+ - interval: 50 - onTriggered: { - passwordContainer.forceActiveFocus(); - } - } - -- Component.onCompleted: { -- if (root.shouldBeVisible) { -- // Use Timer for actual delay to ensure focus works correctly -- passwordFocusTimer.start(); -- } -- } -- - StyledRect { - anchors.fill: parent -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.shouldBeVisible ? 1 : 0) - border.color: { -@@ -303,11 +365,8 @@ ColumnLayout { - StateLayer { - hoverEnabled: false - cursorShape: Qt.IBeamCursor -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -- passwordContainer.forceActiveFocus(); -- } -+ radius: Tokens.rounding.normal -+ onClicked: passwordContainer.forceActiveFocus() - } - - StyledText { -@@ -316,8 +375,8 @@ ColumnLayout { - anchors.centerIn: parent - text: qsTr("Password") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.normal -+ font.family: Tokens.font.family.mono - opacity: passwordContainer.passwordBuffer ? 0 : 1 - - Behavior on opacity { -@@ -332,10 +391,10 @@ ColumnLayout { - - anchors.centerIn: parent - implicitWidth: fullWidth -- implicitHeight: Appearance.font.size.normal -+ implicitHeight: Tokens.font.size.normal - - orientation: Qt.Horizontal -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - interactive: false - - model: ScriptModel { -@@ -349,7 +408,7 @@ ColumnLayout { - implicitHeight: charList.implicitHeight - - color: Colours.palette.m3onSurface -- radius: Appearance.rounding.small / 2 -+ radius: Tokens.rounding.small / 2 - - opacity: 0 - scale: 0 -@@ -392,8 +451,7 @@ ColumnLayout { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -@@ -405,15 +463,15 @@ ColumnLayout { - } - - RowLayout { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - TextButton { - id: cancelButton - - Layout.fillWidth: true -- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 -+ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - text: qsTr("Cancel") -@@ -428,7 +486,7 @@ ColumnLayout { - property bool hasError: false - - Layout.fillWidth: true -- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 -+ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - inactiveColour: Colours.palette.m3primary - inactiveOnColour: Colours.palette.m3onPrimary - text: qsTr("Connect") -@@ -491,45 +549,14 @@ ColumnLayout { - } - } - -- function checkConnectionStatus(): void { -- if (!root.shouldBeVisible || !connectButton.connecting) { -- return; -- } -- -- // Check if we're connected to the target network (case-insensitive SSID comparison) -- const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); -- -- if (isConnected) { -- // Successfully connected - give it a moment for network list to update -- // Use Timer for actual delay -- connectionSuccessTimer.start(); -- return; -- } -- -- // Check for connection failures - if pending connection was cleared but we're not connected -- if (Nmcli.pendingConnection === null && connectButton.connecting) { -- // Wait a bit more before giving up (allow time for connection to establish) -- if (connectionMonitor.repeatCount > 10) { -- connectionMonitor.stop(); -- connectButton.connecting = false; -- connectButton.hasError = true; -- connectButton.enabled = true; -- connectButton.text = qsTr("Connect"); -- passwordContainer.passwordBuffer = ""; -- // Delete the failed connection -- if (root.network && root.network.ssid) { -- Nmcli.forgetNetwork(root.network.ssid); -- } -- } -- } -- } -- - Timer { - id: connectionMonitor -+ -+ property int repeatCount: 0 -+ - interval: 1000 - repeat: true - triggeredOnStart: false -- property int repeatCount: 0 - - onTriggered: { - repeatCount++; -@@ -545,6 +572,7 @@ ColumnLayout { - - Timer { - id: connectionSuccessTimer -+ - interval: 500 - onTriggered: { - // Double-check connection is still active -@@ -555,8 +583,8 @@ ColumnLayout { - connectButton.connecting = false; - connectButton.text = qsTr("Connect"); - // Return to network popout on successful connection -- if (root.wrapper.currentName === "wirelesspassword") { -- root.wrapper.currentName = "network"; -+ if (root.popouts.currentName === "wirelesspassword") { -+ root.popouts.currentName = "network"; - } - closeDialog(); - } -@@ -565,12 +593,12 @@ ColumnLayout { - } - - Connections { -- target: Nmcli - function onActiveChanged() { - if (root.shouldBeVisible) { - root.checkConnectionStatus(); - } - } -+ - function onConnectionFailed(ssid: string) { - if (root.shouldBeVisible && root.network && root.network.ssid === ssid && connectButton.connecting) { - connectionMonitor.stop(); -@@ -583,23 +611,7 @@ ColumnLayout { - Nmcli.forgetNetwork(ssid); - } - } -- } -- -- function closeDialog(): void { -- if (isClosing) { -- return; -- } -- -- isClosing = true; -- passwordContainer.passwordBuffer = ""; -- connectButton.connecting = false; -- connectButton.hasError = false; -- connectButton.text = qsTr("Connect"); -- connectionMonitor.stop(); - -- // Return to network popout -- if (root.wrapper.currentName === "wirelesspassword") { -- root.wrapper.currentName = "network"; -- } -+ target: Nmcli - } - } -diff --git a/modules/bar/popouts/Wrapper.qml b/modules/bar/popouts/Wrapper.qml -index 05a1d3c9..7a66d3b9 100644 ---- a/modules/bar/popouts/Wrapper.qml -+++ b/modules/bar/popouts/Wrapper.qml -@@ -1,57 +1,66 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Quickshell.Hyprland -+import Quickshell.Wayland -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import qs.modules.windowinfo - import qs.modules.controlcenter --import Quickshell --import Quickshell.Wayland --import Quickshell.Hyprland --import QtQuick -+import qs.modules.windowinfo - - Item { - id: root - - required property ShellScreen screen -+ required property real offsetScale -+ -+ readonly property alias content: content -+ readonly property alias winfo: winfo -+ readonly property alias controlCenter: controlCenter - -- readonly property real nonAnimWidth: x > 0 || hasCurrent ? children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth : 0 -+ readonly property real nonAnimWidth: children.find(c => c.shouldBeActive)?.implicitWidth ?? content.implicitWidth - readonly property real nonAnimHeight: children.find(c => c.shouldBeActive)?.implicitHeight ?? content.implicitHeight -- readonly property Item current: content.item?.current ?? null -+ readonly property Item current: (content.item as Content)?.current ?? null -+ readonly property bool isDetached: detachedMode.length > 0 - -- property string currentName -+ property alias currentName: popoutState.currentName -+ property alias hasCurrent: popoutState.hasCurrent - property real currentCenter -- property bool hasCurrent - - property string detachedMode - property string queuedMode -- readonly property bool isDetached: detachedMode.length > 0 - -- property int animLength: Appearance.anim.durations.normal -- property list animCurve: Appearance.anim.curves.emphasized -+ // Dummy object so Tokens attached prop resolves to global config -+ // Anim configs are not per-monitor -+ readonly property QtObject dummy: QtObject {} -+ property int animLength: dummy.Tokens.anim.durations.expressiveDefaultSpatial -+ property var animCurve: dummy.Tokens.anim.expressiveDefaultSpatial // The easingCurve type is Qt 6.11+ so we gotta use var for now -+ -+ function setAnims(detach: bool): void { -+ const type = `expressive${detach ? "Slow" : "Default"}Spatial`; -+ animLength = dummy.Tokens.anim.durations[type]; -+ animCurve = dummy.Tokens.anim[type]; -+ } - - function detach(mode: string): void { -- animLength = Appearance.anim.durations.large; -+ setAnims(true); - if (mode === "winfo") { - detachedMode = mode; - } else { - queuedMode = mode; - detachedMode = "any"; - } -+ setAnims(false); - focus = true; - } - - function close(): void { - hasCurrent = false; -- animCurve = Appearance.anim.curves.emphasizedAccel; -- animLength = Appearance.anim.durations.normal; - detachedMode = ""; -- animCurve = Appearance.anim.curves.emphasized; - } - -- visible: width > 0 && height > 0 -- clip: true -- - implicitWidth: nonAnimWidth - implicitHeight: nonAnimHeight - -@@ -59,7 +68,7 @@ Item { - Keys.onEscapePressed: { - // Forward escape to password popout if active, otherwise close - if (currentName === "wirelesspassword" && content.item) { -- const passwordPopout = content.item.children.find(c => c.name === "wirelesspassword"); -+ const passwordPopout = (content.item as Content)?.children.find(c => c.name === "wirelesspassword"); - if (passwordPopout && passwordPopout.item) { - passwordPopout.item.closeDialog(); - return; -@@ -75,6 +84,12 @@ Item { - } - } - -+ PopoutState { -+ id: popoutState -+ -+ onDetachRequested: mode => root.detach(mode) -+ } -+ - HyprlandFocusGrab { - active: root.isDetached - windows: [QsWindow.window] -@@ -82,15 +97,7 @@ Item { - } - - Binding { -- when: root.isDetached -- -- target: QsWindow.window -- property: "WlrLayershell.keyboardFocus" -- value: WlrKeyboardFocus.OnDemand -- } -- -- Binding { -- when: root.hasCurrent && root.currentName === "wirelesspassword" -+ when: root.isDetached || (root.hasCurrent && root.currentName === "wirelesspassword") - - target: QsWindow.window - property: "WlrLayershell.keyboardFocus" -@@ -101,15 +108,16 @@ Item { - id: content - - shouldBeActive: root.hasCurrent && !root.detachedMode -- anchors.right: parent.right -- anchors.verticalCenter: parent.verticalCenter -+ anchors.fill: parent - - sourceComponent: Content { -- wrapper: root -+ popouts: popoutState - } - } - - Comp { -+ id: winfo -+ - shouldBeActive: root.detachedMode === "winfo" - anchors.centerIn: parent - -@@ -120,48 +128,31 @@ Item { - } - - Comp { -+ id: controlCenter -+ - shouldBeActive: root.detachedMode === "any" - anchors.centerIn: parent - - sourceComponent: ControlCenter { - screen: root.screen - active: root.queuedMode -- -- function close(): void { -- root.close(); -- } -- } -- } -- -- Behavior on x { -- Anim { -- duration: root.animLength -- easing.bezierCurve: root.animCurve -- } -- } -- -- Behavior on y { -- enabled: root.implicitWidth > 0 -- -- Anim { -- duration: root.animLength -- easing.bezierCurve: root.animCurve -+ onClose: root.close() - } - } - - Behavior on implicitWidth { - Anim { - duration: root.animLength -- easing.bezierCurve: root.animCurve -+ easing: root.animCurve - } - } - - Behavior on implicitHeight { -- enabled: root.implicitWidth > 0 -+ enabled: root.offsetScale < 1 - - Anim { - duration: root.animLength -- easing.bezierCurve: root.animCurve -+ easing: root.animCurve - } - } - -@@ -173,6 +164,7 @@ Item { - active: false - opacity: 0 - -+ // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set - states: State { - name: "active" - when: comp.shouldBeActive -diff --git a/modules/bar/popouts/kblayout/KbLayout.qml b/modules/bar/popouts/kblayout/KbLayout.qml -index 94b6f7ec..d8f9a462 100644 ---- a/modules/bar/popouts/kblayout/KbLayout.qml -+++ b/modules/bar/popouts/kblayout/KbLayout.qml -@@ -3,51 +3,47 @@ pragma ComponentBehavior: Bound - import QtQuick - import QtQuick.Controls - import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.services --import qs.config --import qs.utils -- --import "." - - ColumnLayout { - id: root - -- required property Item wrapper -+ function refresh() { -+ kb.refresh(); -+ } -+ -+ spacing: Tokens.spacing.small -+ width: Tokens.sizes.bar.kbLayoutWidth - -- spacing: Appearance.spacing.small -- width: Config.bar.sizes.kbLayoutWidth -+ Component.onCompleted: kb.start() - - KbLayoutModel { - id: kb - } - -- function refresh() { -- kb.refresh(); -- } -- Component.onCompleted: kb.start() -- - StyledText { -- Layout.topMargin: Appearance.padding.normal -- Layout.rightMargin: Appearance.padding.small -+ Layout.topMargin: Tokens.padding.normal -+ Layout.rightMargin: Tokens.padding.small - text: qsTr("Keyboard Layouts") - font.weight: 500 - } - - ListView { - id: list -+ - model: kb.visibleModel - - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- Layout.topMargin: Appearance.spacing.small -+ Layout.rightMargin: Tokens.padding.small -+ Layout.topMargin: Tokens.spacing.small - - clip: true - interactive: true - implicitHeight: Math.min(contentHeight, 320) - visible: kb.visibleModel.count > 0 -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - add: Transition { - NumberAnimation { -@@ -85,54 +81,55 @@ ColumnLayout { - } - - delegate: Item { -+ id: kbDelegate -+ - required property int layoutIndex - required property string label -+ readonly property bool isDisabled: layoutIndex > 3 - - width: list.width -- height: Math.max(36, rowText.implicitHeight + Appearance.padding.small * 2) -- -- readonly property bool isDisabled: layoutIndex > 3 -+ height: Math.max(36, rowText.implicitHeight + Tokens.padding.small * 2) -+ ToolTip.visible: isDisabled && layer.containsMouse -+ ToolTip.text: "XKB limitation: maximum 4 layouts allowed" - - StateLayer { - id: layer -+ -+ onClicked: { -+ if (!kbDelegate.isDisabled) -+ kb.switchTo(kbDelegate.layoutIndex); -+ } -+ - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - implicitHeight: parent.height - 4 -- -- radius: Appearance.rounding.full -- enabled: !isDisabled -- -- function onClicked(): void { -- if (!isDisabled) -- kb.switchTo(layoutIndex); -- } -+ radius: Tokens.rounding.full -+ enabled: !kbDelegate.isDisabled - } - - StyledText { - id: rowText -+ - anchors.verticalCenter: layer.verticalCenter - anchors.left: layer.left - anchors.right: layer.right -- anchors.leftMargin: Appearance.padding.small -- anchors.rightMargin: Appearance.padding.small -- text: label -+ anchors.leftMargin: Tokens.padding.small -+ anchors.rightMargin: Tokens.padding.small -+ text: kbDelegate.label - elide: Text.ElideRight -- opacity: isDisabled ? 0.4 : 1.0 -+ opacity: kbDelegate.isDisabled ? 0.4 : 1.0 - } -- -- ToolTip.visible: isDisabled && layer.containsMouse -- ToolTip.text: "XKB limitation: maximum 4 layouts allowed" - } - } - - Rectangle { - visible: kb.activeLabel.length > 0 - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- Layout.topMargin: Appearance.spacing.small -+ Layout.rightMargin: Tokens.padding.small -+ Layout.topMargin: Tokens.spacing.small - -- height: 1 -+ implicitHeight: 1 - color: Colours.palette.m3onSurfaceVariant - opacity: 0.35 - } -@@ -142,9 +139,9 @@ ColumnLayout { - - visible: kb.activeLabel.length > 0 - Layout.fillWidth: true -- Layout.rightMargin: Appearance.padding.small -- Layout.topMargin: Appearance.spacing.small -- spacing: Appearance.spacing.small -+ Layout.rightMargin: Tokens.padding.small -+ Layout.topMargin: Tokens.spacing.small -+ spacing: Tokens.spacing.small - - opacity: 1 - scale: 1 -@@ -163,16 +160,18 @@ ColumnLayout { - } - - Connections { -- target: kb - function onActiveLabelChanged() { - if (!activeRow.visible) - return; - popIn.restart(); - } -+ -+ target: kb - } - - SequentialAnimation { - id: popIn -+ - running: false - - ParallelAnimation { -diff --git a/modules/bar/popouts/kblayout/KbLayoutModel.qml b/modules/bar/popouts/kblayout/KbLayoutModel.qml -index 43710953..f62d0b98 100644 ---- a/modules/bar/popouts/kblayout/KbLayoutModel.qml -+++ b/modules/bar/popouts/kblayout/KbLayoutModel.qml -@@ -1,24 +1,20 @@ - pragma ComponentBehavior: Bound - - import QtQuick -- --import Quickshell - import Quickshell.Io -- --import qs.config - import Caelestia -+import Caelestia.Config -+ -+// TODO: handle this better later - - Item { - id: model -- visible: false - -- ListModel { -- id: _visibleModel -- } - property alias visibleModel: _visibleModel -- - property string activeLabel: "" - property int activeIndex: -1 -+ property var _xkbMap: ({}) -+ property bool _notifiedLimit: false - - function start() { - _xkbXmlBase.running = true; -@@ -35,31 +31,6 @@ Item { - _switchProc.running = true; - } - -- ListModel { -- id: _layoutsModel -- } -- -- property var _xkbMap: ({}) -- property bool _notifiedLimit: false -- -- Process { -- id: _xkbXmlBase -- command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] -- stdout: StdioCollector { -- onStreamFinished: _buildXmlMap(text) -- } -- onRunningChanged: if (!running && (typeof exitCode !== "undefined") && exitCode !== 0) -- _xkbXmlEvdev.running = true -- } -- -- Process { -- id: _xkbXmlEvdev -- command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] -- stdout: StdioCollector { -- onStreamFinished: _buildXmlMap(text) -- } -- } -- - function _buildXmlMap(xml) { - const map = {}; - -@@ -105,8 +76,84 @@ Item { - return `${lang} (${code})`; - } - -+ function _setLayouts(raw) { -+ const parts = raw.split(",").map(s => s.trim()).filter(Boolean); -+ _layoutsModel.clear(); -+ -+ const seen = new Set(); -+ let idx = 0; -+ -+ for (const p of parts) { -+ if (seen.has(p)) -+ continue; -+ seen.add(p); -+ _layoutsModel.append({ -+ layoutIndex: idx, -+ token: p, -+ label: _pretty(p) -+ }); -+ idx++; -+ } -+ } -+ -+ function _rebuildVisible() { -+ _visibleModel.clear(); -+ -+ let arr = []; -+ for (let i = 0; i < _layoutsModel.count; i++) -+ arr.push(_layoutsModel.get(i)); -+ -+ arr = arr.filter(i => i.layoutIndex !== activeIndex); -+ arr.forEach(i => _visibleModel.append(i)); -+ -+ if (!GlobalConfig.utilities.toasts.kbLimit) -+ return; -+ -+ if (_layoutsModel.count > 4) { -+ Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); -+ } -+ } -+ -+ function _pretty(token) { -+ const code = token.replace(/\(.*\)$/, "").trim(); -+ if (_xkbMap[code]) -+ return code.toUpperCase() + " - " + _xkbMap[code]; -+ return code.toUpperCase() + " - " + code; -+ } -+ -+ visible: false -+ -+ ListModel { -+ id: _visibleModel -+ } -+ -+ ListModel { -+ id: _layoutsModel -+ } -+ -+ Process { -+ id: _xkbXmlBase -+ -+ command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/base.xml"] -+ stdout: StdioCollector { -+ onStreamFinished: model._buildXmlMap(text) -+ } -+ onRunningChanged: if (!running && (typeof _xkbXmlBase.exitCode !== "undefined") && _xkbXmlBase.exitCode !== 0) // qmllint disable missing-property -+ _xkbXmlEvdev.running = true -+ } -+ -+ Process { -+ id: _xkbXmlEvdev -+ -+ command: ["xmllint", "--xpath", "//layout/configItem[name and description]", "/usr/share/X11/xkb/rules/evdev.xml"] -+ stdout: StdioCollector { -+ onStreamFinished: model._buildXmlMap(text) -+ } -+ } -+ - Process { - id: _getKbLayoutOpt -+ - command: ["hyprctl", "-j", "getoption", "input:kb_layout"] - stdout: StdioCollector { - onStreamFinished: { -@@ -114,7 +161,7 @@ Item { - const j = JSON.parse(text); - const raw = (j?.str || j?.value || "").toString().trim(); - if (raw.length) { -- _setLayouts(raw); -+ model._setLayouts(raw); - _fetchActiveLayouts.running = true; - return; - } -@@ -126,6 +173,7 @@ Item { - - Process { - id: _fetchLayoutsFromDevices -+ - command: ["hyprctl", "-j", "devices"] - stdout: StdioCollector { - onStreamFinished: { -@@ -134,7 +182,7 @@ Item { - const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; - const raw = (kb?.layout || "").trim(); - if (raw.length) -- _setLayouts(raw); -+ model._setLayouts(raw); - } catch (e) {} - _fetchActiveLayouts.running = true; - } -@@ -143,6 +191,7 @@ Item { - - Process { - id: _fetchActiveLayouts -+ - command: ["hyprctl", "-j", "devices"] - stdout: StdioCollector { - onStreamFinished: { -@@ -151,66 +200,22 @@ Item { - const kb = dev?.keyboards?.find(k => k.main) || dev?.keyboards?.[0]; - const idx = kb?.active_layout_index ?? -1; - -- activeIndex = idx >= 0 ? idx : -1; -- activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; -+ model.activeIndex = idx >= 0 ? idx : -1; -+ model.activeLabel = (idx >= 0 && idx < _layoutsModel.count) ? _layoutsModel.get(idx).label : ""; - } catch (e) { -- activeIndex = -1; -- activeLabel = ""; -+ model.activeIndex = -1; -+ model.activeLabel = ""; - } - -- _rebuildVisible(); -+ model._rebuildVisible(); - } - } - } - - Process { - id: _switchProc -+ - onRunningChanged: if (!running) - _fetchActiveLayouts.running = true - } -- -- function _setLayouts(raw) { -- const parts = raw.split(",").map(s => s.trim()).filter(Boolean); -- _layoutsModel.clear(); -- -- const seen = new Set(); -- let idx = 0; -- -- for (const p of parts) { -- if (seen.has(p)) -- continue; -- seen.add(p); -- _layoutsModel.append({ -- layoutIndex: idx, -- token: p, -- label: _pretty(p) -- }); -- idx++; -- } -- } -- -- function _rebuildVisible() { -- _visibleModel.clear(); -- -- let arr = []; -- for (let i = 0; i < _layoutsModel.count; i++) -- arr.push(_layoutsModel.get(i)); -- -- arr = arr.filter(i => i.layoutIndex !== activeIndex); -- arr.forEach(i => _visibleModel.append(i)); -- -- if (!Config.utilities.toasts.kbLimit) -- return; -- -- if (_layoutsModel.count > 4) { -- Toaster.toast(qsTr("Keyboard layout limit"), qsTr("XKB supports only 4 layouts at a time"), "warning"); -- } -- } -- -- function _pretty(token) { -- const code = token.replace(/\(.*\)$/, "").trim(); -- if (_xkbMap[code]) -- return code.toUpperCase() + " - " + _xkbMap[code]; -- return code.toUpperCase() + " - " + code; -- } - } -diff --git a/modules/controlcenter/ControlCenter.qml b/modules/controlcenter/ControlCenter.qml -index 4aacfad9..c8b435ca 100644 ---- a/modules/controlcenter/ControlCenter.qml -+++ b/modules/controlcenter/ControlCenter.qml -@@ -1,34 +1,34 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config --import Quickshell --import QtQuick --import QtQuick.Layouts - - Item { - id: root - - required property ShellScreen screen -- readonly property int rounding: floating ? 0 : Appearance.rounding.normal -+ readonly property int rounding: floating ? 0 : Tokens.rounding.large - - property alias floating: session.floating - property alias active: session.active - property alias navExpanded: session.navExpanded - -+ readonly property bool initialOpeningComplete: panes.initialOpeningComplete - readonly property Session session: Session { - id: session - - root: root - } - -- function close(): void { -- } -+ signal close - -- implicitWidth: implicitHeight * Config.controlCenter.sizes.ratio -- implicitHeight: screen.height * Config.controlCenter.sizes.heightMult -+ implicitWidth: implicitHeight * Tokens.sizes.controlCenter.ratio -+ implicitHeight: screen.height * Tokens.sizes.controlCenter.heightMult - - GridLayout { - anchors.fill: parent -@@ -42,6 +42,7 @@ Item { - Layout.fillWidth: true - Layout.columnSpan: 2 - -+ asynchronous: true - active: root.floating - visible: active - -@@ -60,8 +61,6 @@ Item { - color: Colours.tPalette.m3surfaceContainer - - CustomMouseArea { -- anchors.fill: parent -- - function onWheel(event: WheelEvent): void { - // Prevent tab switching during initial opening animation to avoid blank pages - if (!panes.initialOpeningComplete) { -@@ -73,6 +72,8 @@ Item { - else if (event.angleDelta.y > 0) - root.session.activeIndex = Math.max(root.session.activeIndex - 1, 0); - } -+ -+ anchors.fill: parent - } - - NavRail { -@@ -95,6 +96,4 @@ Item { - session: root.session - } - } -- -- readonly property bool initialOpeningComplete: panes.initialOpeningComplete - } -diff --git a/modules/controlcenter/NavRail.qml b/modules/controlcenter/NavRail.qml -index e61a741a..a126c800 100644 ---- a/modules/controlcenter/NavRail.qml -+++ b/modules/controlcenter/NavRail.qml -@@ -1,12 +1,12 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.services --import qs.config - import qs.modules.controlcenter --import Quickshell --import QtQuick --import QtQuick.Layouts - - Item { - id: root -@@ -15,23 +15,23 @@ Item { - required property Session session - required property bool initialOpeningComplete - -- implicitWidth: layout.implicitWidth + Appearance.padding.larger * 4 -- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 -+ implicitWidth: layout.implicitWidth + Tokens.padding.larger * 4 -+ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - - ColumnLayout { - id: layout - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: Appearance.padding.larger * 2 -- spacing: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.padding.larger * 2 -+ spacing: Tokens.spacing.normal - - states: State { - name: "expanded" - when: root.session.navExpanded - - PropertyChanges { -- layout.spacing: Appearance.spacing.small -+ layout.spacing: root.Tokens.spacing.small - } - } - -@@ -42,7 +42,8 @@ Item { - } - - Loader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large -+ asynchronous: true - active: !root.session.floating - visible: active - -@@ -50,23 +51,23 @@ Item { - readonly property int nonAnimWidth: normalWinIcon.implicitWidth + (root.session.navExpanded ? normalWinLabel.anchors.leftMargin + normalWinLabel.implicitWidth : 0) + normalWinIcon.anchors.leftMargin * 2 - - implicitWidth: nonAnimWidth -- implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Appearance.padding.normal * 2 : nonAnimWidth -+ implicitHeight: root.session.navExpanded ? normalWinIcon.implicitHeight + Tokens.padding.normal * 2 : nonAnimWidth - - color: Colours.palette.m3primaryContainer -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - - StateLayer { - id: normalWinState - -- color: Colours.palette.m3onPrimaryContainer -- -- function onClicked(): void { -+ onClicked: { - root.session.root.close(); - WindowFactory.create(null, { - active: root.session.active, - navExpanded: root.session.navExpanded - }); - } -+ -+ color: Colours.palette.m3onPrimaryContainer - } - - MaterialIcon { -@@ -74,11 +75,11 @@ Item { - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: Appearance.padding.large -+ anchors.leftMargin: Tokens.padding.large - - text: "select_window" - color: Colours.palette.m3onPrimaryContainer -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: 1 - } - -@@ -87,7 +88,7 @@ Item { - - anchors.left: normalWinIcon.right - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.spacing.normal - - text: qsTr("Float window") - color: Colours.palette.m3onPrimaryContainer -@@ -95,22 +96,20 @@ Item { - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } - - Behavior on implicitWidth { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -@@ -121,7 +120,8 @@ Item { - - NavItem { - required property int index -- Layout.topMargin: index === 0 ? Appearance.spacing.large * 2 : 0 -+ -+ Layout.topMargin: index === 0 ? Tokens.spacing.large * 2 : 0 - icon: PaneRegistry.getByIndex(index).icon - label: PaneRegistry.getByIndex(index).label - } -@@ -146,7 +146,7 @@ Item { - expandedLabel.opacity: 1 - smallLabel.opacity: 0 - background.implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 + expandedLabel.anchors.leftMargin + expandedLabel.implicitWidth -- background.implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 -+ background.implicitHeight: icon.implicitHeight + root.Tokens.padding.normal * 2 - item.implicitHeight: background.implicitHeight - } - } -@@ -154,35 +154,34 @@ Item { - transitions: Transition { - Anim { - property: "opacity" -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - - Anim { - properties: "implicitWidth,implicitHeight" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - StyledRect { - id: background - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3secondaryContainer, item.active ? 1 : 0) - - implicitWidth: icon.implicitWidth + icon.anchors.leftMargin * 2 -- implicitHeight: icon.implicitHeight + Appearance.padding.small -+ implicitHeight: icon.implicitHeight + Tokens.padding.small - - StateLayer { -- color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- -- function onClicked(): void { -+ onClicked: { - // Prevent tab switching during initial opening animation to avoid blank pages - if (!root.initialOpeningComplete) { - return; - } - root.session.active = item.label; - } -+ -+ color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - } - - MaterialIcon { -@@ -190,11 +189,11 @@ Item { - - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: Appearance.padding.large -+ anchors.leftMargin: Tokens.padding.large - - text: item.icon - color: item.active ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: item.active ? 1 : 0 - - Behavior on fill { -@@ -207,7 +206,7 @@ Item { - - anchors.left: icon.right - anchors.verticalCenter: parent.verticalCenter -- anchors.leftMargin: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.spacing.normal - - opacity: 0 - text: item.label -@@ -220,10 +219,10 @@ Item { - - anchors.horizontalCenter: icon.horizontalCenter - anchors.top: icon.bottom -- anchors.topMargin: Appearance.spacing.small / 2 -+ anchors.topMargin: Tokens.spacing.small / 2 - - text: item.label -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - font.capitalization: Font.Capitalize - } - } -diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml -index b3e19c81..f26a2aad 100644 ---- a/modules/controlcenter/PaneRegistry.qml -+++ b/modules/controlcenter/PaneRegistry.qml -@@ -36,6 +36,12 @@ QtObject { - readonly property string icon: "task_alt" - readonly property string component: "taskbar/TaskbarPane.qml" - }, -+ QtObject { -+ readonly property string id: "notifications" -+ readonly property string label: "notifications" -+ readonly property string icon: "notifications" -+ readonly property string component: "notifications/NotificationsPane.qml" -+ }, - QtObject { - readonly property string id: "launcher" - readonly property string label: "launcher" -@@ -47,6 +53,12 @@ QtObject { - readonly property string label: "monitors" - readonly property string icon: "monitor" - readonly property string component: "monitors/MonitorsPane.qml" -+ }, -+ QtObject { -+ readonly property string id: "dashboard" -+ readonly property string label: "dashboard" -+ readonly property string icon: "dashboard" -+ readonly property string component: "dashboard/DashboardPane.qml" - } - ] - -@@ -60,7 +72,7 @@ QtObject { - return result; - } - -- function getByIndex(index: int): QtObject { -+ function getByIndex(index: int): var { - if (index >= 0 && index < panes.length) { - return panes[index]; - } -@@ -76,12 +88,12 @@ QtObject { - return -1; - } - -- function getByLabel(label: string): QtObject { -+ function getByLabel(label: string): var { - const index = getIndexByLabel(label); - return getByIndex(index); - } - -- function getById(id: string): QtObject { -+ function getById(id: string): var { - for (let i = 0; i < panes.length; i++) { - if (panes[i].id === id) { - return panes[i]; -diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml -index 90a369a9..e8803011 100644 ---- a/modules/controlcenter/Panes.qml -+++ b/modules/controlcenter/Panes.qml -@@ -5,15 +5,17 @@ import "network" - import "audio" - import "appearance" - import "taskbar" -+import "notifications" - import "launcher" - import "monitors" -+import "dashboard" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components - import qs.services --import qs.config - import qs.modules.controlcenter --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts - - ClippingRectangle { - id: root -@@ -37,26 +39,27 @@ ClippingRectangle { - } - - Connections { -- target: root.session -- - function onActiveIndexChanged(): void { - root.focus = true; - } -+ -+ target: root.session - } - - ColumnLayout { - id: layout - -+ property bool animationComplete: true -+ property bool initialOpeningComplete: false -+ - spacing: 0 - y: -root.session.activeIndex * root.height - clip: true - -- property bool animationComplete: true -- property bool initialOpeningComplete: false -- - Timer { - id: animationDelayTimer -- interval: Appearance.anim.durations.normal -+ -+ interval: Tokens.anim.durations.normal - onTriggered: { - layout.animationComplete = true; - } -@@ -64,7 +67,8 @@ ClippingRectangle { - - Timer { - id: initialOpeningTimer -- interval: Appearance.anim.durations.large -+ -+ interval: Tokens.anim.durations.large - running: true - onTriggered: { - layout.initialOpeningComplete = true; -@@ -76,6 +80,7 @@ ClippingRectangle { - - Pane { - required property int index -+ - paneIndex: index - componentPath: PaneRegistry.getByIndex(index).component - } -@@ -86,11 +91,12 @@ ClippingRectangle { - } - - Connections { -- target: root.session - function onActiveIndexChanged(): void { - layout.animationComplete = false; - animationDelayTimer.restart(); - } -+ -+ target: root.session - } - } - -@@ -99,10 +105,6 @@ ClippingRectangle { - - required property int paneIndex - required property string componentPath -- -- implicitWidth: root.width -- implicitHeight: root.height -- - property bool hasBeenLoaded: false - - function updateActive(): void { -@@ -125,10 +127,14 @@ ClippingRectangle { - loader.active = shouldBeActive; - } - -+ implicitWidth: root.width -+ implicitHeight: root.height -+ - Loader { - id: loader - - anchors.fill: parent -+ asynchronous: true - clip: false - active: false - -@@ -156,20 +162,22 @@ ClippingRectangle { - } - - Connections { -- target: root.session - function onActiveIndexChanged(): void { - pane.updateActive(); - } -+ -+ target: root.session - } - - Connections { -- target: layout - function onInitialOpeningCompleteChanged(): void { - pane.updateActive(); - } - function onAnimationCompleteChanged(): void { - pane.updateActive(); - } -+ -+ target: layout - } - } - } -diff --git a/modules/controlcenter/Session.qml b/modules/controlcenter/Session.qml -index 3678093b..33805609 100644 ---- a/modules/controlcenter/Session.qml -+++ b/modules/controlcenter/Session.qml -@@ -1,5 +1,5 @@ --import QtQuick - import "./state" -+import QtQuick - import qs.modules.controlcenter - - QtObject { -diff --git a/modules/controlcenter/WindowFactory.qml b/modules/controlcenter/WindowFactory.qml -index abcf5df1..dc0dc4a0 100644 ---- a/modules/controlcenter/WindowFactory.qml -+++ b/modules/controlcenter/WindowFactory.qml -@@ -1,9 +1,9 @@ - pragma Singleton - -+import QtQuick -+import Quickshell - import qs.components - import qs.services --import Quickshell --import QtQuick - - Singleton { - id: root -@@ -47,11 +47,8 @@ Singleton { - - anchors.fill: parent - screen: win.screen -+ onClose: win.destroy() - floating: true -- -- function close(): void { -- win.destroy(); -- } - } - - Behavior on color { -diff --git a/modules/controlcenter/WindowTitle.qml b/modules/controlcenter/WindowTitle.qml -index fb716089..8f66968b 100644 ---- a/modules/controlcenter/WindowTitle.qml -+++ b/modules/controlcenter/WindowTitle.qml -@@ -1,8 +1,8 @@ -+import QtQuick -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell --import QtQuick - - StyledRect { - id: root -@@ -10,7 +10,7 @@ StyledRect { - required property ShellScreen screen - required property Session session - -- implicitHeight: text.implicitHeight + Appearance.padding.normal -+ implicitHeight: text.implicitHeight + Tokens.padding.normal - color: Colours.tPalette.m3surfaceContainer - - StyledText { -@@ -21,24 +21,24 @@ StyledRect { - - text: qsTr("Caelestia Settings - %1").arg(root.session.active) - font.capitalization: Font.Capitalize -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - Item { - anchors.right: parent.right - anchors.top: parent.top -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - implicitWidth: implicitHeight -- implicitHeight: closeIcon.implicitHeight + Appearance.padding.small -+ implicitHeight: closeIcon.implicitHeight + Tokens.padding.small - - StateLayer { -- radius: Appearance.rounding.full -- -- function onClicked(): void { -+ onClicked: { - QsWindow.window.destroy(); - } -+ -+ radius: Tokens.rounding.full - } - - MaterialIcon { -diff --git a/modules/controlcenter/appearance/AppearancePane.qml b/modules/controlcenter/appearance/AppearancePane.qml -index 42511677..a1e0e42c 100644 ---- a/modules/controlcenter/appearance/AppearancePane.qml -+++ b/modules/controlcenter/appearance/AppearancePane.qml -@@ -4,26 +4,26 @@ import ".." - import "../components" - import "./sections" - import "../../launcher/services" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config -+import Caelestia.Models - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.components.images - import qs.services --import qs.config - import qs.utils --import Caelestia.Models --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts - - Item { - id: root - - required property Session session - -- property real animDurationsScale: Config.appearance.anim.durations.scale ?? 1 -+ property real animDurationsScale: GlobalConfig.appearance.anim.durations.scale ?? 1 - property string fontFamilyMaterial: Config.appearance.font.family.material ?? "Material Symbols Rounded" - property string fontFamilyMono: Config.appearance.font.family.mono ?? "CaskaydiaCove NF" - property string fontFamilySans: Config.appearance.font.family.sans ?? "Rubik" -@@ -31,9 +31,9 @@ Item { - property real paddingScale: Config.appearance.padding.scale ?? 1 - property real roundingScale: Config.appearance.rounding.scale ?? 1 - property real spacingScale: Config.appearance.spacing.scale ?? 1 -- property bool transparencyEnabled: Config.appearance.transparency.enabled ?? false -- property real transparencyBase: Config.appearance.transparency.base ?? 0.85 -- property real transparencyLayers: Config.appearance.transparency.layers ?? 0.4 -+ property bool transparencyEnabled: GlobalConfig.appearance.transparency.enabled ?? false -+ property real transparencyBase: GlobalConfig.appearance.transparency.base ?? 0.85 -+ property real transparencyLayers: GlobalConfig.appearance.transparency.layers ?? 0.4 - property real borderRounding: Config.border.rounding ?? 1 - property real borderThickness: Config.border.thickness ?? 1 - -@@ -48,52 +48,53 @@ Item { - property bool desktopClockBackgroundBlur: Config.background.desktopClock.background.blur ?? false - property bool desktopClockInvertColors: Config.background.desktopClock.invertColors ?? false - property bool backgroundEnabled: Config.background.enabled ?? true -+ property bool wallpaperEnabled: Config.background.wallpaperEnabled ?? true - property bool visualiserEnabled: Config.background.visualiser.enabled ?? false - property bool visualiserAutoHide: Config.background.visualiser.autoHide ?? true - property real visualiserRounding: Config.background.visualiser.rounding ?? 1 - property real visualiserSpacing: Config.background.visualiser.spacing ?? 1 - -- anchors.fill: parent -- - function saveConfig() { -- Config.appearance.anim.durations.scale = root.animDurationsScale; -- -- Config.appearance.font.family.material = root.fontFamilyMaterial; -- Config.appearance.font.family.mono = root.fontFamilyMono; -- Config.appearance.font.family.sans = root.fontFamilySans; -- Config.appearance.font.size.scale = root.fontSizeScale; -- -- Config.appearance.padding.scale = root.paddingScale; -- Config.appearance.rounding.scale = root.roundingScale; -- Config.appearance.spacing.scale = root.spacingScale; -- -- Config.appearance.transparency.enabled = root.transparencyEnabled; -- Config.appearance.transparency.base = root.transparencyBase; -- Config.appearance.transparency.layers = root.transparencyLayers; -- -- Config.background.desktopClock.enabled = root.desktopClockEnabled; -- Config.background.enabled = root.backgroundEnabled; -- Config.background.desktopClock.scale = root.desktopClockScale; -- Config.background.desktopClock.position = root.desktopClockPosition; -- Config.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; -- Config.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; -- Config.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; -- Config.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; -- Config.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; -- Config.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; -- Config.background.desktopClock.invertColors = root.desktopClockInvertColors; -- -- Config.background.visualiser.enabled = root.visualiserEnabled; -- Config.background.visualiser.autoHide = root.visualiserAutoHide; -- Config.background.visualiser.rounding = root.visualiserRounding; -- Config.background.visualiser.spacing = root.visualiserSpacing; -- -- Config.border.rounding = root.borderRounding; -- Config.border.thickness = root.borderThickness; -- -- Config.save(); -+ GlobalConfig.appearance.anim.durations.scale = root.animDurationsScale; -+ -+ GlobalConfig.appearance.font.family.material = root.fontFamilyMaterial; -+ GlobalConfig.appearance.font.family.mono = root.fontFamilyMono; -+ GlobalConfig.appearance.font.family.sans = root.fontFamilySans; -+ GlobalConfig.appearance.font.size.scale = root.fontSizeScale; -+ -+ GlobalConfig.appearance.padding.scale = root.paddingScale; -+ GlobalConfig.appearance.rounding.scale = root.roundingScale; -+ GlobalConfig.appearance.spacing.scale = root.spacingScale; -+ -+ GlobalConfig.appearance.transparency.enabled = root.transparencyEnabled; -+ GlobalConfig.appearance.transparency.base = root.transparencyBase; -+ GlobalConfig.appearance.transparency.layers = root.transparencyLayers; -+ -+ GlobalConfig.background.desktopClock.enabled = root.desktopClockEnabled; -+ GlobalConfig.background.enabled = root.backgroundEnabled; -+ GlobalConfig.background.desktopClock.scale = root.desktopClockScale; -+ GlobalConfig.background.desktopClock.position = root.desktopClockPosition; -+ GlobalConfig.background.desktopClock.shadow.enabled = root.desktopClockShadowEnabled; -+ GlobalConfig.background.desktopClock.shadow.opacity = root.desktopClockShadowOpacity; -+ GlobalConfig.background.desktopClock.shadow.blur = root.desktopClockShadowBlur; -+ GlobalConfig.background.desktopClock.background.enabled = root.desktopClockBackgroundEnabled; -+ GlobalConfig.background.desktopClock.background.opacity = root.desktopClockBackgroundOpacity; -+ GlobalConfig.background.desktopClock.background.blur = root.desktopClockBackgroundBlur; -+ GlobalConfig.background.desktopClock.invertColors = root.desktopClockInvertColors; -+ -+ GlobalConfig.background.wallpaperEnabled = root.wallpaperEnabled; -+ -+ GlobalConfig.background.visualiser.enabled = root.visualiserEnabled; -+ GlobalConfig.background.visualiser.autoHide = root.visualiserAutoHide; -+ GlobalConfig.background.visualiser.rounding = root.visualiserRounding; -+ GlobalConfig.background.visualiser.spacing = root.visualiserSpacing; -+ -+ GlobalConfig.border.rounding = root.borderRounding; -+ GlobalConfig.border.thickness = root.borderThickness; - } - -+ anchors.fill: parent -+ - Component { - id: appearanceRightContentComponent - -@@ -108,9 +109,9 @@ Item { - - StyledText { - Layout.alignment: Qt.AlignHCenter -- Layout.bottomMargin: Appearance.spacing.normal -+ Layout.bottomMargin: Tokens.spacing.normal - text: qsTr("Wallpaper") -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - font.weight: 600 - } - -@@ -119,8 +120,9 @@ Item { - - Layout.fillWidth: true - Layout.fillHeight: true -- Layout.bottomMargin: -Appearance.padding.large * 2 -+ Layout.bottomMargin: -Tokens.padding.large * 2 - -+ asynchronous: true - active: { - const isActive = root.session.activeIndex === 3; - const isAdjacent = Math.abs(root.session.activeIndex - 3) === 1; -@@ -132,7 +134,7 @@ Item { - - onStatusChanged: { - if (status === Loader.Error) { -- console.error("[AppearancePane] Wallpaper loader error!"); -+ console.error(lc, "Wallpaper loader error!"); - } - } - -@@ -148,10 +150,11 @@ Item { - anchors.fill: parent - - leftContent: Component { -- - StyledFlickable { - id: sidebarFlickable -+ - readonly property var rootPane: root -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height - -@@ -161,20 +164,20 @@ Item { - - ColumnLayout { - id: sidebarLayout -- anchors.left: parent.left -- anchors.right: parent.right -- spacing: Appearance.spacing.small - - readonly property var rootPane: sidebarFlickable.rootPane -- - readonly property bool allSectionsExpanded: themeModeSection.expanded && colorVariantSection.expanded && colorSchemeSection.expanded && animationsSection.expanded && fontsSection.expanded && scalesSection.expanded && transparencySection.expanded && borderSection.expanded && backgroundSection.expanded - -+ anchors.left: parent.left -+ anchors.right: parent.right -+ spacing: Tokens.spacing.small -+ - RowLayout { -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Appearance") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -215,31 +218,37 @@ Item { - - AnimationsSection { - id: animationsSection -+ - rootPane: sidebarFlickable.rootPane - } - - FontsSection { - id: fontsSection -+ - rootPane: sidebarFlickable.rootPane - } - - ScalesSection { - id: scalesSection -+ - rootPane: sidebarFlickable.rootPane - } - - TransparencySection { - id: transparencySection -+ - rootPane: sidebarFlickable.rootPane - } - - BorderSection { - id: borderSection -+ - rootPane: sidebarFlickable.rootPane - } - - BackgroundSection { - id: backgroundSection -+ - rootPane: sidebarFlickable.rootPane - } - } -@@ -248,4 +257,11 @@ Item { - - rightContent: appearanceRightContentComponent - } -+ -+ LoggingCategory { -+ id: lc -+ -+ name: "caelestia.qml.controlcenter.appearance" -+ defaultLogLevel: LoggingCategory.Info -+ } - } -diff --git a/modules/controlcenter/appearance/sections/AnimationsSection.qml b/modules/controlcenter/appearance/sections/AnimationsSection.qml -index 0cba5cec..412441ab 100644 ---- a/modules/controlcenter/appearance/sections/AnimationsSection.qml -+++ b/modules/controlcenter/appearance/sections/AnimationsSection.qml -@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - id: root -@@ -19,7 +19,7 @@ CollapsibleSection { - showBackground: true - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -diff --git a/modules/controlcenter/appearance/sections/BackgroundSection.qml b/modules/controlcenter/appearance/sections/BackgroundSection.qml -index 2f75c9e0..3f31f10b 100644 ---- a/modules/controlcenter/appearance/sections/BackgroundSection.qml -+++ b/modules/controlcenter/appearance/sections/BackgroundSection.qml -@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - id: root -@@ -27,10 +27,19 @@ CollapsibleSection { - } - } - -+ SwitchRow { -+ label: qsTr("Wallpaper enabled") -+ checked: rootPane.wallpaperEnabled -+ onToggled: checked => { -+ rootPane.wallpaperEnabled = checked; -+ rootPane.saveConfig(); -+ } -+ } -+ - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Desktop Clock") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -46,9 +55,6 @@ CollapsibleSection { - SectionContainer { - id: posContainer - -- contentSpacing: Appearance.spacing.small -- z: 1 -- - readonly property var pos: (rootPane.desktopClockPosition || "top-left").split('-') - readonly property string currentV: pos[0] - readonly property string currentH: pos[1] -@@ -58,9 +64,12 @@ CollapsibleSection { - rootPane.saveConfig(); - } - -+ contentSpacing: Tokens.spacing.small -+ z: 1 -+ - StyledText { - text: qsTr("Positioning") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -70,19 +79,22 @@ CollapsibleSection { - - menuItems: [ - MenuItem { -+ property string val: "top" -+ - text: qsTr("Top") - icon: "vertical_align_top" -- property string val: "top" - }, - MenuItem { -+ property string val: "middle" -+ - text: qsTr("Middle") - icon: "vertical_align_center" -- property string val: "middle" - }, - MenuItem { -+ property string val: "bottom" -+ - text: qsTr("Bottom") - icon: "vertical_align_bottom" -- property string val: "bottom" - } - ] - -@@ -104,19 +116,22 @@ CollapsibleSection { - - menuItems: [ - MenuItem { -+ property string val: "left" -+ - text: qsTr("Left") - icon: "align_horizontal_left" -- property string val: "left" - }, - MenuItem { -+ property string val: "center" -+ - text: qsTr("Center") - icon: "align_horizontal_center" -- property string val: "center" - }, - MenuItem { -+ property string val: "right" -+ - text: qsTr("Right") - icon: "align_horizontal_right" -- property string val: "right" - } - ] - -@@ -141,11 +156,11 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small -+ contentSpacing: Tokens.spacing.small - - StyledText { - text: qsTr("Shadow") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -159,7 +174,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -184,7 +199,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -210,11 +225,11 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small -+ contentSpacing: Tokens.spacing.small - - StyledText { - text: qsTr("Background") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -237,7 +252,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -263,9 +278,9 @@ CollapsibleSection { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Visualiser") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -288,7 +303,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -313,7 +328,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -diff --git a/modules/controlcenter/appearance/sections/BorderSection.qml b/modules/controlcenter/appearance/sections/BorderSection.qml -index 9532d70d..7dbd2dbe 100644 ---- a/modules/controlcenter/appearance/sections/BorderSection.qml -+++ b/modules/controlcenter/appearance/sections/BorderSection.qml -@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - id: root -@@ -19,7 +19,7 @@ CollapsibleSection { - showBackground: true - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -43,14 +43,14 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Border thickness") - value: rootPane.borderThickness -- from: 0.1 -+ from: 0 - to: 100 - decimals: 1 - suffix: "px" -diff --git a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml -index 95cb4b72..0a65cbdc 100644 ---- a/modules/controlcenter/appearance/sections/ColorSchemeSection.qml -+++ b/modules/controlcenter/appearance/sections/ColorSchemeSection.qml -@@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../../launcher/services" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import Quickshell --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - title: qsTr("Color scheme") -@@ -18,7 +18,7 @@ CollapsibleSection { - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - Repeater { - model: Schemes.list -@@ -32,12 +32,13 @@ CollapsibleSection { - readonly property bool isCurrent: schemeKey === Schemes.currentScheme - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary -+ implicitHeight: schemeRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -+ onClicked: { - const name = modelData.name; - const flavour = modelData.flavour; - const schemeKey = `${name} ${flavour}`; -@@ -53,6 +54,7 @@ CollapsibleSection { - - Timer { - id: reloadTimer -+ - interval: 300 - onTriggered: { - Schemes.reload(); -@@ -63,9 +65,9 @@ CollapsibleSection { - id: schemeRow - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - id: preview -@@ -76,15 +78,16 @@ CollapsibleSection { - border.color: Qt.alpha(`#${modelData.colours?.outline}`, 0.5) - - color: `#${modelData.colours?.surface}` -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - implicitWidth: iconPlaceholder.implicitWidth - implicitHeight: iconPlaceholder.implicitWidth - - MaterialIcon { - id: iconPlaceholder -+ - visible: false - text: "circle" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - - Item { -@@ -102,7 +105,7 @@ CollapsibleSection { - - implicitWidth: preview.implicitWidth - color: `#${modelData.colours?.primary}` -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - } - } - } -@@ -113,12 +116,12 @@ CollapsibleSection { - - StyledText { - text: modelData.flavour ?? "" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: modelData.name ?? "" -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - - elide: Text.ElideRight -@@ -128,17 +131,16 @@ CollapsibleSection { - } - - Loader { -+ asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -- -- implicitHeight: schemeRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } -diff --git a/modules/controlcenter/appearance/sections/ColorVariantSection.qml b/modules/controlcenter/appearance/sections/ColorVariantSection.qml -index 3aa17dd9..c612485d 100644 ---- a/modules/controlcenter/appearance/sections/ColorVariantSection.qml -+++ b/modules/controlcenter/appearance/sections/ColorVariantSection.qml -@@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../../launcher/services" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import Quickshell --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - title: qsTr("Color variant") -@@ -18,7 +18,7 @@ CollapsibleSection { - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - Repeater { - model: M3Variants.list -@@ -29,12 +29,13 @@ CollapsibleSection { - Layout.fillWidth: true - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, modelData.variant === Schemes.currentVariant ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - border.width: modelData.variant === Schemes.currentVariant ? 1 : 0 - border.color: Colours.palette.m3primary -+ implicitHeight: variantRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -+ onClicked: { - const variant = modelData.variant; - - Schemes.currentVariant = variant; -@@ -48,6 +49,7 @@ CollapsibleSection { - - Timer { - id: reloadTimer -+ - interval: 300 - onTriggered: { - Schemes.reload(); -@@ -60,13 +62,13 @@ CollapsibleSection { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: modelData.icon -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: modelData.variant === Schemes.currentVariant ? 1 : 0 - } - -@@ -80,11 +82,9 @@ CollapsibleSection { - visible: modelData.variant === Schemes.currentVariant - text: "check" - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } -- -- implicitHeight: variantRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } -diff --git a/modules/controlcenter/appearance/sections/FontsSection.qml b/modules/controlcenter/appearance/sections/FontsSection.qml -index 3988863a..10040c58 100644 ---- a/modules/controlcenter/appearance/sections/FontsSection.qml -+++ b/modules/controlcenter/appearance/sections/FontsSection.qml -@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - id: root -@@ -19,62 +19,64 @@ CollapsibleSection { - showBackground: true - - CollapsibleSection { -- id: materialFontSection -- title: qsTr("Material font family") -+ id: sansFontSection -+ -+ title: qsTr("Sans-serif font family") - expanded: true - showBackground: true - nested: true - - Loader { -- id: materialFontLoader - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 -- active: materialFontSection.expanded -+ asynchronous: true -+ active: sansFontSection.expanded - - sourceComponent: StyledListView { -- id: materialFontList -- property alias contentHeight: materialFontList.contentHeight -+ id: sansFontList -+ -+ property alias contentHeight: sansFontList.contentHeight - - clip: true -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { -- flickable: materialFontList -+ flickable: sansFontList - } - - delegate: StyledRect { - required property string modelData - required property int index -+ readonly property bool isCurrent: modelData === rootPane.fontFamilySans - - width: ListView.view.width -- -- readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary -+ implicitHeight: fontFamilySansRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -- rootPane.fontFamilyMaterial = modelData; -+ onClicked: { -+ rootPane.fontFamilySans = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { -- id: fontFamilyMaterialRow -+ id: fontFamilySansRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: modelData -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - Item { -@@ -82,17 +84,16 @@ CollapsibleSection { - } - - Loader { -+ asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -- -- implicitHeight: fontFamilyMaterialRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } -@@ -100,6 +101,7 @@ CollapsibleSection { - - CollapsibleSection { - id: monoFontSection -+ - title: qsTr("Monospace font family") - expanded: false - showBackground: true -@@ -108,14 +110,16 @@ CollapsibleSection { - Loader { - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 -+ asynchronous: true - active: monoFontSection.expanded - - sourceComponent: StyledListView { - id: monoFontList -+ - property alias contentHeight: monoFontList.contentHeight - - clip: true -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - model: Qt.fontFamilies() - - StyledScrollBar.vertical: StyledScrollBar { -@@ -125,17 +129,17 @@ CollapsibleSection { - delegate: StyledRect { - required property string modelData - required property int index -+ readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - - width: ListView.view.width -- -- readonly property bool isCurrent: modelData === rootPane.fontFamilyMono - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary -+ implicitHeight: fontFamilyMonoRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -+ onClicked: { - rootPane.fontFamilyMono = modelData; - rootPane.saveConfig(); - } -@@ -147,13 +151,13 @@ CollapsibleSection { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: modelData -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - Item { -@@ -161,78 +165,82 @@ CollapsibleSection { - } - - Loader { -+ asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -- -- implicitHeight: fontFamilyMonoRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } - } - - CollapsibleSection { -- id: sansFontSection -- title: qsTr("Sans-serif font family") -+ id: materialFontSection -+ -+ title: qsTr("Material font family") - expanded: false - showBackground: true - nested: true - - Loader { -+ id: materialFontLoader -+ - Layout.fillWidth: true - Layout.preferredHeight: item ? Math.min(item.contentHeight, 300) : 0 -- active: sansFontSection.expanded -+ asynchronous: true -+ active: materialFontSection.expanded - - sourceComponent: StyledListView { -- id: sansFontList -- property alias contentHeight: sansFontList.contentHeight -+ id: materialFontList -+ -+ property alias contentHeight: materialFontList.contentHeight - - clip: true -- spacing: Appearance.spacing.small / 2 -- model: Qt.fontFamilies() -+ spacing: Tokens.spacing.small / 2 -+ model: Qt.fontFamilies().filter(f => f.startsWith("Material Symbols")) - - StyledScrollBar.vertical: StyledScrollBar { -- flickable: sansFontList -+ flickable: materialFontList - } - - delegate: StyledRect { - required property string modelData - required property int index -+ readonly property bool isCurrent: modelData === rootPane.fontFamilyMaterial - - width: ListView.view.width -- -- readonly property bool isCurrent: modelData === rootPane.fontFamilySans - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, isCurrent ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - border.width: isCurrent ? 1 : 0 - border.color: Colours.palette.m3primary -+ implicitHeight: fontFamilyMaterialRow.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -- rootPane.fontFamilySans = modelData; -+ onClicked: { -+ rootPane.fontFamilyMaterial = modelData; - rootPane.saveConfig(); - } - } - - RowLayout { -- id: fontFamilySansRow -+ id: fontFamilyMaterialRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: modelData -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - Item { -@@ -240,24 +248,23 @@ CollapsibleSection { - } - - Loader { -+ asynchronous: true - active: isCurrent - - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -- -- implicitHeight: fontFamilySansRow.implicitHeight + Appearance.padding.normal * 2 - } - } - } - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -diff --git a/modules/controlcenter/appearance/sections/ScalesSection.qml b/modules/controlcenter/appearance/sections/ScalesSection.qml -index b0e6e38b..dac6226e 100644 ---- a/modules/controlcenter/appearance/sections/ScalesSection.qml -+++ b/modules/controlcenter/appearance/sections/ScalesSection.qml -@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - id: root -@@ -19,7 +19,7 @@ CollapsibleSection { - showBackground: true - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -43,7 +43,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -67,7 +67,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -diff --git a/modules/controlcenter/appearance/sections/ThemeModeSection.qml b/modules/controlcenter/appearance/sections/ThemeModeSection.qml -index 04eed911..aab53b8b 100644 ---- a/modules/controlcenter/appearance/sections/ThemeModeSection.qml -+++ b/modules/controlcenter/appearance/sections/ThemeModeSection.qml -@@ -1,12 +1,12 @@ - pragma ComponentBehavior: Bound - - import ".." -+import QtQuick -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick - - CollapsibleSection { - title: qsTr("Theme mode") -diff --git a/modules/controlcenter/appearance/sections/TransparencySection.qml b/modules/controlcenter/appearance/sections/TransparencySection.qml -index 9a48629c..f2f15ba1 100644 ---- a/modules/controlcenter/appearance/sections/TransparencySection.qml -+++ b/modules/controlcenter/appearance/sections/TransparencySection.qml -@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound - - import ".." - import "../../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - CollapsibleSection { - id: root -@@ -28,7 +28,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -@@ -53,7 +53,7 @@ CollapsibleSection { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true -diff --git a/modules/controlcenter/audio/AudioPane.qml b/modules/controlcenter/audio/AudioPane.qml -index 01d90be7..f3edb731 100644 ---- a/modules/controlcenter/audio/AudioPane.qml -+++ b/modules/controlcenter/audio/AudioPane.qml -@@ -2,15 +2,15 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts - - Item { - id: root -@@ -23,9 +23,9 @@ Item { - anchors.fill: parent - - leftContent: Component { -- - StyledFlickable { - id: leftAudioFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: leftContent.height - -@@ -38,15 +38,15 @@ Item { - - anchors.left: parent.left - anchors.right: parent.right -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Audio") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -64,15 +64,15 @@ Item { - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Devices (%1)").arg(Audio.sinks.length) -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - } -@@ -93,10 +93,11 @@ Item { - Layout.fillWidth: true - - color: Audio.sink?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal -+ implicitHeight: outputRowLayout.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -+ onClicked: { - Audio.setAudioSink(modelData); - } - } -@@ -107,13 +108,13 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: Audio.sink?.id === modelData.id ? "speaker" : "speaker_group" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: Audio.sink?.id === modelData.id ? 1 : 0 - } - -@@ -126,8 +127,6 @@ Item { - font.weight: Audio.sink?.id === modelData.id ? 500 : 400 - } - } -- -- implicitHeight: outputRowLayout.implicitHeight + Appearance.padding.normal * 2 - } - } - } -@@ -142,15 +141,15 @@ Item { - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Devices (%1)").arg(Audio.sources.length) -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - } -@@ -171,10 +170,11 @@ Item { - Layout.fillWidth: true - - color: Audio.source?.id === modelData.id ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal -+ implicitHeight: inputRowLayout.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- function onClicked(): void { -+ onClicked: { - Audio.setAudioSource(modelData); - } - } -@@ -185,13 +185,13 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: "mic" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: Audio.source?.id === modelData.id ? 1 : 0 - } - -@@ -204,8 +204,6 @@ Item { - font.weight: Audio.source?.id === modelData.id ? 500 : 400 - } - } -- -- implicitHeight: inputRowLayout.implicitHeight + Appearance.padding.normal * 2 - } - } - } -@@ -217,6 +215,7 @@ Item { - rightContent: Component { - StyledFlickable { - id: rightAudioFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: contentLayout.height - -@@ -230,7 +229,7 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "volume_up" -@@ -243,19 +242,19 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Volume") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - -@@ -265,6 +264,7 @@ Item { - - StyledInputField { - id: outputVolumeInput -+ - Layout.preferredWidth: 70 - validator: IntValidator { - bottom: 0 -@@ -276,15 +276,6 @@ Item { - text = Math.round(Audio.volume * 100).toString(); - } - -- Connections { -- target: Audio -- function onVolumeChanged() { -- if (!outputVolumeInput.hasFocus) { -- outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); -- } -- } -- } -- - onTextEdited: text => { - if (hasFocus) { - const val = parseInt(text); -@@ -300,24 +291,34 @@ Item { - text = Math.round(Audio.volume * 100).toString(); - } - } -+ -+ Connections { -+ function onVolumeChanged() { -+ if (!outputVolumeInput.hasFocus) { -+ outputVolumeInput.text = Math.round(Audio.volume * 100).toString(); -+ } -+ } -+ -+ target: Audio -+ } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - opacity: Audio.muted ? 0.5 : 1 - } - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: muteIcon.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: muteIcon.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Audio.muted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - StateLayer { -- function onClicked(): void { -+ onClicked: { - if (Audio.sink?.audio) { - Audio.sink.audio.muted = !Audio.sink.audio.muted; - } -@@ -336,8 +337,9 @@ Item { - - StyledSlider { - id: outputVolumeSlider -+ - Layout.fillWidth: true -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - - value: Audio.volume - enabled: !Audio.muted -@@ -358,19 +360,19 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Volume") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - -@@ -380,6 +382,7 @@ Item { - - StyledInputField { - id: inputVolumeInput -+ - Layout.preferredWidth: 70 - validator: IntValidator { - bottom: 0 -@@ -391,15 +394,6 @@ Item { - text = Math.round(Audio.sourceVolume * 100).toString(); - } - -- Connections { -- target: Audio -- function onSourceVolumeChanged() { -- if (!inputVolumeInput.hasFocus) { -- inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); -- } -- } -- } -- - onTextEdited: text => { - if (hasFocus) { - const val = parseInt(text); -@@ -415,24 +409,34 @@ Item { - text = Math.round(Audio.sourceVolume * 100).toString(); - } - } -+ -+ Connections { -+ function onSourceVolumeChanged() { -+ if (!inputVolumeInput.hasFocus) { -+ inputVolumeInput.text = Math.round(Audio.sourceVolume * 100).toString(); -+ } -+ } -+ -+ target: Audio -+ } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - opacity: Audio.sourceMuted ? 0.5 : 1 - } - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: muteInputIcon.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: muteInputIcon.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Audio.sourceMuted ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - StateLayer { -- function onClicked(): void { -+ onClicked: { - if (Audio.source?.audio) { - Audio.source.audio.muted = !Audio.source.audio.muted; - } -@@ -451,8 +455,9 @@ Item { - - StyledSlider { - id: inputVolumeSlider -+ - Layout.fillWidth: true -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - - value: Audio.sourceVolume - enabled: !Audio.sourceMuted -@@ -473,11 +478,11 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Repeater { - model: Audio.streams -@@ -488,15 +493,15 @@ Item { - required property int index - - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: "apps" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - fill: 0 - } - -@@ -505,12 +510,13 @@ Item { - elide: Text.ElideRight - maximumLineCount: 1 - text: Audio.getStreamName(modelData) -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - - StyledInputField { - id: streamVolumeInput -+ - Layout.preferredWidth: 70 - validator: IntValidator { - bottom: 0 -@@ -522,15 +528,6 @@ Item { - text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); - } - -- Connections { -- target: modelData -- function onAudioChanged() { -- if (!streamVolumeInput.hasFocus && modelData?.audio) { -- streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); -- } -- } -- } -- - onTextEdited: text => { - if (hasFocus) { - const val = parseInt(text); -@@ -546,24 +543,34 @@ Item { - text = Math.round(Audio.getStreamVolume(modelData) * 100).toString(); - } - } -+ -+ Connections { -+ function onAudioChanged() { -+ if (!streamVolumeInput.hasFocus && modelData?.audio) { -+ streamVolumeInput.text = Math.round(modelData.audio.volume * 100).toString(); -+ } -+ } -+ -+ target: modelData -+ } - } - - StyledText { - text: "%" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - opacity: Audio.getStreamMuted(modelData) ? 0.5 : 1 - } - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: streamMuteIcon.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: streamMuteIcon.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Audio.getStreamMuted(modelData) ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - StateLayer { -- function onClicked(): void { -+ onClicked: { - Audio.setStreamMuted(modelData, !Audio.getStreamMuted(modelData)); - } - } -@@ -580,7 +587,7 @@ Item { - - StyledSlider { - Layout.fillWidth: true -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - - value: Audio.getStreamVolume(modelData) - enabled: !Audio.getStreamMuted(modelData) -@@ -593,12 +600,13 @@ Item { - } - - Connections { -- target: modelData - function onAudioChanged() { - if (modelData?.audio) { - value = modelData.audio.volume; - } - } -+ -+ target: modelData - } - } - } -@@ -609,7 +617,7 @@ Item { - visible: Audio.streams.length === 0 - text: qsTr("No applications currently playing audio") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - horizontalAlignment: Text.AlignHCenter - } - } -diff --git a/modules/controlcenter/bluetooth/BtPane.qml b/modules/controlcenter/bluetooth/BtPane.qml -index 7d3b9ca3..b2b9c0f6 100644 ---- a/modules/controlcenter/bluetooth/BtPane.qml -+++ b/modules/controlcenter/bluetooth/BtPane.qml -@@ -3,13 +3,13 @@ pragma ComponentBehavior: Bound - import ".." - import "../components" - import "." -+import QtQuick -+import Quickshell.Bluetooth -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers --import qs.config --import Quickshell.Widgets --import Quickshell.Bluetooth --import QtQuick -+import qs.components.controls - - SplitPaneWithDetails { - id: root -@@ -53,6 +53,7 @@ SplitPaneWithDetails { - rightSettingsComponent: Component { - StyledFlickable { - id: settingsFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - -diff --git a/modules/controlcenter/bluetooth/Details.qml b/modules/controlcenter/bluetooth/Details.qml -index 52990454..41575226 100644 ---- a/modules/controlcenter/bluetooth/Details.qml -+++ b/modules/controlcenter/bluetooth/Details.qml -@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Bluetooth -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config - import qs.utils --import Quickshell.Bluetooth --import QtQuick --import QtQuick.Layouts - - StyledFlickable { - id: root -@@ -54,12 +54,12 @@ StyledFlickable { - sections: [ - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Connection status") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -70,9 +70,9 @@ StyledFlickable { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: deviceStatus.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: deviceStatus.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { -@@ -81,9 +81,9 @@ StyledFlickable { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.larger -+ spacing: Tokens.spacing.larger - - Toggle { - label: qsTr("Connected") -@@ -113,12 +113,12 @@ StyledFlickable { - }, - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Device properties") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -129,9 +129,9 @@ StyledFlickable { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: deviceProps.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: deviceProps.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { -@@ -140,19 +140,19 @@ StyledFlickable { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.larger -+ spacing: Tokens.spacing.larger - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Item { - id: renameDevice - - Layout.fillWidth: true -- Layout.rightMargin: Appearance.spacing.small -+ Layout.rightMargin: Tokens.spacing.small - - implicitHeight: renameLabel.implicitHeight + deviceNameEdit.implicitHeight - -@@ -167,15 +167,13 @@ StyledFlickable { - PropertyChanges { - renameDevice.implicitHeight: deviceNameEdit.implicitHeight - renameLabel.opacity: 0 -- deviceNameEdit.padding: Appearance.padding.normal -+ deviceNameEdit.padding: root.Tokens.padding.normal - } - } - - transitions: Transition { -- AnchorAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard -+ AnchorAnim { -+ type: AnchorAnim.Standard - } - Anim { - properties: "implicitHeight,opacity,padding" -@@ -189,7 +187,7 @@ StyledFlickable { - - text: qsTr("Device name") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledTextField { -@@ -198,7 +196,7 @@ StyledFlickable { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: renameLabel.bottom -- anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Appearance.padding.normal -+ anchors.leftMargin: root.session.bt.editingDeviceName ? 0 : -Tokens.padding.normal - - text: root.device?.name ?? "" - readOnly: !root.session.bt.editingDeviceName -@@ -207,11 +205,11 @@ StyledFlickable { - root.device.name = text; - } - -- leftPadding: Appearance.padding.normal -- rightPadding: Appearance.padding.normal -+ leftPadding: Tokens.padding.normal -+ rightPadding: Tokens.padding.normal - - background: StyledRect { -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - border.width: 2 - border.color: Colours.palette.m3primary - opacity: root.session.bt.editingDeviceName ? 1 : 0 -@@ -233,21 +231,21 @@ StyledFlickable { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.palette.m3secondaryContainer - opacity: root.session.bt.editingDeviceName ? 1 : 0 - scale: root.session.bt.editingDeviceName ? 1 : 0.5 - - StateLayer { -- color: Colours.palette.m3onSecondaryContainer -- disabled: !root.session.bt.editingDeviceName -- -- function onClicked(): void { -+ onClicked: { - root.session.bt.editingDeviceName = false; - deviceNameEdit.text = Qt.binding(() => root.device?.name ?? ""); - } -+ -+ color: Colours.palette.m3onSecondaryContainer -+ disabled: !root.session.bt.editingDeviceName - } - - MaterialIcon { -@@ -265,29 +263,28 @@ StyledFlickable { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: root.session.bt.editingDeviceName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) -+ radius: root.session.bt.editingDeviceName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingDeviceName ? 1 : 0) - - StateLayer { -- color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface -- -- function onClicked(): void { -+ onClicked: { - root.session.bt.editingDeviceName = !root.session.bt.editingDeviceName; - if (root.session.bt.editingDeviceName) - deviceNameEdit.forceActiveFocus(); - else - deviceNameEdit.accepted(); - } -+ -+ color: root.session.bt.editingDeviceName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } - - MaterialIcon { -@@ -322,12 +319,12 @@ StyledFlickable { - }, - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Device information") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -338,9 +335,9 @@ StyledFlickable { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: deviceInfo.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: deviceInfo.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { -@@ -349,88 +346,83 @@ StyledFlickable { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { - text: root.device?.batteryAvailable ? qsTr("Device battery (%1%)").arg(root.device.battery * 100) : qsTr("Battery unavailable") - } - - RowLayout { -- Layout.topMargin: Appearance.spacing.small / 2 -- Layout.fillWidth: true -- Layout.preferredHeight: Appearance.padding.smaller -- spacing: Appearance.spacing.small / 2 -+ id: batteryPercent - -- StyledRect { -- Layout.fillHeight: true -- implicitWidth: root.device?.batteryAvailable ? parent.width * root.device.battery : 0 -- radius: Appearance.rounding.full -- color: Colours.palette.m3primary -- } -+ Layout.topMargin: Tokens.spacing.small / 2 -+ Layout.fillWidth: true -+ Layout.preferredHeight: Tokens.padding.smaller -+ spacing: Tokens.spacing.small / 2 - - StyledRect { - Layout.fillWidth: true - Layout.fillHeight: true -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3secondaryContainer - - StyledRect { -- anchors.right: parent.right -+ anchors.left: parent.left - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.margins: parent.height * 0.25 - -- implicitWidth: height -- radius: Appearance.rounding.full -+ implicitWidth: root.device?.batteryAvailable ? batteryPercent.width * root.device.battery : 0 -+ radius: Tokens.rounding.full - color: Colours.palette.m3primary - } - } - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Dbus path") - } - - StyledText { - text: root.device?.dbusPath ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("MAC address") - } - - StyledText { - text: root.device?.address ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Bonded") - } - - StyledText { - text: root.device?.bonded ? qsTr("Yes") : qsTr("No") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("System name") - } - - StyledText { - text: root.device?.deviceName ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - } -@@ -443,7 +435,7 @@ StyledFlickable { - ColumnLayout { - anchors.right: fabRoot.right - anchors.bottom: fabRoot.top -- anchors.bottomMargin: Appearance.padding.normal -+ anchors.bottomMargin: Tokens.padding.normal - - Repeater { - id: fabMenu -@@ -475,9 +467,9 @@ StyledFlickable { - - Layout.alignment: Qt.AlignRight - -- implicitHeight: fabMenuItemInner.implicitHeight + Appearance.padding.larger * 2 -+ implicitHeight: fabMenuItemInner.implicitHeight + Tokens.padding.larger * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3primaryContainer - - opacity: 0 -@@ -487,7 +479,7 @@ StyledFlickable { - when: root.session.bt.fabMenuOpen - - PropertyChanges { -- fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + Appearance.padding.large * 2 -+ fabMenuItem.implicitWidth: fabMenuItemInner.implicitWidth + root.Tokens.padding.large * 2 - fabMenuItem.opacity: 1 - fabMenuItemInner.opacity: 1 - } -@@ -499,17 +491,16 @@ StyledFlickable { - - SequentialAnimation { - PauseAnimation { -- duration: (fabMenu.count - 1 - fabMenuItem.index) * Appearance.anim.durations.small / 8 -+ duration: (fabMenu.count - 1 - fabMenuItem.index) * Tokens.anim.durations.small / 8 - } - ParallelAnimation { - Anim { - property: "implicitWidth" -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - Anim { - property: "opacity" -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -@@ -519,17 +510,16 @@ StyledFlickable { - - SequentialAnimation { - PauseAnimation { -- duration: fabMenuItem.index * Appearance.anim.durations.small / 8 -+ duration: fabMenuItem.index * Tokens.anim.durations.small / 8 - } - ParallelAnimation { - Anim { - property: "implicitWidth" -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - Anim { - property: "opacity" -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -@@ -537,7 +527,7 @@ StyledFlickable { - ] - - StateLayer { -- function onClicked(): void { -+ onClicked: { - root.session.bt.fabMenuOpen = false; - - const name = fabMenuItem.modelData.name; -@@ -554,7 +544,7 @@ StyledFlickable { - id: fabMenuItemInner - - anchors.centerIn: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - opacity: 0 - - MaterialIcon { -@@ -572,7 +562,7 @@ StyledFlickable { - - Behavior on Layout.preferredWidth { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -@@ -599,7 +589,7 @@ StyledFlickable { - implicitWidth: 64 - implicitHeight: 64 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: root.session.bt.fabMenuOpen ? Colours.palette.m3primary : Colours.palette.m3primaryContainer - - states: State { -@@ -610,15 +600,14 @@ StyledFlickable { - fabBg.implicitWidth: 48 - fabBg.implicitHeight: 48 - fabBg.radius: 48 / 2 -- fab.font.pointSize: Appearance.font.size.larger -+ fab.font.pointSize: Tokens.font.size.larger - } - } - - transitions: Transition { - Anim { - properties: "implicitWidth,implicitHeight" -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - Anim { - properties: "radius,font.pointSize" -@@ -635,11 +624,11 @@ StyledFlickable { - StateLayer { - id: fabState - -- color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer -- -- function onClicked(): void { -+ onClicked: { - root.session.bt.fabMenuOpen = !root.session.bt.fabMenuOpen; - } -+ -+ color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer - } - - MaterialIcon { -@@ -649,7 +638,7 @@ StyledFlickable { - animate: true - text: root.session.bt.fabMenuOpen ? "close" : "settings" - color: root.session.bt.fabMenuOpen ? Colours.palette.m3onPrimary : Colours.palette.m3onPrimaryContainer -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: 1 - } - } -@@ -661,7 +650,7 @@ StyledFlickable { - property alias toggle: toggle - - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -diff --git a/modules/controlcenter/bluetooth/DeviceList.qml b/modules/controlcenter/bluetooth/DeviceList.qml -index 2a2bde93..419410ee 100644 ---- a/modules/controlcenter/bluetooth/DeviceList.qml -+++ b/modules/controlcenter/bluetooth/DeviceList.qml -@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Bluetooth -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config - import qs.utils --import Quickshell --import Quickshell.Bluetooth --import QtQuick --import QtQuick.Layouts - - DeviceList { - id: root -@@ -32,11 +32,11 @@ DeviceList { - - headerComponent: Component { - RowLayout { -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Bluetooth") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -48,9 +48,9 @@ DeviceList { - toggled: Bluetooth.defaultAdapter?.enabled ?? false - icon: "power" - accent: "Tertiary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Toggle Bluetooth") - - onClicked: { -@@ -64,9 +64,9 @@ DeviceList { - toggled: Bluetooth.defaultAdapter?.discoverable ?? false - icon: root.smallDiscoverable ? "group_search" : "" - label: root.smallDiscoverable ? "" : qsTr("Discoverable") -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Make discoverable") - - onClicked: { -@@ -80,9 +80,9 @@ DeviceList { - toggled: Bluetooth.defaultAdapter?.pairable ?? false - icon: "missing_controller" - label: root.smallPairable ? "" : qsTr("Pairable") -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Make pairable") - - onClicked: { -@@ -96,9 +96,9 @@ DeviceList { - toggled: Bluetooth.defaultAdapter?.discovering ?? false - icon: "bluetooth_searching" - accent: "Secondary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Scan for devices") - - onClicked: { -@@ -112,9 +112,9 @@ DeviceList { - toggled: !root.session.bt.active - icon: "settings" - accent: "Primary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Bluetooth settings") - - onClicked: { -@@ -137,15 +137,15 @@ DeviceList { - readonly property bool connected: modelData && modelData.state === BluetoothDeviceState.Connected - - width: ListView.view ? ListView.view.width : undefined -- implicitHeight: deviceInner.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: deviceInner.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - StateLayer { - id: stateLayer - -- function onClicked(): void { -+ onClicked: { - if (device.modelData) - root.session.bt.active = device.modelData; - } -@@ -155,15 +155,15 @@ DeviceList { - id: deviceInner - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: device.connected ? Colours.palette.m3primaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainerHigh - - StyledRect { -@@ -178,7 +178,7 @@ DeviceList { - anchors.centerIn: parent - text: Icons.getBluetoothIcon(device.modelData ? device.modelData.icon : "") - color: device.connected ? Colours.palette.m3onPrimaryContainer : (device.modelData && device.modelData.bonded) ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: device.connected ? 1 : 0 - - Behavior on fill { -@@ -202,7 +202,7 @@ DeviceList { - Layout.fillWidth: true - text: (device.modelData ? device.modelData.address : "") + (device.connected ? qsTr(" (Connected)") : (device.modelData && device.modelData.bonded) ? qsTr(" (Paired)") : "") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - } -@@ -211,9 +211,9 @@ DeviceList { - id: connectBtn - - implicitWidth: implicitHeight -- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, device.connected ? 1 : 0) - - CircularIndicator { -@@ -222,10 +222,7 @@ DeviceList { - } - - StateLayer { -- color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface -- disabled: device.loading -- -- function onClicked(): void { -+ onClicked: { - if (device.loading) - return; - -@@ -239,6 +236,9 @@ DeviceList { - } - } - } -+ -+ color: device.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface -+ disabled: device.loading - } - - MaterialIcon { -diff --git a/modules/controlcenter/bluetooth/Settings.qml b/modules/controlcenter/bluetooth/Settings.qml -index c5472406..1202cc5b 100644 ---- a/modules/controlcenter/bluetooth/Settings.qml -+++ b/modules/controlcenter/bluetooth/Settings.qml -@@ -2,21 +2,21 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Bluetooth -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import Quickshell.Bluetooth --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property Session session - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "bluetooth" -@@ -24,9 +24,9 @@ ColumnLayout { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Adapter status") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -37,9 +37,9 @@ ColumnLayout { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: adapterStatus.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: adapterStatus.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { -@@ -48,9 +48,9 @@ ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.larger -+ spacing: Tokens.spacing.larger - - Toggle { - label: qsTr("Powered") -@@ -85,9 +85,9 @@ ColumnLayout { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Adapter properties") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -98,9 +98,9 @@ ColumnLayout { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: adapterSettings.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: adapterSettings.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { -@@ -109,13 +109,13 @@ ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.larger -+ spacing: Tokens.spacing.larger - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -@@ -127,28 +127,28 @@ ColumnLayout { - - property bool expanded - -- implicitWidth: adapterPicker.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: adapterPicker.implicitHeight + Appearance.padding.smaller * 2 -+ implicitWidth: adapterPicker.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: adapterPicker.implicitHeight + Tokens.padding.smaller * 2 - - StateLayer { -- radius: Appearance.rounding.small -- -- function onClicked(): void { -+ onClicked: { - adapterPickerButton.expanded = !adapterPickerButton.expanded; - } -+ -+ radius: Tokens.rounding.small - } - - RowLayout { - id: adapterPicker - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -- anchors.topMargin: Appearance.padding.smaller -- anchors.bottomMargin: Appearance.padding.smaller -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.normal -+ anchors.topMargin: Tokens.padding.smaller -+ anchors.bottomMargin: Tokens.padding.smaller -+ spacing: Tokens.spacing.normal - - StyledText { -- Layout.leftMargin: Appearance.padding.small -+ Layout.leftMargin: Tokens.padding.small - text: Bluetooth.defaultAdapter?.name ?? qsTr("None") - } - -@@ -170,8 +170,7 @@ ColumnLayout { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -@@ -185,7 +184,7 @@ ColumnLayout { - implicitHeight: adapterPickerButton.expanded ? adapterList.implicitHeight : adapterPickerButton.implicitHeight - - color: Colours.palette.m3secondaryContainer -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - opacity: adapterPickerButton.expanded ? 1 : 0 - scale: adapterPickerButton.expanded ? 1 : 0.7 - -@@ -207,15 +206,15 @@ ColumnLayout { - required property BluetoothAdapter modelData - - Layout.fillWidth: true -- implicitHeight: adapterInner.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: adapterInner.implicitHeight + Tokens.padding.normal * 2 - - StateLayer { -- disabled: !adapterPickerButton.expanded -- -- function onClicked(): void { -+ onClicked: { - adapterPickerButton.expanded = false; - root.session.bt.currentAdapter = adapter.modelData; - } -+ -+ disabled: !adapterPickerButton.expanded - } - - RowLayout { -@@ -224,12 +223,12 @@ ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -- Layout.leftMargin: Appearance.padding.small -+ Layout.leftMargin: Tokens.padding.small - text: adapter.modelData.name - color: Colours.palette.m3onSecondaryContainer - } -@@ -250,15 +249,13 @@ ColumnLayout { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -@@ -267,7 +264,7 @@ ColumnLayout { - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -@@ -287,13 +284,13 @@ ColumnLayout { - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Item { - id: renameAdapter - - Layout.fillWidth: true -- Layout.rightMargin: Appearance.spacing.small -+ Layout.rightMargin: Tokens.spacing.small - - implicitHeight: renameLabel.implicitHeight + adapterNameEdit.implicitHeight - -@@ -308,15 +305,13 @@ ColumnLayout { - PropertyChanges { - renameAdapter.implicitHeight: adapterNameEdit.implicitHeight - renameLabel.opacity: 0 -- adapterNameEdit.padding: Appearance.padding.normal -+ adapterNameEdit.padding: Tokens.padding.normal - } - } - - transitions: Transition { -- AnchorAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard -+ AnchorAnim { -+ type: AnchorAnim.Standard - } - Anim { - properties: "implicitHeight,opacity,padding" -@@ -330,7 +325,7 @@ ColumnLayout { - - text: qsTr("Rename adapter (currently does not work)") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledTextField { -@@ -339,7 +334,7 @@ ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: renameLabel.bottom -- anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Appearance.padding.normal -+ anchors.leftMargin: root.session.bt.editingAdapterName ? 0 : -Tokens.padding.normal - - text: root.session.bt.currentAdapter?.name ?? "" - readOnly: !root.session.bt.editingAdapterName -@@ -347,11 +342,11 @@ ColumnLayout { - root.session.bt.editingAdapterName = false; - } - -- leftPadding: Appearance.padding.normal -- rightPadding: Appearance.padding.normal -+ leftPadding: Tokens.padding.normal -+ rightPadding: Tokens.padding.normal - - background: StyledRect { -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - border.width: 2 - border.color: Colours.palette.m3primary - opacity: root.session.bt.editingAdapterName ? 1 : 0 -@@ -373,21 +368,21 @@ ColumnLayout { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: cancelEditIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: cancelEditIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.palette.m3secondaryContainer - opacity: root.session.bt.editingAdapterName ? 1 : 0 - scale: root.session.bt.editingAdapterName ? 1 : 0.5 - - StateLayer { -- color: Colours.palette.m3onSecondaryContainer -- disabled: !root.session.bt.editingAdapterName -- -- function onClicked(): void { -+ onClicked: { - root.session.bt.editingAdapterName = false; - adapterNameEdit.text = Qt.binding(() => root.session.bt.currentAdapter?.name ?? ""); - } -+ -+ color: Colours.palette.m3onSecondaryContainer -+ disabled: !root.session.bt.editingAdapterName - } - - MaterialIcon { -@@ -405,29 +400,28 @@ ColumnLayout { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: editIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: editIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: root.session.bt.editingAdapterName ? Appearance.rounding.small : implicitHeight / 2 * Math.min(1, Appearance.rounding.scale) -+ radius: root.session.bt.editingAdapterName ? Tokens.rounding.small : implicitHeight / 2 * Math.min(1, Tokens.rounding.scale) - color: Qt.alpha(Colours.palette.m3primary, root.session.bt.editingAdapterName ? 1 : 0) - - StateLayer { -- color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface -- -- function onClicked(): void { -+ onClicked: { - root.session.bt.editingAdapterName = !root.session.bt.editingAdapterName; - if (root.session.bt.editingAdapterName) - adapterNameEdit.forceActiveFocus(); - else - adapterNameEdit.accepted(); - } -+ -+ color: root.session.bt.editingAdapterName ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface - } - - MaterialIcon { -@@ -448,9 +442,9 @@ ColumnLayout { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Adapter information") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -461,9 +455,9 @@ ColumnLayout { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: adapterInfo.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: adapterInfo.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { -@@ -472,9 +466,9 @@ ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { - text: qsTr("Adapter state") -@@ -483,29 +477,29 @@ ColumnLayout { - StyledText { - text: Bluetooth.defaultAdapter ? BluetoothAdapterState.toString(Bluetooth.defaultAdapter.state) : qsTr("Unknown") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Dbus path") - } - - StyledText { - text: Bluetooth.defaultAdapter?.dbusPath ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Adapter id") - } - - StyledText { - text: Bluetooth.defaultAdapter?.adapterId ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - } -@@ -516,7 +510,7 @@ ColumnLayout { - property alias toggle: toggle - - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -diff --git a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml b/modules/controlcenter/components/ConnectedButtonGroup.qml -similarity index 71% -rename from modules/controlcenter/taskbar/ConnectedButtonGroup.qml -rename to modules/controlcenter/components/ConnectedButtonGroup.qml -index 01cd612c..f782838d 100644 ---- a/modules/controlcenter/taskbar/ConnectedButtonGroup.qml -+++ b/modules/controlcenter/components/ConnectedButtonGroup.qml -@@ -1,22 +1,23 @@ - import ".." -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - StyledRect { - id: root - -- property var options: [] // Array of {label: string, propertyName: string, onToggled: function} -+ property var options: [] // Array of {label: string, propertyName: string, onToggled: function, state: bool?} - property var rootItem: null // The root item that contains the properties we want to bind to - property string title: "" // Optional title text -+ property int rows: 1 // Number of rows - - Layout.fillWidth: true -- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -+ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 -+ radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - clip: true - -@@ -28,41 +29,48 @@ StyledRect { - id: layout - - anchors.fill: parent -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - StyledText { - visible: root.title !== "" - text: root.title -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - -- RowLayout { -- id: buttonRow -+ GridLayout { -+ id: buttonGrid -+ - Layout.alignment: Qt.AlignHCenter -- spacing: Appearance.spacing.small -+ rowSpacing: Tokens.spacing.small -+ columnSpacing: Tokens.spacing.small -+ rows: root.rows -+ columns: Math.ceil(root.options.length / root.rows) - - Repeater { - id: repeater -+ - model: root.options - - delegate: TextButton { - id: button -+ - required property int index - required property var modelData - -- Layout.fillWidth: true -- text: modelData.label -- - property bool _checked: false - -+ Layout.fillWidth: true -+ text: modelData.label - checked: _checked - toggle: false - type: TextButton.Tonal - - // Create binding in Component.onCompleted - Component.onCompleted: { -- if (root.rootItem && modelData.propertyName) { -+ if (modelData.state !== undefined && modelData.state) { -+ _checked = modelData.state; -+ } else if (root.rootItem && modelData.propertyName) { - const propName = modelData.propertyName; - const rootItem = root.rootItem; - _checked = Qt.binding(function () { -@@ -73,13 +81,13 @@ StyledRect { - - // Match utilities Toggles radius styling - // Each button has full rounding (not connected) since they have spacing -- radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal -+ radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal - - // Match utilities Toggles inactive color - inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - - // Adjust width similar to utilities toggles -- Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) -+ Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) - - onClicked: { - if (modelData.onToggled && root.rootItem && modelData.propertyName) { -@@ -90,15 +98,13 @@ StyledRect { - - Behavior on Layout.preferredWidth { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - Behavior on radius { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -diff --git a/modules/controlcenter/components/DeviceDetails.qml b/modules/controlcenter/components/DeviceDetails.qml -index a5d06471..075024f6 100644 ---- a/modules/controlcenter/components/DeviceDetails.qml -+++ b/modules/controlcenter/components/DeviceDetails.qml -@@ -1,13 +1,13 @@ - pragma ComponentBehavior: Bound - - import ".." -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers --import qs.config --import QtQuick --import QtQuick.Layouts - - Item { - id: root -@@ -30,12 +30,13 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Loader { - id: headerLoader - - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: root.headerComponent - visible: root.headerComponent !== null - } -@@ -44,6 +45,7 @@ Item { - id: topContentLoader - - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: root.topContent - visible: root.topContent !== null - } -@@ -55,6 +57,7 @@ Item { - required property Component modelData - - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: modelData - } - } -@@ -63,6 +66,7 @@ Item { - id: bottomContentLoader - - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: root.bottomContent - visible: root.bottomContent !== null - } -diff --git a/modules/controlcenter/components/DeviceList.qml b/modules/controlcenter/components/DeviceList.qml -index 722f9a16..f02cc3c0 100644 ---- a/modules/controlcenter/components/DeviceList.qml -+++ b/modules/controlcenter/components/DeviceList.qml -@@ -1,14 +1,14 @@ - pragma ComponentBehavior: Bound - - import ".." -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import Quickshell --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root -@@ -23,15 +23,17 @@ ColumnLayout { - property Component headerComponent: null - property Component titleSuffix: null - property bool showHeader: true -+ property alias view: view - - signal itemSelected(var item) - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Loader { - id: headerLoader - - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: root.headerComponent - visible: root.headerComponent !== null && root.showHeader - } -@@ -39,17 +41,18 @@ ColumnLayout { - RowLayout { - Layout.fillWidth: true - Layout.topMargin: root.headerComponent ? 0 : 0 -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - visible: root.title !== "" || root.description !== "" - - StyledText { - visible: root.title !== "" - text: root.title -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - Loader { -+ asynchronous: true - sourceComponent: root.titleSuffix - visible: root.titleSuffix !== null - } -@@ -59,8 +62,6 @@ ColumnLayout { - } - } - -- property alias view: view -- - StyledText { - visible: root.description !== "" - Layout.fillWidth: true -@@ -77,7 +78,7 @@ ColumnLayout { - model: root.model - delegate: root.delegate - -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - interactive: false - clip: false - } -diff --git a/modules/controlcenter/components/PaneTransition.qml b/modules/controlcenter/components/PaneTransition.qml -index 5d80dbec..489d3c78 100644 ---- a/modules/controlcenter/components/PaneTransition.qml -+++ b/modules/controlcenter/components/PaneTransition.qml -@@ -1,7 +1,7 @@ - pragma ComponentBehavior: Bound - --import qs.config - import QtQuick -+import Caelestia.Config - - SequentialAnimation { - id: root -@@ -20,9 +20,8 @@ SequentialAnimation { - property: "opacity" - from: root.opacityFrom - to: root.opacityTo -- duration: Appearance.anim.durations.normal / 2 -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ duration: Tokens.anim.durations.normal / 2 -+ easing: Tokens.anim.standardAccel - } - - NumberAnimation { -@@ -30,9 +29,8 @@ SequentialAnimation { - property: "scale" - from: root.scaleFrom - to: root.scaleTo -- duration: Appearance.anim.durations.normal / 2 -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ duration: Tokens.anim.durations.normal / 2 -+ easing: Tokens.anim.standardAccel - } - } - -@@ -53,9 +51,8 @@ SequentialAnimation { - property: "opacity" - from: root.opacityTo - to: root.opacityFrom -- duration: Appearance.anim.durations.normal / 2 -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ duration: Tokens.anim.durations.normal / 2 -+ easing: Tokens.anim.standardDecel - } - - NumberAnimation { -@@ -63,9 +60,8 @@ SequentialAnimation { - property: "scale" - from: root.scaleTo - to: root.scaleFrom -- duration: Appearance.anim.durations.normal / 2 -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ duration: Tokens.anim.durations.normal / 2 -+ easing: Tokens.anim.standardDecel - } - } - } -diff --git a/modules/controlcenter/components/ReadonlySlider.qml b/modules/controlcenter/components/ReadonlySlider.qml -new file mode 100644 -index 00000000..8cd493f4 ---- /dev/null -+++ b/modules/controlcenter/components/ReadonlySlider.qml -@@ -0,0 +1,67 @@ -+import ".." -+import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.components.controls -+import qs.services -+ -+ColumnLayout { -+ id: root -+ -+ property string label: "" -+ property real value: 0 -+ property real from: 0 -+ property real to: 100 -+ property string suffix: "" -+ property bool readonly: false -+ -+ spacing: Tokens.spacing.small -+ -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ -+ StyledText { -+ visible: root.label !== "" -+ text: root.label -+ font.pointSize: Tokens.font.size.normal -+ color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ -+ MaterialIcon { -+ visible: root.readonly -+ text: "lock" -+ color: Colours.palette.m3outline -+ font.pointSize: Tokens.font.size.small -+ } -+ -+ StyledText { -+ text: Math.round(root.value) + (root.suffix !== "" ? " " + root.suffix : "") -+ font.pointSize: Tokens.font.size.normal -+ color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3onSurface -+ } -+ } -+ -+ StyledRect { -+ Layout.fillWidth: true -+ implicitHeight: Tokens.padding.normal -+ radius: Tokens.rounding.full -+ color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 1) -+ opacity: root.readonly ? 0.5 : 1.0 -+ -+ StyledRect { -+ anchors.left: parent.left -+ anchors.top: parent.top -+ anchors.bottom: parent.bottom -+ width: parent.width * ((root.value - root.from) / (root.to - root.from)) -+ radius: parent.radius -+ color: root.readonly ? Colours.palette.m3outline : Colours.palette.m3primary -+ } -+ } -+} -diff --git a/modules/controlcenter/components/SettingsHeader.qml b/modules/controlcenter/components/SettingsHeader.qml -index 0dc190c0..c6ccbb67 100644 ---- a/modules/controlcenter/components/SettingsHeader.qml -+++ b/modules/controlcenter/components/SettingsHeader.qml -@@ -1,9 +1,9 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components - - Item { - id: root -@@ -18,19 +18,19 @@ Item { - id: column - - anchors.centerIn: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: root.icon -- font.pointSize: Appearance.font.size.extraLarge * 3 -+ font.pointSize: Tokens.font.size.extraLarge * 3 - font.bold: true - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: root.title -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.bold: true - } - } -diff --git a/modules/controlcenter/components/SliderInput.qml b/modules/controlcenter/components/SliderInput.qml -index 11b3f70d..73fd00ae 100644 ---- a/modules/controlcenter/components/SliderInput.qml -+++ b/modules/controlcenter/components/SliderInput.qml -@@ -1,12 +1,12 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root -@@ -21,6 +21,9 @@ ColumnLayout { - property int decimals: 1 // Number of decimal places to show (default: 1) - property var formatValueFunction: null // Optional custom format function - property var parseValueFunction: null // Optional custom parse function -+ property bool _initialized: false -+ -+ signal valueModified(real newValue) - - function formatValue(val: real): string { - if (formatValueFunction) { -@@ -49,11 +52,7 @@ ColumnLayout { - return parseFloat(text); - } - -- signal valueModified(real newValue) -- -- property bool _initialized: false -- -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Component.onCompleted: { - // Set initialized flag after a brief delay to allow component to fully load -@@ -62,14 +61,22 @@ ColumnLayout { - }); - } - -+ // Update input field when value changes externally (slider is already bound) -+ onValueChanged: { -+ // Only update if component is initialized to avoid issues during creation -+ if (root._initialized && !inputField.hasFocus) { -+ inputField.text = root.formatValue(root.value); -+ } -+ } -+ - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - visible: root.label !== "" - text: root.label -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - Item { -@@ -78,6 +85,7 @@ ColumnLayout { - - StyledInputField { - id: inputField -+ - Layout.preferredWidth: 70 - validator: root.validator - -@@ -130,7 +138,7 @@ ColumnLayout { - visible: root.suffix !== "" - text: root.suffix - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - } - -@@ -138,20 +146,12 @@ ColumnLayout { - id: slider - - Layout.fillWidth: true -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - - from: root.from - to: root.to - stepSize: root.stepSize - -- // Use Binding to allow slider to move freely during dragging -- Binding { -- target: slider -- property: "value" -- value: root.value -- when: !slider.pressed -- } -- - onValueChanged: { - // Update input field text in real-time as slider moves during dragging - // Always update when slider value changes (during dragging or external updates) -@@ -168,13 +168,13 @@ ColumnLayout { - inputField.text = root.formatValue(newValue); - } - } -- } - -- // Update input field when value changes externally (slider is already bound) -- onValueChanged: { -- // Only update if component is initialized to avoid issues during creation -- if (root._initialized && !inputField.hasFocus) { -- inputField.text = root.formatValue(root.value); -+ // Use Binding to allow slider to move freely during dragging -+ Binding { -+ target: slider -+ property: "value" -+ value: root.value -+ when: !slider.pressed - } - } - } -diff --git a/modules/controlcenter/components/SplitPaneLayout.qml b/modules/controlcenter/components/SplitPaneLayout.qml -index 89504a0b..64c7c9fc 100644 ---- a/modules/controlcenter/components/SplitPaneLayout.qml -+++ b/modules/controlcenter/components/SplitPaneLayout.qml -@@ -1,28 +1,26 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.components.effects --import qs.config --import Quickshell.Widgets - import QtQuick - import QtQuick.Layouts -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components -+import qs.components.effects - - RowLayout { - id: root - -- spacing: 0 -- - property Component leftContent: null - property Component rightContent: null -- - property real leftWidthRatio: 0.4 - property int leftMinimumWidth: 420 - property var leftLoaderProperties: ({}) - property var rightLoaderProperties: ({}) -- - property alias leftLoader: leftLoader - property alias rightLoader: rightLoader - -+ spacing: 0 -+ - Item { - id: leftPane - -@@ -34,9 +32,9 @@ RowLayout { - id: leftClippingRect - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 -- anchors.rightMargin: Appearance.padding.normal / 2 -+ anchors.rightMargin: Tokens.padding.normal / 2 - - radius: leftBorder.innerRadius - color: "transparent" -@@ -45,10 +43,11 @@ RowLayout { - id: leftLoader - - anchors.fill: parent -- anchors.margins: Appearance.padding.large + Appearance.padding.normal -- anchors.leftMargin: Appearance.padding.large -- anchors.rightMargin: Appearance.padding.large + Appearance.padding.normal / 2 -+ anchors.margins: Tokens.padding.large + Tokens.padding.normal -+ anchors.leftMargin: Tokens.padding.large -+ anchors.rightMargin: Tokens.padding.large + Tokens.padding.normal / 2 - -+ asynchronous: true - sourceComponent: root.leftContent - - Component.onCompleted: { -@@ -63,7 +62,7 @@ RowLayout { - id: leftBorder - - leftThickness: 0 -- rightThickness: Appearance.padding.normal / 2 -+ rightThickness: Tokens.padding.normal / 2 - } - } - -@@ -77,9 +76,9 @@ RowLayout { - id: rightClippingRect - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 -- anchors.rightMargin: Appearance.padding.normal / 2 -+ anchors.rightMargin: Tokens.padding.normal / 2 - - radius: rightBorder.innerRadius - color: "transparent" -@@ -88,8 +87,9 @@ RowLayout { - id: rightLoader - - anchors.fill: parent -- anchors.margins: Appearance.padding.large * 2 -+ anchors.margins: Tokens.padding.large * 2 - -+ asynchronous: true - sourceComponent: root.rightContent - - Component.onCompleted: { -@@ -103,7 +103,7 @@ RowLayout { - InnerBorder { - id: rightBorder - -- leftThickness: Appearance.padding.normal / 2 -+ leftThickness: Tokens.padding.normal / 2 - } - } - } -diff --git a/modules/controlcenter/components/SplitPaneWithDetails.qml b/modules/controlcenter/components/SplitPaneWithDetails.qml -index ce8c9d07..ad1c444d 100644 ---- a/modules/controlcenter/components/SplitPaneWithDetails.qml -+++ b/modules/controlcenter/components/SplitPaneWithDetails.qml -@@ -1,13 +1,13 @@ - pragma ComponentBehavior: Bound - - import ".." --import qs.components --import qs.components.effects --import qs.components.containers --import qs.config --import Quickshell.Widgets - import QtQuick - import QtQuick.Layouts -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components -+import qs.components.containers -+import qs.components.effects - - Item { - id: root -@@ -48,11 +48,17 @@ Item { - nextComponent = targetComponent; - } - -+ onPaneChanged: { -+ nextComponent = getComponentForPane(); -+ paneId = root.paneIdGenerator(pane); -+ } -+ - Loader { - id: rightLoader - - anchors.fill: parent - -+ asynchronous: true - opacity: 1 - scale: 1 - transformOrigin: Item.Center -@@ -73,11 +79,6 @@ Item { - ] - } - } -- -- onPaneChanged: { -- nextComponent = getComponentForPane(); -- paneId = root.paneIdGenerator(pane); -- } - } - } - } -@@ -86,6 +87,7 @@ Item { - id: overlayLoader - - anchors.fill: parent -+ asynchronous: true - z: 1000 - sourceComponent: root.overlayComponent - active: root.overlayComponent !== null -diff --git a/modules/controlcenter/components/WallpaperGrid.qml b/modules/controlcenter/components/WallpaperGrid.qml -index ed6bb40a..44a615c6 100644 ---- a/modules/controlcenter/components/WallpaperGrid.qml -+++ b/modules/controlcenter/components/WallpaperGrid.qml -@@ -1,25 +1,25 @@ - pragma ComponentBehavior: Bound - - import ".." -+import QtQuick -+import Caelestia.Config -+import Caelestia.Models - import qs.components - import qs.components.controls - import qs.components.effects - import qs.components.images - import qs.services --import qs.config --import Caelestia.Models --import QtQuick - - GridView { - id: root - - required property Session session - -- readonly property int minCellWidth: 200 + Appearance.spacing.normal -+ readonly property int minCellWidth: 200 + Tokens.spacing.normal - readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) - - cellWidth: width / columnsCount -- cellHeight: 140 + Appearance.spacing.normal -+ cellHeight: 140 + Tokens.spacing.normal - - model: Wallpapers.list - -@@ -32,25 +32,24 @@ GridView { - delegate: Item { - required property var modelData - required property int index -+ readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent -+ readonly property real itemMargin: Tokens.spacing.normal / 2 -+ readonly property real itemRadius: Tokens.rounding.normal - - width: root.cellWidth - height: root.cellHeight - -- readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent -- readonly property real itemMargin: Appearance.spacing.normal / 2 -- readonly property real itemRadius: Appearance.rounding.normal -- - StateLayer { -+ onClicked: { -+ Wallpapers.setWallpaper(modelData.path); -+ } -+ - anchors.fill: parent - anchors.leftMargin: itemMargin - anchors.rightMargin: itemMargin - anchors.topMargin: itemMargin - anchors.bottomMargin: itemMargin - radius: itemRadius -- -- function onClicked(): void { -- Wallpapers.setWallpaper(modelData.path); -- } - } - - StyledClippingRect { -@@ -117,6 +116,7 @@ GridView { - id: fallbackTimer - - property bool triggered: false -+ - interval: 800 - running: cachingImage.status === Image.Loading || cachingImage.status === Image.Null - onTriggered: triggered = true -@@ -130,7 +130,7 @@ GridView { - anchors.right: parent.right - anchors.bottom: parent.bottom - -- implicitHeight: filenameText.implicitHeight + Appearance.padding.normal * 1.5 -+ implicitHeight: filenameText.implicitHeight + Tokens.padding.normal * 1.5 - radius: 0 - - gradient: Gradient { -@@ -154,16 +154,16 @@ GridView { - - opacity: 0 - -+ Component.onCompleted: { -+ opacity = 1; -+ } -+ - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } -- -- Component.onCompleted: { -- opacity = 1; -- } - } - } - -@@ -190,26 +190,27 @@ GridView { - MaterialIcon { - anchors.right: parent.right - anchors.top: parent.top -- anchors.margins: Appearance.padding.small -+ anchors.margins: Tokens.padding.small - - visible: isCurrent - text: "check_circle" - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - - StyledText { - id: filenameText -+ - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom -- anchors.leftMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 -- anchors.rightMargin: Appearance.padding.normal + Appearance.spacing.normal / 2 -- anchors.bottomMargin: Appearance.padding.normal -+ anchors.leftMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 -+ anchors.rightMargin: Tokens.padding.normal + Tokens.spacing.normal / 2 -+ anchors.bottomMargin: Tokens.padding.normal - - text: modelData.name -- font.pointSize: Appearance.font.size.smaller -+ font.pointSize: Tokens.font.size.smaller - font.weight: 500 - color: isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurface - elide: Text.ElideMiddle -@@ -218,16 +219,16 @@ GridView { - - opacity: 0 - -+ Component.onCompleted: { -+ opacity = 1; -+ } -+ - Behavior on opacity { - NumberAnimation { - duration: 1000 - easing.type: Easing.OutCubic - } - } -- -- Component.onCompleted: { -- opacity = 1; -- } - } - } - } -diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml -new file mode 100644 -index 00000000..e9b6e568 ---- /dev/null -+++ b/modules/controlcenter/dashboard/DashboardPane.qml -@@ -0,0 +1,527 @@ -+pragma ComponentBehavior: Bound -+ -+import ".." -+import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Services.UPower -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components -+import qs.components.containers -+import qs.components.controls -+import qs.components.effects -+import qs.services -+import qs.utils -+ -+Item { -+ id: root -+ -+ required property Session session -+ property string activeSection: "general" -+ -+ property bool enabled: Config.dashboard.enabled ?? true -+ property bool showOnHover: Config.dashboard.showOnHover ?? true -+ property int mediaUpdateInterval: GlobalConfig.dashboard.mediaUpdateInterval ?? 1000 -+ property int resourceUpdateInterval: GlobalConfig.dashboard.resourceUpdateInterval ?? 1000 -+ property int dragThreshold: Config.dashboard.dragThreshold ?? 50 -+ -+ property bool showDashboard: Config.dashboard.showDashboard ?? true -+ property bool showMedia: Config.dashboard.showMedia ?? true -+ property bool showPerformance: Config.dashboard.showPerformance ?? true -+ property bool showWeather: Config.dashboard.showWeather ?? true -+ -+ property bool showBattery: Config.dashboard.performance.showBattery ?? false -+ property bool showGpu: Config.dashboard.performance.showGpu ?? true -+ property bool showCpu: Config.dashboard.performance.showCpu ?? true -+ property bool showMemory: Config.dashboard.performance.showMemory ?? true -+ property bool showStorage: Config.dashboard.performance.showStorage ?? true -+ property bool showNetwork: Config.dashboard.performance.showNetwork ?? true -+ -+ readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" -+ readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery -+ readonly property var sections: [ -+ { -+ id: "general", -+ title: qsTr("General"), -+ description: qsTr("Visibility and timing"), -+ icon: "dashboard" -+ }, -+ { -+ id: "tabs", -+ title: qsTr("Tabs"), -+ description: qsTr("Dashboard pages"), -+ icon: "tab" -+ }, -+ { -+ id: "performance", -+ title: qsTr("Performance"), -+ description: qsTr("Resource cards"), -+ icon: "monitoring" -+ } -+ ] -+ -+ function componentForSection(sectionId) { -+ switch (sectionId) { -+ case "tabs": -+ return tabsComponent; -+ case "performance": -+ return performanceComponent; -+ case "general": -+ default: -+ return generalComponent; -+ } -+ } -+ -+ function performanceOptions() { -+ const options = []; -+ -+ if (root.batteryAvailable) { -+ options.push({ -+ label: qsTr("Battery"), -+ propertyName: "showBattery", -+ onToggled: function (checked) { -+ root.showBattery = checked; -+ root.saveConfig(); -+ } -+ }); -+ } -+ -+ if (root.gpuAvailable) { -+ options.push({ -+ label: qsTr("GPU"), -+ propertyName: "showGpu", -+ onToggled: function (checked) { -+ root.showGpu = checked; -+ root.saveConfig(); -+ } -+ }); -+ } -+ -+ options.push({ -+ label: qsTr("CPU"), -+ propertyName: "showCpu", -+ onToggled: function (checked) { -+ root.showCpu = checked; -+ root.saveConfig(); -+ } -+ }, { -+ label: qsTr("Memory"), -+ propertyName: "showMemory", -+ onToggled: function (checked) { -+ root.showMemory = checked; -+ root.saveConfig(); -+ } -+ }, { -+ label: qsTr("Storage"), -+ propertyName: "showStorage", -+ onToggled: function (checked) { -+ root.showStorage = checked; -+ root.saveConfig(); -+ } -+ }, { -+ label: qsTr("Network"), -+ propertyName: "showNetwork", -+ onToggled: function (checked) { -+ root.showNetwork = checked; -+ root.saveConfig(); -+ } -+ }); -+ -+ return options; -+ } -+ -+ function saveConfig() { -+ GlobalConfig.dashboard.enabled = root.enabled; -+ GlobalConfig.dashboard.showOnHover = root.showOnHover; -+ GlobalConfig.dashboard.mediaUpdateInterval = root.mediaUpdateInterval; -+ GlobalConfig.dashboard.resourceUpdateInterval = root.resourceUpdateInterval; -+ GlobalConfig.dashboard.dragThreshold = root.dragThreshold; -+ GlobalConfig.dashboard.showDashboard = root.showDashboard; -+ GlobalConfig.dashboard.showMedia = root.showMedia; -+ GlobalConfig.dashboard.showPerformance = root.showPerformance; -+ GlobalConfig.dashboard.showWeather = root.showWeather; -+ GlobalConfig.dashboard.performance.showBattery = root.showBattery; -+ GlobalConfig.dashboard.performance.showGpu = root.showGpu; -+ GlobalConfig.dashboard.performance.showCpu = root.showCpu; -+ GlobalConfig.dashboard.performance.showMemory = root.showMemory; -+ GlobalConfig.dashboard.performance.showStorage = root.showStorage; -+ GlobalConfig.dashboard.performance.showNetwork = root.showNetwork; -+ } -+ -+ anchors.fill: parent -+ -+ SplitPaneLayout { -+ anchors.fill: parent -+ leftWidthRatio: 0.32 -+ leftMinimumWidth: 300 -+ -+ leftContent: Component { -+ StyledFlickable { -+ id: leftFlickable -+ -+ flickableDirection: Flickable.VerticalFlick -+ contentHeight: leftContentLayout.height -+ -+ StyledScrollBar.vertical: StyledScrollBar { -+ flickable: leftFlickable -+ } -+ -+ ColumnLayout { -+ id: leftContentLayout -+ -+ anchors.left: parent.left -+ anchors.right: parent.right -+ spacing: Tokens.spacing.normal -+ -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.smaller -+ -+ StyledText { -+ text: qsTr("Dashboard") -+ font.pointSize: Tokens.font.size.large -+ font.weight: 500 -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ } -+ -+ Repeater { -+ model: root.sections -+ -+ delegate: SectionNavButton { -+ required property var modelData -+ -+ Layout.fillWidth: true -+ section: modelData -+ active: root.activeSection === modelData.id -+ onClicked: root.activeSection = modelData.id -+ } -+ } -+ } -+ } -+ } -+ -+ rightContent: Component { -+ Item { -+ id: rightPaneItem -+ -+ property string paneId: root.activeSection -+ property Component targetComponent: root.componentForSection(root.activeSection) -+ property Component nextComponent: root.componentForSection(root.activeSection) -+ -+ onPaneIdChanged: { -+ nextComponent = root.componentForSection(root.activeSection); -+ } -+ -+ Loader { -+ id: rightLoader -+ -+ anchors.fill: parent -+ asynchronous: true -+ opacity: 1 -+ scale: 1 -+ transformOrigin: Item.Center -+ sourceComponent: rightPaneItem.targetComponent -+ } -+ -+ Behavior on paneId { -+ PaneTransition { -+ target: rightLoader -+ propertyActions: [ -+ PropertyAction { -+ target: rightPaneItem -+ property: "targetComponent" -+ value: rightPaneItem.nextComponent -+ } -+ ] -+ } -+ } -+ } -+ } -+ } -+ -+ Component { -+ id: generalComponent -+ -+ SectionPage { -+ title: qsTr("General") -+ subtitle: qsTr("Control dashboard visibility and interaction timing.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SwitchRow { -+ label: qsTr("Enabled") -+ checked: root.enabled -+ onToggled: checked => { -+ root.enabled = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ label: qsTr("Show on hover") -+ checked: root.showOnHover -+ onToggled: checked => { -+ root.showOnHover = checked; -+ root.saveConfig(); -+ } -+ } -+ } -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ contentSpacing: Tokens.spacing.normal -+ -+ SliderInput { -+ Layout.fillWidth: true -+ label: qsTr("Media update interval") -+ value: root.mediaUpdateInterval -+ from: 100 -+ to: 10000 -+ stepSize: 100 -+ suffix: "ms" -+ validator: IntValidator { -+ bottom: 100 -+ top: 10000 -+ } -+ formatValueFunction: val => Math.round(val).toString() -+ parseValueFunction: text => parseInt(text) -+ onValueModified: newValue => { -+ root.mediaUpdateInterval = Math.round(newValue); -+ root.saveConfig(); -+ } -+ } -+ -+ SliderInput { -+ Layout.fillWidth: true -+ label: qsTr("Drag threshold") -+ value: root.dragThreshold -+ from: 0 -+ to: 100 -+ suffix: "px" -+ validator: IntValidator { -+ bottom: 0 -+ top: 100 -+ } -+ formatValueFunction: val => Math.round(val).toString() -+ parseValueFunction: text => parseInt(text) -+ onValueModified: newValue => { -+ root.dragThreshold = Math.round(newValue); -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } -+ -+ Component { -+ id: tabsComponent -+ -+ SectionPage { -+ title: qsTr("Tabs") -+ subtitle: qsTr("Choose which dashboard pages are available.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ ConnectedButtonGroup { -+ rootItem: root -+ rows: 2 -+ options: [ -+ { -+ label: qsTr("Dashboard"), -+ propertyName: "showDashboard", -+ onToggled: function (checked) { -+ root.showDashboard = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Media"), -+ propertyName: "showMedia", -+ onToggled: function (checked) { -+ root.showMedia = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Performance"), -+ propertyName: "showPerformance", -+ onToggled: function (checked) { -+ root.showPerformance = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Weather"), -+ propertyName: "showWeather", -+ onToggled: function (checked) { -+ root.showWeather = checked; -+ root.saveConfig(); -+ } -+ } -+ ] -+ } -+ } -+ } -+ } -+ -+ Component { -+ id: performanceComponent -+ -+ SectionPage { -+ title: qsTr("Performance") -+ subtitle: qsTr("Configure which resource cards the dashboard shows.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ ConnectedButtonGroup { -+ rootItem: root -+ options: root.performanceOptions() -+ } -+ } -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ contentSpacing: Tokens.spacing.normal -+ -+ SliderInput { -+ Layout.fillWidth: true -+ label: qsTr("Resource update interval") -+ value: root.resourceUpdateInterval -+ from: 100 -+ to: 10000 -+ stepSize: 100 -+ suffix: "ms" -+ validator: IntValidator { -+ bottom: 100 -+ top: 10000 -+ } -+ formatValueFunction: val => Math.round(val).toString() -+ parseValueFunction: text => parseInt(text) -+ onValueModified: newValue => { -+ root.resourceUpdateInterval = Math.round(newValue); -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } -+ -+ component SectionPage: StyledFlickable { -+ id: sectionPage -+ -+ required property string title -+ property string subtitle: "" -+ default property alias contentItems: contentLayout.data -+ -+ flickableDirection: Flickable.VerticalFlick -+ contentHeight: contentLayout.height -+ -+ StyledScrollBar.vertical: StyledScrollBar { -+ flickable: sectionPage -+ } -+ -+ ColumnLayout { -+ id: contentLayout -+ -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.top: parent.top -+ spacing: Tokens.spacing.normal -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: sectionPage.title -+ font.pointSize: Tokens.font.size.extraLarge -+ font.weight: 600 -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ Layout.bottomMargin: Tokens.spacing.small -+ text: sectionPage.subtitle -+ color: Colours.palette.m3outline -+ visible: text.length > 0 -+ wrapMode: Text.WordWrap -+ } -+ } -+ } -+ -+ component SectionNavButton: StyledRect { -+ id: navButton -+ -+ required property var section -+ property bool active: false -+ -+ signal clicked -+ -+ implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 -+ color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" -+ radius: Tokens.rounding.normal -+ -+ Behavior on color { -+ CAnim {} -+ } -+ -+ StateLayer { -+ onClicked: navButton.clicked() -+ } -+ -+ RowLayout { -+ id: navRow -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.normal -+ -+ MaterialIcon { -+ Layout.alignment: Qt.AlignVCenter -+ text: navButton.section.icon -+ fill: navButton.active ? 1 : 0 -+ color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant -+ font.pointSize: Tokens.font.size.large -+ } -+ -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: 0 -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: navButton.section.title -+ font.weight: navButton.active ? 500 : 400 -+ elide: Text.ElideRight -+ maximumLineCount: 1 -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: navButton.section.description -+ color: Colours.palette.m3outline -+ font.pointSize: Tokens.font.size.small -+ elide: Text.ElideRight -+ maximumLineCount: 1 -+ } -+ } -+ -+ MaterialIcon { -+ Layout.alignment: Qt.AlignVCenter -+ text: "chevron_right" -+ opacity: navButton.active ? 1 : 0 -+ color: Colours.palette.m3primary -+ } -+ } -+ } -+} -diff --git a/modules/controlcenter/dashboard/GeneralSection.qml b/modules/controlcenter/dashboard/GeneralSection.qml -new file mode 100644 -index 00000000..be67eab2 ---- /dev/null -+++ b/modules/controlcenter/dashboard/GeneralSection.qml -@@ -0,0 +1,128 @@ -+import ".." -+import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.components.controls -+import qs.services -+ -+SectionContainer { -+ id: root -+ -+ required property var rootItem -+ -+ Layout.fillWidth: true -+ alignTop: true -+ -+ StyledText { -+ text: qsTr("General Settings") -+ font.pointSize: Tokens.font.size.normal -+ } -+ -+ SwitchRow { -+ label: qsTr("Enabled") -+ checked: root.rootItem.enabled -+ onToggled: checked => { -+ root.rootItem.enabled = checked; -+ root.rootItem.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ label: qsTr("Show on hover") -+ checked: root.rootItem.showOnHover -+ onToggled: checked => { -+ root.rootItem.showOnHover = checked; -+ root.rootItem.saveConfig(); -+ } -+ } -+ -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Show Dashboard tab") -+ checked: root.rootItem.showDashboard -+ onToggled: checked => { -+ root.rootItem.showDashboard = checked; -+ root.rootItem.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Show Media tab") -+ checked: root.rootItem.showMedia -+ onToggled: checked => { -+ root.rootItem.showMedia = checked; -+ root.rootItem.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Show Performance tab") -+ checked: root.rootItem.showPerformance -+ onToggled: checked => { -+ root.rootItem.showPerformance = checked; -+ root.rootItem.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Show Weather tab") -+ checked: root.rootItem.showWeather -+ onToggled: checked => { -+ root.rootItem.showWeather = checked; -+ root.rootItem.saveConfig(); -+ } -+ } -+ } -+ -+ SliderInput { -+ Layout.fillWidth: true -+ -+ label: qsTr("Media update interval") -+ value: root.rootItem.mediaUpdateInterval -+ from: 100 -+ to: 10000 -+ stepSize: 100 -+ suffix: "ms" -+ validator: IntValidator { -+ bottom: 100 -+ top: 10000 -+ } -+ formatValueFunction: val => Math.round(val).toString() -+ parseValueFunction: text => parseInt(text) -+ -+ onValueModified: newValue => { -+ root.rootItem.mediaUpdateInterval = Math.round(newValue); -+ root.rootItem.saveConfig(); -+ } -+ } -+ -+ SliderInput { -+ Layout.fillWidth: true -+ -+ label: qsTr("Drag threshold") -+ value: root.rootItem.dragThreshold -+ from: 0 -+ to: 100 -+ suffix: "px" -+ validator: IntValidator { -+ bottom: 0 -+ top: 100 -+ } -+ formatValueFunction: val => Math.round(val).toString() -+ parseValueFunction: text => parseInt(text) -+ -+ onValueModified: newValue => { -+ root.rootItem.dragThreshold = Math.round(newValue); -+ root.rootItem.saveConfig(); -+ } -+ } -+} -diff --git a/modules/controlcenter/dashboard/PerformanceSection.qml b/modules/controlcenter/dashboard/PerformanceSection.qml -new file mode 100644 -index 00000000..ea6c68b3 ---- /dev/null -+++ b/modules/controlcenter/dashboard/PerformanceSection.qml -@@ -0,0 +1,106 @@ -+import ".." -+import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Services.UPower -+import Caelestia.Config -+import qs.components -+import qs.components.controls -+import qs.services -+ -+SectionContainer { -+ id: root -+ -+ required property var rootItem -+ // GPU toggle is hidden when gpuType is "NONE" (no GPU data available) -+ readonly property bool gpuAvailable: SystemUsage.gpuType !== "NONE" -+ // Battery toggle is hidden when no laptop battery is present -+ readonly property bool batteryAvailable: UPower.displayDevice.isLaptopBattery -+ -+ Layout.fillWidth: true -+ alignTop: true -+ -+ StyledText { -+ text: qsTr("Performance Resources") -+ font.pointSize: Tokens.font.size.normal -+ } -+ -+ ConnectedButtonGroup { -+ rootItem: root.rootItem -+ options: { -+ let opts = []; -+ if (root.batteryAvailable) -+ opts.push({ -+ "label": qsTr("Battery"), -+ "propertyName": "showBattery", -+ "onToggled": function (checked) { -+ root.rootItem.showBattery = checked; -+ root.rootItem.saveConfig(); -+ } -+ }); -+ -+ if (root.gpuAvailable) -+ opts.push({ -+ "label": qsTr("GPU"), -+ "propertyName": "showGpu", -+ "onToggled": function (checked) { -+ root.rootItem.showGpu = checked; -+ root.rootItem.saveConfig(); -+ } -+ }); -+ -+ opts.push({ -+ "label": qsTr("CPU"), -+ "propertyName": "showCpu", -+ "onToggled": function (checked) { -+ root.rootItem.showCpu = checked; -+ root.rootItem.saveConfig(); -+ } -+ }, { -+ "label": qsTr("Memory"), -+ "propertyName": "showMemory", -+ "onToggled": function (checked) { -+ root.rootItem.showMemory = checked; -+ root.rootItem.saveConfig(); -+ } -+ }, { -+ "label": qsTr("Storage"), -+ "propertyName": "showStorage", -+ "onToggled": function (checked) { -+ root.rootItem.showStorage = checked; -+ root.rootItem.saveConfig(); -+ } -+ }, { -+ "label": qsTr("Network"), -+ "propertyName": "showNetwork", -+ "onToggled": function (checked) { -+ root.rootItem.showNetwork = checked; -+ root.rootItem.saveConfig(); -+ } -+ }); -+ return opts; -+ } -+ } -+ -+ SliderInput { -+ Layout.fillWidth: true -+ -+ label: qsTr("Resource update interval") -+ value: root.rootItem.resourceUpdateInterval -+ from: 100 -+ to: 10000 -+ stepSize: 100 -+ suffix: "ms" -+ validator: IntValidator { -+ bottom: 100 -+ top: 10000 -+ } -+ formatValueFunction: val => Math.round(val).toString() -+ parseValueFunction: text => parseInt(text) -+ -+ onValueModified: newValue => { -+ root.rootItem.resourceUpdateInterval = Math.round(newValue); -+ root.rootItem.saveConfig(); -+ } -+ } -+} -diff --git a/modules/controlcenter/launcher/LauncherPane.qml b/modules/controlcenter/launcher/LauncherPane.qml -index 0dd464f1..f2372dbe 100644 ---- a/modules/controlcenter/launcher/LauncherPane.qml -+++ b/modules/controlcenter/launcher/LauncherPane.qml -@@ -3,19 +3,19 @@ pragma ComponentBehavior: Bound - import ".." - import "../components" - import "../../launcher/services" -+import "../../../utils/scripts/fuzzysort.js" as Fuzzy -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config - import qs.utils --import Caelestia --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts --import "../../../utils/scripts/fuzzysort.js" as Fuzzy - - Item { - id: root -@@ -24,35 +24,21 @@ Item { - - property var selectedApp: root.session.launcher.active - property bool hideFromLauncherChecked: false -- -- anchors.fill: parent -- -- onSelectedAppChanged: { -- root.session.launcher.active = root.selectedApp; -- updateToggleState(); -- } -- -- Connections { -- target: root.session.launcher -- function onActiveChanged() { -- root.selectedApp = root.session.launcher.active; -- updateToggleState(); -- } -- } -+ property bool favouriteChecked: false -+ property string searchText: "" -+ property list filteredApps: [] - - function updateToggleState() { - if (!root.selectedApp) { - root.hideFromLauncherChecked = false; -+ root.favouriteChecked = false; - return; - } - - const appId = root.selectedApp.id || root.selectedApp.entry?.id; - -- if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { -- root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); -- } else { -- root.hideFromLauncherChecked = false; -- } -+ root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); -+ root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); - } - - function saveHiddenApps(isHidden) { -@@ -62,7 +48,7 @@ Item { - - const appId = root.selectedApp.id || root.selectedApp.entry?.id; - -- const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; -+ const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; - - if (isHidden) { - if (!hiddenApps.includes(appId)) { -@@ -75,19 +61,9 @@ Item { - } - } - -- Config.launcher.hiddenApps = hiddenApps; -- Config.save(); -- } -- -- AppDb { -- id: allAppsDb -- -- path: `${Paths.state}/apps.sqlite` -- entries: DesktopEntries.applications.values -+ GlobalConfig.launcher.hiddenApps = hiddenApps; - } - -- property string searchText: "" -- - function filterApps(search: string): list { - if (!search || search.trim() === "") { - const apps = []; -@@ -120,12 +96,17 @@ Item { - return results.sort((a, b) => b._score - a._score).map(r => r.obj._item); - } - -- property list filteredApps: [] -- - function updateFilteredApps() { - filteredApps = filterApps(searchText); - } - -+ anchors.fill: parent -+ -+ onSelectedAppChanged: { -+ root.session.launcher.active = root.selectedApp; -+ updateToggleState(); -+ } -+ - onSearchTextChanged: { - updateFilteredApps(); - } -@@ -135,29 +116,47 @@ Item { - } - - Connections { -- target: allAppsDb -+ function onActiveChanged() { -+ root.selectedApp = root.session.launcher.active; -+ updateToggleState(); -+ } -+ -+ target: root.session.launcher -+ } -+ -+ AppDb { -+ id: allAppsDb -+ -+ path: `${Paths.state}/apps.sqlite` -+ favouriteApps: GlobalConfig.launcher.favouriteApps -+ entries: DesktopEntries.applications.values -+ } -+ -+ Connections { - function onAppsChanged() { - updateFilteredApps(); - } -+ -+ target: allAppsDb - } - - SplitPaneLayout { - anchors.fill: parent - - leftContent: Component { -- - ColumnLayout { - id: leftLauncherLayout -+ - anchors.fill: parent - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - RowLayout { -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Launcher") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -169,9 +168,9 @@ Item { - toggled: !root.session.launcher.active - icon: "settings" - accent: "Primary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Launcher settings") - - onClicked: { -@@ -187,9 +186,9 @@ Item { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Applications (%1)").arg(root.searchText ? root.filteredApps.length : allAppsDb.apps.length) -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - -@@ -200,11 +199,11 @@ Item { - - StyledRect { - Layout.fillWidth: true -- Layout.topMargin: Appearance.spacing.normal -- Layout.bottomMargin: Appearance.spacing.small -+ Layout.topMargin: Tokens.spacing.normal -+ Layout.bottomMargin: Tokens.spacing.small - - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - implicitHeight: Math.max(searchIcon.implicitHeight, searchField.implicitHeight, clearIcon.implicitHeight) - -@@ -213,7 +212,7 @@ Item { - - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left -- anchors.leftMargin: Appearance.padding.normal -+ anchors.leftMargin: Tokens.padding.normal - - text: "search" - color: Colours.palette.m3onSurfaceVariant -@@ -224,11 +223,11 @@ Item { - - anchors.left: searchIcon.right - anchors.right: clearIcon.left -- anchors.leftMargin: Appearance.spacing.small -- anchors.rightMargin: Appearance.spacing.small -+ anchors.leftMargin: Tokens.spacing.small -+ anchors.rightMargin: Tokens.spacing.small - -- topPadding: Appearance.padding.normal -- bottomPadding: Appearance.padding.normal -+ topPadding: Tokens.padding.normal -+ bottomPadding: Tokens.padding.normal - - placeholderText: qsTr("Search applications...") - -@@ -242,7 +241,7 @@ Item { - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right -- anchors.rightMargin: Appearance.padding.normal -+ anchors.rightMargin: Tokens.padding.normal - - width: searchField.text ? implicitWidth : implicitWidth / 2 - opacity: { -@@ -270,13 +269,13 @@ Item { - - Behavior on width { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -@@ -284,8 +283,10 @@ Item { - - Loader { - id: appsListLoader -+ - Layout.fillWidth: true - Layout.fillHeight: true -+ asynchronous: true - active: true - - sourceComponent: StyledListView { -@@ -295,7 +296,7 @@ Item { - Layout.fillHeight: true - - model: root.filteredApps -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - clip: true - - StyledScrollBar.vertical: StyledScrollBar { -@@ -305,15 +306,20 @@ Item { - delegate: StyledRect { - required property var modelData - -- width: parent ? parent.width : 0 -- - readonly property bool isSelected: root.selectedApp === modelData - -+ width: parent ? parent.width : 0 -+ implicitHeight: 40 -+ - color: isSelected ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - opacity: 0 - -+ Component.onCompleted: { -+ opacity = 1; -+ } -+ - Behavior on opacity { - NumberAnimation { - duration: 1000 -@@ -321,12 +327,8 @@ Item { - } - } - -- Component.onCompleted: { -- opacity = 1; -- } -- - StateLayer { -- function onClicked(): void { -+ onClicked: { - root.session.launcher.active = modelData; - } - } -@@ -335,11 +337,12 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - IconImage { -+ asynchronous: true - Layout.alignment: Qt.AlignVCenter - implicitSize: 32 - source: { -@@ -351,11 +354,40 @@ Item { - StyledText { - Layout.fillWidth: true - text: modelData.name || modelData.entry?.name || qsTr("Unknown") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } -- } - -- implicitHeight: 40 -+ Loader { -+ readonly property bool isHidden: modelData ? Strings.testRegexList(GlobalConfig.launcher.hiddenApps, modelData.id) : false -+ readonly property bool isFav: modelData ? Strings.testRegexList(GlobalConfig.launcher.favouriteApps, modelData.id) : false -+ -+ Layout.alignment: Qt.AlignVCenter -+ asynchronous: true -+ active: isHidden || isFav -+ -+ sourceComponent: isHidden ? hiddenIcon : (isFav ? favouriteIcon : null) -+ } -+ -+ Component { -+ id: hiddenIcon -+ -+ MaterialIcon { -+ text: "visibility_off" -+ fill: 1 -+ color: Colours.palette.m3primary -+ } -+ } -+ -+ Component { -+ id: favouriteIcon -+ -+ MaterialIcon { -+ text: "favorite" -+ fill: 1 -+ color: Colours.palette.m3primary -+ } -+ } -+ } - } - } - } -@@ -382,11 +414,30 @@ Item { - nextComponent = targetComponent; - } - -+ onPaneChanged: { -+ nextComponent = getComponentForPane(); -+ paneId = pane ? (pane.id || pane.entry?.id || "") : ""; -+ } -+ -+ onDisplayedAppChanged: { -+ if (displayedApp) { -+ const appId = displayedApp.id || displayedApp.entry?.id; -+ root.hideFromLauncherChecked = GlobalConfig.launcher.hiddenApps && GlobalConfig.launcher.hiddenApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.hiddenApps, appId); -+ root.favouriteChecked = GlobalConfig.launcher.favouriteApps && GlobalConfig.launcher.favouriteApps.length > 0 && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, appId); -+ } else { -+ root.hideFromLauncherChecked = false; -+ root.favouriteChecked = false; -+ } -+ } -+ - Loader { - id: rightLauncherLoader - -+ property var displayedApp: rightLauncherPane.displayedApp -+ - anchors.fill: parent - -+ asynchronous: true - opacity: 1 - scale: 1 - transformOrigin: Item.Center -@@ -395,8 +446,6 @@ Item { - sourceComponent: rightLauncherPane.targetComponent - active: true - -- property var displayedApp: rightLauncherPane.displayedApp -- - onItemChanged: { - if (item && rightLauncherPane.pane && rightLauncherPane.displayedApp !== rightLauncherPane.pane) { - rightLauncherPane.displayedApp = rightLauncherPane.pane; -@@ -431,24 +480,6 @@ Item { - ] - } - } -- -- onPaneChanged: { -- nextComponent = getComponentForPane(); -- paneId = pane ? (pane.id || pane.entry?.id || "") : ""; -- } -- -- onDisplayedAppChanged: { -- if (displayedApp) { -- const appId = displayedApp.id || displayedApp.entry?.id; -- if (Config.launcher.hiddenApps && Config.launcher.hiddenApps.length > 0) { -- root.hideFromLauncherChecked = Config.launcher.hiddenApps.includes(appId); -- } else { -- root.hideFromLauncherChecked = false; -- } -- } else { -- root.hideFromLauncherChecked = false; -- } -- } - } - } - } -@@ -458,6 +489,7 @@ Item { - - StyledFlickable { - id: settingsFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - -@@ -481,16 +513,16 @@ Item { - - ColumnLayout { - id: appDetailsLayout -- anchors.fill: parent - - readonly property var displayedApp: parent && parent.displayedApp !== undefined ? parent.displayedApp : null - -- spacing: Appearance.spacing.normal -+ anchors.fill: parent -+ spacing: Tokens.spacing.normal - - SettingsHeader { -- Layout.leftMargin: Appearance.padding.large * 2 -- Layout.rightMargin: Appearance.padding.large * 2 -- Layout.topMargin: Appearance.padding.large * 2 -+ Layout.leftMargin: Tokens.padding.large * 2 -+ Layout.rightMargin: Tokens.padding.large * 2 -+ Layout.topMargin: Tokens.padding.large * 2 - visible: displayedApp === null - icon: "apps" - title: qsTr("Launcher Applications") -@@ -498,21 +530,23 @@ Item { - - Item { - Layout.alignment: Qt.AlignHCenter -- Layout.leftMargin: Appearance.padding.large * 2 -- Layout.rightMargin: Appearance.padding.large * 2 -- Layout.topMargin: Appearance.padding.large * 2 -+ Layout.leftMargin: Tokens.padding.large * 2 -+ Layout.rightMargin: Tokens.padding.large * 2 -+ Layout.topMargin: Tokens.padding.large * 2 - visible: displayedApp !== null - implicitWidth: Math.max(appIconImage.implicitWidth, appTitleText.implicitWidth) -- implicitHeight: appIconImage.implicitHeight + Appearance.spacing.normal + appTitleText.implicitHeight -+ implicitHeight: appIconImage.implicitHeight + Tokens.spacing.normal + appTitleText.implicitHeight - - ColumnLayout { - anchors.centerIn: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - IconImage { - id: appIconImage -+ -+ asynchronous: true - Layout.alignment: Qt.AlignHCenter -- implicitSize: Appearance.font.size.extraLarge * 3 * 2 -+ implicitSize: Tokens.font.size.extraLarge * 3 * 2 - source: { - const app = appDetailsLayout.displayedApp; - if (!app) -@@ -527,9 +561,10 @@ Item { - - StyledText { - id: appTitleText -+ - Layout.alignment: Qt.AlignHCenter - text: displayedApp ? (displayedApp.name || displayedApp.entry?.name || qsTr("Application Details")) : "" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.bold: true - } - } -@@ -538,12 +573,13 @@ Item { - Item { - Layout.fillWidth: true - Layout.fillHeight: true -- Layout.topMargin: Appearance.spacing.large -- Layout.leftMargin: Appearance.padding.large * 2 -- Layout.rightMargin: Appearance.padding.large * 2 -+ Layout.topMargin: Tokens.spacing.large -+ Layout.leftMargin: Tokens.padding.large * 2 -+ Layout.rightMargin: Tokens.padding.large * 2 - - StyledFlickable { - id: detailsFlickable -+ - anchors.fill: parent - flickableDirection: Flickable.VerticalFlick - contentHeight: debugLayout.height -@@ -554,23 +590,62 @@ Item { - - ColumnLayout { - id: debugLayout -+ - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SwitchRow { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal -+ visible: appDetailsLayout.displayedApp !== null -+ label: qsTr("Mark as favourite") -+ checked: root.favouriteChecked -+ // disabled if: -+ // * app is hidden -+ // * app isn't in favouriteApps array but marked as favourite anyway -+ // ^^^ This means that this app is favourited because of a regex check -+ // this button can not toggle regexed apps -+ enabled: appDetailsLayout.displayedApp !== null && !root.hideFromLauncherChecked && (GlobalConfig.launcher.favouriteApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.favouriteChecked) -+ opacity: enabled ? 1 : 0.6 -+ onToggled: checked => { -+ root.favouriteChecked = checked; -+ const app = appDetailsLayout.displayedApp; -+ if (app) { -+ const appId = app.id || app.entry?.id; -+ const favouriteApps = GlobalConfig.launcher.favouriteApps ? [...GlobalConfig.launcher.favouriteApps] : []; -+ if (checked) { -+ if (!favouriteApps.includes(appId)) { -+ favouriteApps.push(appId); -+ } -+ } else { -+ const index = favouriteApps.indexOf(appId); -+ if (index !== -1) { -+ favouriteApps.splice(index, 1); -+ } -+ } -+ GlobalConfig.launcher.favouriteApps = favouriteApps; -+ } -+ } -+ } -+ SwitchRow { -+ Layout.topMargin: Tokens.spacing.normal - visible: appDetailsLayout.displayedApp !== null - label: qsTr("Hide from launcher") - checked: root.hideFromLauncherChecked -- enabled: appDetailsLayout.displayedApp !== null -+ // disabled if: -+ // * app is favourited -+ // * app isn't in hiddenApps array but marked as hidden anyway -+ // ^^^ This means that this app is hidden because of a regex check -+ // this button can not toggle regexed apps -+ enabled: appDetailsLayout.displayedApp !== null && !root.favouriteChecked && (GlobalConfig.launcher.hiddenApps.indexOf(appDetailsLayout.displayedApp.id || appDetailsLayout.displayedApp.entry?.id) !== -1 || !root.hideFromLauncherChecked) -+ opacity: enabled ? 1 : 0.6 - onToggled: checked => { - root.hideFromLauncherChecked = checked; - const app = appDetailsLayout.displayedApp; - if (app) { - const appId = app.id || app.entry?.id; -- const hiddenApps = Config.launcher.hiddenApps ? [...Config.launcher.hiddenApps] : []; -+ const hiddenApps = GlobalConfig.launcher.hiddenApps ? [...GlobalConfig.launcher.hiddenApps] : []; - if (checked) { - if (!hiddenApps.includes(appId)) { - hiddenApps.push(appId); -@@ -581,8 +656,7 @@ Item { - hiddenApps.splice(index, 1); - } - } -- Config.launcher.hiddenApps = hiddenApps; -- Config.save(); -+ GlobalConfig.launcher.hiddenApps = hiddenApps; - } - } - } -diff --git a/modules/controlcenter/launcher/Settings.qml b/modules/controlcenter/launcher/Settings.qml -index 5eaf6e0e..f0181230 100644 ---- a/modules/controlcenter/launcher/Settings.qml -+++ b/modules/controlcenter/launcher/Settings.qml -@@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property Session session - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "apps" -@@ -23,7 +23,7 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("General") - description: qsTr("General launcher settings") - } -@@ -33,8 +33,7 @@ ColumnLayout { - label: qsTr("Enabled") - checked: Config.launcher.enabled - toggle.onToggled: { -- Config.launcher.enabled = checked; -- Config.save(); -+ GlobalConfig.launcher.enabled = checked; - } - } - -@@ -42,38 +41,35 @@ ColumnLayout { - label: qsTr("Show on hover") - checked: Config.launcher.showOnHover - toggle.onToggled: { -- Config.launcher.showOnHover = checked; -- Config.save(); -+ GlobalConfig.launcher.showOnHover = checked; - } - } - - ToggleRow { - label: qsTr("Vim keybinds") -- checked: Config.launcher.vimKeybinds -+ checked: GlobalConfig.launcher.vimKeybinds - toggle.onToggled: { -- Config.launcher.vimKeybinds = checked; -- Config.save(); -+ GlobalConfig.launcher.vimKeybinds = checked; - } - } - - ToggleRow { - label: qsTr("Enable dangerous actions") -- checked: Config.launcher.enableDangerousActions -+ checked: GlobalConfig.launcher.enableDangerousActions - toggle.onToggled: { -- Config.launcher.enableDangerousActions = checked; -- Config.save(); -+ GlobalConfig.launcher.enableDangerousActions = checked; - } - } - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Display") - description: qsTr("Display and appearance settings") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Max shown items") -@@ -94,28 +90,28 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Prefixes") - description: qsTr("Command prefix settings") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Special prefix") -- value: Config.launcher.specialPrefix || qsTr("None") -+ value: GlobalConfig.launcher.specialPrefix || qsTr("None") - } - - PropertyRow { - showTopMargin: true - label: qsTr("Action prefix") -- value: Config.launcher.actionPrefix || qsTr("None") -+ value: GlobalConfig.launcher.actionPrefix || qsTr("None") - } - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Fuzzy search") - description: qsTr("Fuzzy search settings") - } -@@ -123,95 +119,90 @@ ColumnLayout { - SectionContainer { - ToggleRow { - label: qsTr("Apps") -- checked: Config.launcher.useFuzzy.apps -+ checked: GlobalConfig.launcher.useFuzzy.apps - toggle.onToggled: { -- Config.launcher.useFuzzy.apps = checked; -- Config.save(); -+ GlobalConfig.launcher.useFuzzy.apps = checked; - } - } - - ToggleRow { - label: qsTr("Actions") -- checked: Config.launcher.useFuzzy.actions -+ checked: GlobalConfig.launcher.useFuzzy.actions - toggle.onToggled: { -- Config.launcher.useFuzzy.actions = checked; -- Config.save(); -+ GlobalConfig.launcher.useFuzzy.actions = checked; - } - } - - ToggleRow { - label: qsTr("Schemes") -- checked: Config.launcher.useFuzzy.schemes -+ checked: GlobalConfig.launcher.useFuzzy.schemes - toggle.onToggled: { -- Config.launcher.useFuzzy.schemes = checked; -- Config.save(); -+ GlobalConfig.launcher.useFuzzy.schemes = checked; - } - } - - ToggleRow { - label: qsTr("Variants") -- checked: Config.launcher.useFuzzy.variants -+ checked: GlobalConfig.launcher.useFuzzy.variants - toggle.onToggled: { -- Config.launcher.useFuzzy.variants = checked; -- Config.save(); -+ GlobalConfig.launcher.useFuzzy.variants = checked; - } - } - - ToggleRow { - label: qsTr("Wallpapers") -- checked: Config.launcher.useFuzzy.wallpapers -+ checked: GlobalConfig.launcher.useFuzzy.wallpapers - toggle.onToggled: { -- Config.launcher.useFuzzy.wallpapers = checked; -- Config.save(); -+ GlobalConfig.launcher.useFuzzy.wallpapers = checked; - } - } - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Sizes") - description: qsTr("Size settings for launcher items") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Item width") -- value: qsTr("%1 px").arg(Config.launcher.sizes.itemWidth) -+ value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemWidth) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Item height") -- value: qsTr("%1 px").arg(Config.launcher.sizes.itemHeight) -+ value: qsTr("%1 px").arg(Tokens.sizes.launcher.itemHeight) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Wallpaper width") -- value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperWidth) -+ value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperWidth) - } - - PropertyRow { - showTopMargin: true - label: qsTr("Wallpaper height") -- value: qsTr("%1 px").arg(Config.launcher.sizes.wallpaperHeight) -+ value: qsTr("%1 px").arg(Tokens.sizes.launcher.wallpaperHeight) - } - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Hidden apps") - description: qsTr("Applications hidden from launcher") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Total hidden") -- value: qsTr("%1").arg(Config.launcher.hiddenApps ? Config.launcher.hiddenApps.length : 0) -+ value: qsTr("%1").arg(GlobalConfig.launcher.hiddenApps ? GlobalConfig.launcher.hiddenApps.length : 0) - } - } - } -diff --git a/modules/controlcenter/monitors/MonitorsPane.qml b/modules/controlcenter/monitors/MonitorsPane.qml -index 8cdc49e9..82809fd9 100644 ---- a/modules/controlcenter/monitors/MonitorsPane.qml -+++ b/modules/controlcenter/monitors/MonitorsPane.qml -@@ -8,7 +8,7 @@ import qs.components.controls - import qs.components.effects - import qs.components.containers - import qs.services --import qs.config -+import Caelestia.Config - import Quickshell - import Quickshell.Hyprland - import QtQuick -@@ -18,9 +18,47 @@ Item { - id: root - - required property Session session -+ readonly property var monitorModel: Hyprctl.monitors -+ -+ function selectMonitor(monitor: var): void { -+ if (!monitor) -+ return; -+ root.session.monitor.active = { -+ id: monitor.id, -+ name: monitor.name -+ }; -+ } -+ -+ function selectedMonitor(): var { -+ const active = root.session.monitor.active; -+ if (!active) -+ return null; -+ -+ for (const monitor of root.monitorModel) { -+ if (monitor.name === active.name || monitor.id === active.id) -+ return monitor; -+ } -+ -+ return null; -+ } -+ -+ function ensureSingleMonitorSelected(): void { -+ if (!root.session.monitor.active && (root.monitorModel?.length ?? 0) === 1) -+ root.selectMonitor(root.monitorModel[0]); -+ } - - anchors.fill: parent - -+ Component.onCompleted: Qt.callLater(root.ensureSingleMonitorSelected) -+ -+ Connections { -+ target: Hyprctl -+ -+ function onMonitorsChanged(): void { -+ root.ensureSingleMonitorSelected(); -+ } -+ } -+ - // ── Two-column split (mirrors NetworkingPane) ────────────────────── - SplitPaneLayout { - id: splitLayout -@@ -44,16 +82,16 @@ Item { - - anchors.left: parent.left - anchors.right: parent.right -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - // Header row - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Monitors") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -64,9 +102,9 @@ Item { - toggled: Monitors.identifying - icon: "tv_signin" - accent: "Secondary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Identify monitors") - - onClicked: Monitors.toggleIdentification() -@@ -76,14 +114,14 @@ Item { - // Subtitle - StyledText { - Layout.fillWidth: true -- text: qsTr("%1 display(s) connected").arg(Hyprland.monitors.length) -+ text: qsTr("%1 display(s) connected").arg(root.monitorModel.length) - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - -- // Monitor list — use Hyprland.monitors directly as model -+ // Monitor list — use hyprctl data so refresh rate and modes are available - Repeater { -- model: Hyprland.monitors -+ model: root.monitorModel - - delegate: MonitorListItem { - required property var modelData -@@ -94,9 +132,10 @@ Item { - monitor: modelData - active: root.session.monitor.active !== null - && root.session.monitor.active !== undefined -- && root.session.monitor.active.id === modelData.id -+ && (root.session.monitor.active.id === modelData.id -+ || root.session.monitor.active.name === modelData.name) - -- onClicked: root.session.monitor.active = modelData -+ onClicked: root.selectMonitor(modelData) - } - } - } -@@ -108,7 +147,7 @@ Item { - Item { - id: rightPaneItem - -- property var selectedMonitor: root.session.monitor.active -+ property var selectedMonitor: root.selectedMonitor() - property string paneId: selectedMonitor - ? ("mon:" + (selectedMonitor.name ?? "")) - : "overview" -@@ -127,6 +166,15 @@ Item { - Connections { - target: root.session.monitor - function onActiveChanged(): void { -+ rightPaneItem.selectedMonitor = root.selectedMonitor(); -+ rightPaneItem.nextComponent = rightPaneItem.resolveComponent(); -+ } -+ } -+ -+ Connections { -+ target: Hyprctl -+ function onMonitorsChanged(): void { -+ rightPaneItem.selectedMonitor = root.selectedMonitor(); - rightPaneItem.nextComponent = rightPaneItem.resolveComponent(); - } - } -@@ -166,11 +214,11 @@ Item { - - signal clicked() - -- implicitHeight: itemRow.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: itemRow.implicitHeight + Tokens.padding.normal * 2 - - StyledRect { - anchors.fill: parent -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Qt.alpha( - Colours.tPalette.m3surfaceContainer, - listItem.active ? Colours.tPalette.m3surfaceContainer.a : 0 -@@ -186,14 +234,14 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.normal - - // Monitor icon badge - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: monIcon.implicitHeight + Appearance.padding.normal * 2 -- radius: Appearance.rounding.normal -+ implicitHeight: monIcon.implicitHeight + Tokens.padding.normal * 2 -+ radius: Tokens.rounding.normal - color: listItem.active - ? Colours.palette.m3primaryContainer - : Colours.tPalette.m3surfaceContainerHigh -@@ -202,7 +250,7 @@ Item { - id: monIcon - anchors.centerIn: parent - text: "monitor" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: listItem.active ? 1 : 0 - color: listItem.active - ? Colours.palette.m3onPrimaryContainer -@@ -226,7 +274,7 @@ Item { - StyledText { - Layout.fillWidth: true - elide: Text.ElideRight -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - text: { - const m = listItem.monitor; -@@ -240,16 +288,16 @@ Item { - // Focused badge - StyledRect { - visible: listItem.monitor?.focused ?? false -- implicitWidth: focusedLabel.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: focusedLabel.implicitHeight + Appearance.padding.small * 2 -- radius: Appearance.rounding.full -+ implicitWidth: focusedLabel.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: focusedLabel.implicitHeight + Tokens.padding.small * 2 -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, 0.9) - - StyledText { - id: focusedLabel - anchors.centerIn: parent - text: qsTr("Active") -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onPrimaryContainer - } - } -@@ -284,7 +332,7 @@ Item { - - anchors.left: parent.left - anchors.right: parent.right -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "monitor" -@@ -297,10 +345,10 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small -+ contentSpacing: Tokens.spacing.small - - Repeater { -- model: Hyprland.monitors -+ model: root.monitorModel - - delegate: PropertyRow { - required property var modelData -@@ -329,15 +377,15 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: "tv_signin" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - color: Colours.palette.m3onSurfaceVariant - } - -@@ -346,12 +394,12 @@ Item { - spacing: 0 - StyledText { - text: qsTr("Identify displays") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - StyledText { - text: qsTr("Show monitor IDs on each screen") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - -@@ -381,7 +429,7 @@ Item { - flickable: detailFlickable - } - -- readonly property var mon: root.session.monitor.active -+ readonly property var mon: root.selectedMonitor() - readonly property var brightnessMon: mon ? Brightness.getMonitor(mon.name) : null - - ColumnLayout { -@@ -389,7 +437,7 @@ Item { - - anchors.left: parent.left - anchors.right: parent.right -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - // ── Header ────────────────────────────────────────── - ConnectionHeader { -@@ -402,7 +450,7 @@ Item { - Layout.fillWidth: true - visible: detailFlickable.brightnessMon !== null - && detailFlickable.brightnessMon !== undefined -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Brightness") -@@ -410,22 +458,22 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: (detailFlickable.brightnessMon?.brightness ?? 0) > 0.5 - ? "brightness_high" : "brightness_low" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - - StyledSlider { - Layout.fillWidth: true -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - from: 0; to: 1; stepSize: 0.01 - value: detailFlickable.brightnessMon?.brightness ?? 0 - onMoved: detailFlickable.brightnessMon?.setBrightness(value) -@@ -435,17 +483,96 @@ Item { - text: qsTr("%1%").arg( - Math.round((detailFlickable.brightnessMon?.brightness ?? 0) * 100)) - Layout.preferredWidth: 38 -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - } - } - } - } - -+ // ── Refresh rate ───────────────────────────────────── -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ -+ SectionHeader { -+ title: qsTr("Refresh rate") -+ description: qsTr("Set the display refresh rate") -+ } -+ -+ SectionContainer { -+ contentSpacing: Tokens.spacing.normal -+ -+ SpinBoxRow { -+ Layout.fillWidth: true -+ label: qsTr("Refresh rate") -+ value: detailFlickable.mon?.refreshRate ?? 60 -+ min: 10 -+ max: 1000 -+ step: 0.01 -+ onValueModified: value => { -+ if (detailFlickable.mon) -+ Monitors.setRefreshRate(detailFlickable.mon.name, value); -+ } -+ } -+ -+ RowLayout { -+ Layout.fillWidth: true -+ visible: (detailFlickable.mon?.availableModes?.length ?? 0) > 0 -+ spacing: Tokens.spacing.small -+ -+ Repeater { -+ model: detailFlickable.mon?.availableModes ?? [] -+ -+ delegate: StyledRect { -+ required property string modelData -+ required property int index -+ -+ Layout.fillWidth: true -+ implicitHeight: modeLabel.implicitHeight + Tokens.padding.normal * 2 -+ radius: Tokens.rounding.full -+ -+ readonly property real modeRate: { -+ const match = modelData.match(/@(\d+(?:\.\d+)?)Hz/); -+ return match ? parseFloat(match[1]) : 0; -+ } -+ readonly property bool isActive: Math.abs((detailFlickable.mon?.refreshRate ?? 0) - modeRate) < 0.1 -+ -+ color: isActive -+ ? Colours.palette.m3secondaryContainer -+ : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) -+ -+ StateLayer { -+ color: parent.isActive -+ ? Colours.palette.m3onSecondaryContainer -+ : Colours.palette.m3onSurfaceVariant -+ onClicked: { -+ if (detailFlickable.mon && parent.modeRate > 0) -+ Monitors.setRefreshRate(detailFlickable.mon.name, parent.modeRate); -+ } -+ } -+ -+ StyledText { -+ id: modeLabel -+ anchors.centerIn: parent -+ text: modelData -+ font.pointSize: Tokens.font.size.small -+ color: parent.isActive -+ ? Colours.palette.m3onSecondaryContainer -+ : Colours.palette.m3onSurfaceVariant -+ } -+ -+ Behavior on color { CAnim {} } -+ } -+ } -+ } -+ } -+ } -+ - // ── Rotation ───────────────────────────────────────── - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Rotation") -@@ -453,11 +580,11 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small -+ contentSpacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Repeater { - model: [ -@@ -488,7 +615,7 @@ Item { - // ── Scale ──────────────────────────────────────────── - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Scale") -@@ -496,22 +623,22 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: "zoom_in" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - - StyledSlider { - id: scaleSlider - Layout.fillWidth: true -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - from: 0.5; to: 3.0; stepSize: 0.25 - value: detailFlickable.mon?.scale ?? 1 - -@@ -524,7 +651,7 @@ Item { - StyledText { - text: qsTr("×%1").arg((detailFlickable.mon?.scale ?? 1).toFixed(2)) - Layout.preferredWidth: 42 -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - } - } -@@ -532,7 +659,7 @@ Item { - // Quick-pick chips: 1×, 1.25×, 1.5×, 2× - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Repeater { - model: [1.0, 1.25, 1.5, 2.0] -@@ -542,8 +669,8 @@ Item { - required property int index - - Layout.fillWidth: true -- implicitHeight: scaleChipLabel.implicitHeight + Appearance.padding.normal * 2 -- radius: Appearance.rounding.full -+ implicitHeight: scaleChipLabel.implicitHeight + Tokens.padding.normal * 2 -+ radius: Tokens.rounding.full - - readonly property bool isActive: - Math.abs((detailFlickable.mon?.scale ?? 1) - modelData) < 0.01 -@@ -566,7 +693,7 @@ Item { - id: scaleChipLabel - anchors.centerIn: parent - text: qsTr("×%1").arg(modelData.toFixed(2)) -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: parent.isActive - ? Colours.palette.m3onSecondaryContainer - : Colours.palette.m3onSurfaceVariant -@@ -582,8 +709,8 @@ Item { - // ── Arrangement ────────────────────────────────────── - ColumnLayout { - Layout.fillWidth: true -- visible: Hyprland.monitors.length > 1 -- spacing: Appearance.spacing.normal -+ visible: root.monitorModel.length > 1 -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Arrangement") -@@ -592,14 +719,16 @@ Item { - - // One card per OTHER monitor — use visible to skip self - Repeater { -- model: Hyprland.monitors -+ model: root.monitorModel - - delegate: SectionContainer { -+ id: targetSection -+ - required property var modelData - required property int index - - Layout.fillWidth: true -- contentSpacing: Appearance.spacing.small -+ contentSpacing: Tokens.spacing.small - - // Hide the current monitor's own entry without JS filter - visible: detailFlickable.mon !== null -@@ -609,11 +738,11 @@ Item { - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - MaterialIcon { - text: "tv" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - -@@ -622,15 +751,15 @@ Item { - text: qsTr("Relative to Monitor %1 (%2)") - .arg(modelData.id ?? 0) - .arg(modelData.name ?? "") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - } - - GridLayout { - Layout.fillWidth: true - columns: 4 -- columnSpacing: Appearance.spacing.small -- rowSpacing: Appearance.spacing.small -+ columnSpacing: Tokens.spacing.small -+ rowSpacing: Tokens.spacing.small - - Repeater { - model: [ -@@ -652,7 +781,7 @@ Item { - Monitors.arrange( - detailFlickable.mon.name, - modelData.pos, -- parent.parent.parent.modelData.id -+ targetSection.modelData.id - ); - } - } -@@ -665,7 +794,7 @@ Item { - // ── Display information ─────────────────────────────── - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Display information") -@@ -673,7 +802,7 @@ Item { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Name") -@@ -756,11 +885,11 @@ Item { - required property bool isActive - signal clicked() - -- implicitHeight: chipContent.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: chipContent.implicitHeight + Tokens.padding.normal * 2 - - StyledRect { - anchors.fill: parent -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: chip.isActive - ? Colours.palette.m3secondaryContainer - : Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) -@@ -781,7 +910,7 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: "screen_rotation" - rotation: chip.chipAngle -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - color: chip.isActive - ? Colours.palette.m3onSecondaryContainer - : Colours.palette.m3onSurfaceVariant -@@ -791,7 +920,7 @@ Item { - StyledText { - Layout.alignment: Qt.AlignHCenter - text: chip.chipLabel -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: chip.isActive - ? Colours.palette.m3onSecondaryContainer - : Colours.palette.m3onSurfaceVariant -@@ -808,11 +937,11 @@ Item { - required property string btnLabel - signal clicked() - -- implicitHeight: btnContent.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: btnContent.implicitHeight + Tokens.padding.normal * 2 - - StyledRect { - anchors.fill: parent -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Qt.alpha(Colours.palette.m3surfaceVariant, 0.5) - - StateLayer { -@@ -828,14 +957,14 @@ Item { - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: arrangeBtn.btnIcon -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: arrangeBtn.btnLabel -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - } -diff --git a/modules/controlcenter/network/EthernetDetails.qml b/modules/controlcenter/network/EthernetDetails.qml -index 4e60b3d4..1daeb4a2 100644 ---- a/modules/controlcenter/network/EthernetDetails.qml -+++ b/modules/controlcenter/network/EthernetDetails.qml -@@ -2,14 +2,14 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - DeviceDetails { - id: root -@@ -43,7 +43,7 @@ DeviceDetails { - sections: [ - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection status") -@@ -69,7 +69,7 @@ DeviceDetails { - }, - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Device properties") -@@ -77,7 +77,7 @@ DeviceDetails { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Interface") -@@ -100,7 +100,7 @@ DeviceDetails { - }, - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection information") -diff --git a/modules/controlcenter/network/EthernetList.qml b/modules/controlcenter/network/EthernetList.qml -index d1eb9579..3a947c86 100644 ---- a/modules/controlcenter/network/EthernetList.qml -+++ b/modules/controlcenter/network/EthernetList.qml -@@ -2,13 +2,13 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - DeviceList { - id: root -@@ -23,11 +23,11 @@ DeviceList { - - headerComponent: Component { - RowLayout { -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Settings") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -39,9 +39,9 @@ DeviceList { - toggled: !root.session.ethernet.active - icon: "settings" - accent: "Primary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - - onClicked: { - if (root.session.ethernet.active) -@@ -62,15 +62,15 @@ DeviceList { - readonly property bool isActive: root.activeItem && modelData && root.activeItem.interface === modelData.interface - - width: ListView.view ? ListView.view.width : undefined -- implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, ethernetItem.isActive ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - StateLayer { - id: stateLayer - -- function onClicked(): void { -+ onClicked: { - root.session.ethernet.active = modelData; - } - } -@@ -79,15 +79,15 @@ DeviceList { - id: rowLayout - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: modelData.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - StyledRect { -@@ -101,7 +101,7 @@ DeviceList { - - anchors.centerIn: parent - text: "cable" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: modelData.connected ? 1 : 0 - color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - -@@ -124,13 +124,13 @@ DeviceList { - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: modelData.connected ? qsTr("Connected") : qsTr("Disconnected") - color: modelData.connected ? Colours.palette.m3primary : Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - font.weight: modelData.connected ? 500 : 400 - elide: Text.ElideRight - } -@@ -141,21 +141,21 @@ DeviceList { - id: connectBtn - - implicitWidth: implicitHeight -- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.connected ? 1 : 0) - - StateLayer { -- color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface -- -- function onClicked(): void { -+ onClicked: { - if (modelData.connected && modelData.connection) { - Nmcli.disconnectEthernet(modelData.connection, () => {}); - } else { - Nmcli.connectEthernet(modelData.connection || "", modelData.interface || "", () => {}); - } - } -+ -+ color: modelData.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } - - MaterialIcon { -diff --git a/modules/controlcenter/network/EthernetPane.qml b/modules/controlcenter/network/EthernetPane.qml -index 59d82bb0..d66620f1 100644 ---- a/modules/controlcenter/network/EthernetPane.qml -+++ b/modules/controlcenter/network/EthernetPane.qml -@@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components - import qs.components.containers --import qs.config --import Quickshell.Widgets --import QtQuick - - SplitPaneWithDetails { - id: root -diff --git a/modules/controlcenter/network/EthernetSettings.qml b/modules/controlcenter/network/EthernetSettings.qml -index 90bfcf46..f13b6fca 100644 ---- a/modules/controlcenter/network/EthernetSettings.qml -+++ b/modules/controlcenter/network/EthernetSettings.qml -@@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property Session session - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "cable" -@@ -23,9 +23,9 @@ ColumnLayout { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - text: qsTr("Ethernet devices") -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - -@@ -36,9 +36,9 @@ ColumnLayout { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: ethernetInfo.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: ethernetInfo.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { -@@ -47,9 +47,9 @@ ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { - text: qsTr("Total devices") -@@ -58,18 +58,18 @@ ColumnLayout { - StyledText { - text: qsTr("%1").arg(Nmcli.ethernetDevices.length) - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("Connected devices") - } - - StyledText { - text: qsTr("%1").arg(Nmcli.ethernetDevices.filter(d => d.connected).length) - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - } -diff --git a/modules/controlcenter/network/NetworkSettings.qml b/modules/controlcenter/network/NetworkSettings.qml -index bda7cb18..d8700ecd 100644 ---- a/modules/controlcenter/network/NetworkSettings.qml -+++ b/modules/controlcenter/network/NetworkSettings.qml -@@ -2,22 +2,22 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Controls -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Controls --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property Session session - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "router" -@@ -25,13 +25,13 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Ethernet") - description: qsTr("Ethernet device information") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Total devices") -@@ -46,7 +46,7 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Wireless") - description: qsTr("WiFi network settings") - } -@@ -62,34 +62,33 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("VPN") - description: qsTr("VPN provider settings") -- visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 -+ visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 - } - - SectionContainer { -- visible: Config.utilities.vpn.enabled || Config.utilities.vpn.provider.length > 0 -+ visible: GlobalConfig.utilities.vpn.enabled || GlobalConfig.utilities.vpn.provider.length > 0 - - ToggleRow { - label: qsTr("VPN enabled") -- checked: Config.utilities.vpn.enabled -+ checked: GlobalConfig.utilities.vpn.enabled - toggle.onToggled: { -- Config.utilities.vpn.enabled = checked; -- Config.save(); -+ GlobalConfig.utilities.vpn.enabled = checked; - } - } - - PropertyRow { - showTopMargin: true - label: qsTr("Providers") -- value: qsTr("%1").arg(Config.utilities.vpn.provider.length) -+ value: qsTr("%1").arg(GlobalConfig.utilities.vpn.provider.length) - } - - TextButton { - Layout.fillWidth: true -- Layout.topMargin: Appearance.spacing.normal -- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 -+ Layout.topMargin: Tokens.spacing.normal -+ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - text: qsTr("⚙ Manage VPN Providers") - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer -@@ -101,13 +100,13 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Current connection") - description: qsTr("Active network connection information") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Network") -@@ -141,20 +140,20 @@ ColumnLayout { - - parent: Overlay.overlay - anchors.centerIn: parent -- width: Math.min(600, parent.width - Appearance.padding.large * 2) -- height: Math.min(700, parent.height - Appearance.padding.large * 2) -+ width: Math.min(600, parent.width - Tokens.padding.large * 2) -+ height: Math.min(700, parent.height - Tokens.padding.large * 2) - - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - - background: StyledRect { - color: Colours.palette.m3surface -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - } - - StyledFlickable { - anchors.fill: parent -- anchors.margins: Appearance.padding.large * 1.5 -+ anchors.margins: Tokens.padding.large * 1.5 - flickableDirection: Flickable.VerticalFlick - contentHeight: vpnSettingsContent.height - clip: true -diff --git a/modules/controlcenter/network/NetworkingPane.qml b/modules/controlcenter/network/NetworkingPane.qml -index 26cdbfac..e02bfafb 100644 ---- a/modules/controlcenter/network/NetworkingPane.qml -+++ b/modules/controlcenter/network/NetworkingPane.qml -@@ -3,17 +3,17 @@ pragma ComponentBehavior: Bound - import ".." - import "../components" - import "." -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config - import qs.utils --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts - - Item { - id: root -@@ -43,15 +43,15 @@ Item { - - anchors.left: parent.left - anchors.right: parent.right -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Network") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -63,9 +63,9 @@ Item { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Toggle WiFi") - - onClicked: { -@@ -77,9 +77,9 @@ Item { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Scan for networks") - - onClicked: { -@@ -91,9 +91,9 @@ Item { - toggled: !root.session.ethernet.active && !root.session.network.active - icon: "settings" - accent: "Primary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - tooltip: qsTr("Network settings") - - onClicked: { -@@ -120,6 +120,7 @@ Item { - - Loader { - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: Component { - VpnList { - session: root.session -@@ -138,6 +139,7 @@ Item { - - Loader { - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: Component { - EthernetList { - session: root.session -@@ -156,6 +158,7 @@ Item { - - Loader { - Layout.fillWidth: true -+ asynchronous: true - sourceComponent: Component { - WirelessList { - session: root.session -@@ -196,9 +199,6 @@ Item { - } - - Connections { -- target: root.session && root.session.vpn ? root.session.vpn : null -- enabled: target !== null -- - function onActiveChanged() { - // Clear others when VPN is selected - if (root.session && root.session.vpn && root.session.vpn.active) { -@@ -209,12 +209,12 @@ Item { - } - rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - } -- } - -- Connections { -- target: root.session && root.session.ethernet ? root.session.ethernet : null -+ target: root.session && root.session.vpn ? root.session.vpn : null - enabled: target !== null -+ } - -+ Connections { - function onActiveChanged() { - // Clear others when ethernet is selected - if (root.session && root.session.ethernet && root.session.ethernet.active) { -@@ -225,12 +225,12 @@ Item { - } - rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - } -- } - -- Connections { -- target: root.session && root.session.network ? root.session.network : null -+ target: root.session && root.session.ethernet ? root.session.ethernet : null - enabled: target !== null -+ } - -+ Connections { - function onActiveChanged() { - // Clear others when wireless is selected - if (root.session && root.session.network && root.session.network.active) { -@@ -241,6 +241,9 @@ Item { - } - rightPaneItem.nextComponent = rightPaneItem.getComponentForPane(); - } -+ -+ target: root.session && root.session.network ? root.session.network : null -+ enabled: target !== null - } - - Loader { -@@ -278,6 +281,7 @@ Item { - - StyledFlickable { - id: settingsFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: settingsInner.height - -@@ -301,6 +305,7 @@ Item { - - StyledFlickable { - id: ethernetFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: ethernetDetailsInner.height - -@@ -324,6 +329,7 @@ Item { - - StyledFlickable { - id: wirelessFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: wirelessDetailsInner.height - -@@ -347,6 +353,7 @@ Item { - - StyledFlickable { - id: vpnFlickable -+ - flickableDirection: Flickable.VerticalFlick - contentHeight: vpnDetailsInner.height - -diff --git a/modules/controlcenter/network/VpnDetails.qml b/modules/controlcenter/network/VpnDetails.qml -index 1c71cd71..7ac2dc50 100644 ---- a/modules/controlcenter/network/VpnDetails.qml -+++ b/modules/controlcenter/network/VpnDetails.qml -@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Controls -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config - import qs.utils --import QtQuick --import QtQuick.Controls --import QtQuick.Layouts - - DeviceDetails { - id: root -@@ -21,7 +21,7 @@ DeviceDetails { - readonly property bool providerEnabled: { - if (!vpnProvider || vpnProvider.index === undefined) - return false; -- const provider = Config.utilities.vpn.provider[vpnProvider.index]; -+ const provider = GlobalConfig.utilities.vpn.provider[vpnProvider.index]; - return provider && typeof provider === "object" && provider.enabled === true; - } - -@@ -37,7 +37,7 @@ DeviceDetails { - sections: [ - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection status") -@@ -55,8 +55,8 @@ DeviceDetails { - const index = root.vpnProvider.index; - - // Copy providers and update enabled state -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -- const p = Config.utilities.vpn.provider[i]; -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ const p = GlobalConfig.utilities.vpn.provider[i]; - if (typeof p === "object") { - const newProvider = { - name: p.name, -@@ -72,25 +72,31 @@ DeviceDetails { - newProvider.enabled = (i === index) ? false : (p.enabled !== false); - } - -+ if (p.connectCmd && p.connectCmd.length > 0) { -+ newProvider.connectCmd = p.connectCmd; -+ } -+ if (p.disconnectCmd && p.disconnectCmd.length > 0) { -+ newProvider.disconnectCmd = p.disconnectCmd; -+ } -+ - providers.push(newProvider); - } else { - providers.push(p); - } - } - -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - } - } - - RowLayout { - Layout.fillWidth: true -- Layout.topMargin: Appearance.spacing.normal -- spacing: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal -+ spacing: Tokens.spacing.normal - - TextButton { - Layout.fillWidth: true -- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 -+ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - visible: root.providerEnabled - enabled: !VPN.connecting - inactiveColour: Colours.palette.m3primaryContainer -@@ -109,10 +115,13 @@ DeviceDetails { - inactiveOnColour: Colours.palette.m3onSecondaryContainer - - onClicked: { -+ const provider = GlobalConfig.utilities.vpn.provider[root.vpnProvider.index]; - editVpnDialog.editIndex = root.vpnProvider.index; - editVpnDialog.providerName = root.vpnProvider.name; - editVpnDialog.displayName = root.vpnProvider.displayName; - editVpnDialog.interfaceName = root.vpnProvider.interface; -+ editVpnDialog.connectCmd = (provider && provider.connectCmd) ? provider.connectCmd.join(" ") : ""; -+ editVpnDialog.disconnectCmd = (provider && provider.disconnectCmd) ? provider.disconnectCmd.join(" ") : ""; - editVpnDialog.open(); - } - } -@@ -125,23 +134,46 @@ DeviceDetails { - - onClicked: { - const providers = []; -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i !== root.vpnProvider.index) { -- providers.push(Config.utilities.vpn.provider[i]); -+ providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - } -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - root.session.vpn.active = null; - } - } - } -+ -+ TextButton { -+ Layout.fillWidth: true -+ Layout.topMargin: Tokens.spacing.normal -+ visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl !== "" -+ text: qsTr("Open Login Page") -+ inactiveColour: Colours.palette.m3tertiaryContainer -+ inactiveOnColour: Colours.palette.m3onTertiaryContainer -+ -+ onClicked: { -+ Qt.openUrlExternally(VPN.status.authUrl); -+ } -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ Layout.topMargin: Tokens.spacing.normal -+ visible: root.providerEnabled && VPN.status.state === "needs-auth" && VPN.status.authUrl === "" -+ text: qsTr("Click 'Connect' to generate authentication URL") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ horizontalAlignment: Text.AlignHCenter -+ wrapMode: Text.WordWrap -+ } - } - } - }, - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Provider details") -@@ -149,7 +181,7 @@ DeviceDetails { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Provider") -@@ -176,12 +208,31 @@ DeviceDetails { - return qsTr("Disabled"); - if (VPN.connecting) - return qsTr("Connecting..."); -- if (VPN.connected) -+ -+ switch (VPN.status.state) { -+ case "connected": - return qsTr("Connected"); -- return qsTr("Enabled (Not connected)"); -+ case "disconnected": -+ return qsTr("Disconnected"); -+ case "connecting": -+ return qsTr("Connecting..."); -+ case "needs-auth": -+ return qsTr("Authentication required"); -+ case "error": -+ return qsTr("Error"); -+ default: -+ return qsTr("Unknown"); -+ } - } - } - -+ PropertyRow { -+ visible: VPN.status.reason !== "" -+ showTopMargin: true -+ label: qsTr("Details") -+ value: VPN.status.reason -+ } -+ - PropertyRow { - showTopMargin: true - label: qsTr("Enabled") -@@ -200,11 +251,17 @@ DeviceDetails { - property string providerName: "" - property string displayName: "" - property string interfaceName: "" -+ property string connectCmd: "" -+ property string disconnectCmd: "" -+ -+ function closeWithAnimation(): void { -+ close(); -+ } - - parent: Overlay.overlay - anchors.centerIn: parent -- width: Math.min(400, parent.width - Appearance.padding.large * 2) -- padding: Appearance.padding.large * 1.5 -+ width: Math.min(400, parent.width - Tokens.padding.large * 2) -+ padding: Tokens.padding.large * 1.5 - - modal: true - closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside -@@ -217,15 +274,13 @@ DeviceDetails { - property: "opacity" - from: 0 - to: 1 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - Anim { - property: "scale" - from: 0.7 - to: 1 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - -@@ -234,29 +289,23 @@ DeviceDetails { - property: "opacity" - from: 1 - to: 0 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - Anim { - property: "scale" - from: 1 - to: 0.7 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - -- function closeWithAnimation(): void { -- close(); -- } -- - Overlay.modal: Rectangle { - color: Qt.rgba(0, 0, 0, 0.4 * editVpnDialog.opacity) - } - - background: StyledRect { - color: Colours.palette.m3surfaceContainerHigh -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - - Elevation { - anchors.fill: parent -@@ -267,21 +316,21 @@ DeviceDetails { - } - - contentItem: ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Edit VPN Provider") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller / 2 -+ spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Display Name") -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - -@@ -289,7 +338,7 @@ DeviceDetails { - Layout.fillWidth: true - implicitHeight: 40 - color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - border.width: 1 - border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - -@@ -302,8 +351,9 @@ DeviceDetails { - - StyledTextField { - id: displayNameField -+ - anchors.centerIn: parent -- width: parent.width - Appearance.padding.normal -+ width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: editVpnDialog.displayName - onTextChanged: editVpnDialog.displayName = text -@@ -313,11 +363,11 @@ DeviceDetails { - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller / 2 -+ spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Interface (e.g., wg0, torguard)") -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - -@@ -325,7 +375,7 @@ DeviceDetails { - Layout.fillWidth: true - implicitHeight: 40 - color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - border.width: 1 - border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - -@@ -338,8 +388,9 @@ DeviceDetails { - - StyledTextField { - id: interfaceNameField -+ - anchors.centerIn: parent -- width: parent.width - Appearance.padding.normal -+ width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: editVpnDialog.interfaceName - onTextChanged: editVpnDialog.interfaceName = text -@@ -347,10 +398,86 @@ DeviceDetails { - } - } - -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.smaller / 2 -+ visible: editVpnDialog.connectCmd.length > 0 -+ -+ StyledText { -+ text: qsTr("Connect Command") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ -+ StyledRect { -+ Layout.fillWidth: true -+ implicitHeight: 40 -+ color: connectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -+ radius: Tokens.rounding.small -+ border.width: 1 -+ border.color: connectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) -+ -+ Behavior on color { -+ CAnim {} -+ } -+ Behavior on border.color { -+ CAnim {} -+ } -+ -+ StyledTextField { -+ id: connectCmdFieldEdit -+ -+ anchors.centerIn: parent -+ width: parent.width - Tokens.padding.normal -+ horizontalAlignment: TextInput.AlignLeft -+ text: editVpnDialog.connectCmd -+ onTextChanged: editVpnDialog.connectCmd = text -+ } -+ } -+ } -+ -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.smaller / 2 -+ visible: editVpnDialog.disconnectCmd.length > 0 -+ -+ StyledText { -+ text: qsTr("Disconnect Command") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ -+ StyledRect { -+ Layout.fillWidth: true -+ implicitHeight: 40 -+ color: disconnectCmdFieldEdit.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -+ radius: Tokens.rounding.small -+ border.width: 1 -+ border.color: disconnectCmdFieldEdit.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) -+ -+ Behavior on color { -+ CAnim {} -+ } -+ Behavior on border.color { -+ CAnim {} -+ } -+ -+ StyledTextField { -+ id: disconnectCmdFieldEdit -+ -+ anchors.centerIn: parent -+ width: parent.width - Tokens.padding.normal -+ horizontalAlignment: TextInput.AlignLeft -+ text: editVpnDialog.disconnectCmd -+ onTextChanged: editVpnDialog.disconnectCmd = text -+ } -+ } -+ } -+ - RowLayout { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - TextButton { - Layout.fillWidth: true -@@ -369,24 +496,44 @@ DeviceDetails { - - onClicked: { - const providers = []; -- const oldProvider = Config.utilities.vpn.provider[editVpnDialog.editIndex]; -+ const oldProvider = GlobalConfig.utilities.vpn.provider[editVpnDialog.editIndex]; - const wasEnabled = typeof oldProvider === "object" ? (oldProvider.enabled !== false) : true; - -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i === editVpnDialog.editIndex) { -- providers.push({ -- name: editVpnDialog.providerName, -+ const hasCommands = editVpnDialog.connectCmd.length > 0 && editVpnDialog.disconnectCmd.length > 0; -+ const newProvider = { - displayName: editVpnDialog.displayName || editVpnDialog.interfaceName, -+ enabled: wasEnabled, - interface: editVpnDialog.interfaceName, -- enabled: wasEnabled -- }); -+ name: editVpnDialog.providerName -+ }; -+ -+ if (hasCommands) { -+ newProvider.connectCmd = editVpnDialog.connectCmd.split(" ").filter(s => s.length > 0); -+ newProvider.disconnectCmd = editVpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); -+ } -+ -+ providers.push(newProvider); - } else { -- providers.push(Config.utilities.vpn.provider[i]); -+ const p = GlobalConfig.utilities.vpn.provider[i]; -+ const reconstructed = { -+ displayName: p.displayName, -+ enabled: p.enabled, -+ interface: p.interface, -+ name: p.name -+ }; -+ if (p.connectCmd && p.connectCmd.length > 0) { -+ reconstructed.connectCmd = p.connectCmd; -+ } -+ if (p.disconnectCmd && p.disconnectCmd.length > 0) { -+ reconstructed.disconnectCmd = p.disconnectCmd; -+ } -+ providers.push(reconstructed); - } - } - -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - editVpnDialog.closeWithAnimation(); - } - } -diff --git a/modules/controlcenter/network/VpnList.qml b/modules/controlcenter/network/VpnList.qml -index 81f4a45a..d9266b34 100644 ---- a/modules/controlcenter/network/VpnList.qml -+++ b/modules/controlcenter/network/VpnList.qml -@@ -1,15 +1,15 @@ - pragma ComponentBehavior: Bound - - import ".." -+import QtQuick -+import QtQuick.Controls -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import Quickshell --import QtQuick --import QtQuick.Controls --import QtQuick.Layouts - - ColumnLayout { - id: root -@@ -18,18 +18,17 @@ ColumnLayout { - property bool showHeader: true - property int pendingSwitchIndex: -1 - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Connections { -- target: VPN - function onConnectedChanged() { - if (!VPN.connected && root.pendingSwitchIndex >= 0) { - const targetIndex = root.pendingSwitchIndex; - root.pendingSwitchIndex = -1; - - const providers = []; -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -- const p = Config.utilities.vpn.provider[i]; -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ const p = GlobalConfig.utilities.vpn.provider[i]; - if (typeof p === "object") { - const newProvider = { - name: p.name, -@@ -37,19 +36,26 @@ ColumnLayout { - interface: p.interface, - enabled: (i === targetIndex) - }; -+ if (p.connectCmd && p.connectCmd.length > 0) { -+ newProvider.connectCmd = p.connectCmd; -+ } -+ if (p.disconnectCmd && p.disconnectCmd.length > 0) { -+ newProvider.disconnectCmd = p.disconnectCmd; -+ } - providers.push(newProvider); - } else { - providers.push(p); - } - } -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - - Qt.callLater(function () { - VPN.toggle(); - }); - } - } -+ -+ target: VPN - } - - TextButton { -@@ -70,10 +76,10 @@ ColumnLayout { - Layout.preferredHeight: contentHeight - - interactive: false -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - model: ScriptModel { -- values: Config.utilities.vpn.provider.map((provider, index) => { -+ values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { - const isObject = typeof provider === "object"; - const name = isObject ? (provider.name || "custom") : String(provider); - const displayName = isObject ? (provider.displayName || name) : name; -@@ -97,12 +103,13 @@ ColumnLayout { - required property int index - - width: ListView.view ? ListView.view.width : undefined -+ implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, (root.session && root.session.vpn && root.session.vpn.active === modelData) ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - StateLayer { -- function onClicked(): void { -+ onClicked: { - if (root.session && root.session.vpn) { - root.session.vpn.active = modelData; - } -@@ -115,15 +122,15 @@ ColumnLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: modelData.enabled && VPN.connected ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { -@@ -131,7 +138,7 @@ ColumnLayout { - - anchors.centerIn: parent - text: modelData.enabled && VPN.connected ? "vpn_key" : "vpn_key_off" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: modelData.enabled && VPN.connected ? 1 : 0 - color: modelData.enabled && VPN.connected ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } -@@ -152,21 +159,44 @@ ColumnLayout { - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: { -- if (modelData.enabled && VPN.connected) -+ if (!modelData.enabled) -+ return qsTr("Disabled"); -+ -+ if (VPN.connecting) -+ return qsTr("Connecting..."); -+ -+ switch (VPN.status.state) { -+ case "connected": - return qsTr("Connected"); -- if (modelData.enabled && VPN.connecting) -+ case "disconnected": -+ return qsTr("Enabled"); -+ case "connecting": - return qsTr("Connecting..."); -- if (modelData.enabled) -+ case "needs-auth": -+ return qsTr("Auth required"); -+ case "error": -+ return qsTr("Error"); -+ default: - return qsTr("Enabled"); -- return qsTr("Disabled"); -+ } -+ } -+ color: { -+ if (!modelData.enabled) -+ return Colours.palette.m3outline; -+ if (VPN.status.state === "connected") -+ return Colours.palette.m3primary; -+ if (VPN.status.state === "error") -+ return Colours.palette.m3error; -+ if (VPN.status.state === "needs-auth") -+ return Colours.palette.m3tertiary; -+ return Colours.palette.m3onSurface; - } -- color: modelData.enabled ? (VPN.connected ? Colours.palette.m3primary : Colours.palette.m3onSurface) : Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - font.weight: modelData.enabled && VPN.connected ? 500 : 400 - elide: Text.ElideRight - } -@@ -175,14 +205,13 @@ ColumnLayout { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, VPN.connected && modelData.enabled ? 1 : 0) - - StateLayer { -- enabled: !VPN.connecting -- function onClicked(): void { -+ onClicked: { - const clickedIndex = modelData.index; - - if (modelData.enabled) { -@@ -193,8 +222,8 @@ ColumnLayout { - VPN.toggle(); - } else { - const providers = []; -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -- const p = Config.utilities.vpn.provider[i]; -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ const p = GlobalConfig.utilities.vpn.provider[i]; - if (typeof p === "object") { - const newProvider = { - name: p.name, -@@ -202,13 +231,18 @@ ColumnLayout { - interface: p.interface, - enabled: (i === clickedIndex) - }; -+ if (p.connectCmd && p.connectCmd.length > 0) { -+ newProvider.connectCmd = p.connectCmd; -+ } -+ if (p.disconnectCmd && p.disconnectCmd.length > 0) { -+ newProvider.disconnectCmd = p.disconnectCmd; -+ } - providers.push(newProvider); - } else { - providers.push(p); - } - } -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - - Qt.callLater(function () { - VPN.toggle(); -@@ -216,6 +250,8 @@ ColumnLayout { - } - } - } -+ -+ enabled: !VPN.connecting - } - - MaterialIcon { -@@ -229,21 +265,33 @@ ColumnLayout { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: deleteIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: deleteIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: "transparent" - - StateLayer { -- function onClicked(): void { -+ onClicked: { - const providers = []; -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i !== modelData.index) { -- providers.push(Config.utilities.vpn.provider[i]); -+ const p = GlobalConfig.utilities.vpn.provider[i]; -+ const reconstructed = { -+ name: p.name, -+ displayName: p.displayName, -+ interface: p.interface, -+ enabled: p.enabled -+ }; -+ if (p.connectCmd && p.connectCmd.length > 0) { -+ reconstructed.connectCmd = p.connectCmd; -+ } -+ if (p.disconnectCmd && p.disconnectCmd.length > 0) { -+ reconstructed.disconnectCmd = p.disconnectCmd; -+ } -+ providers.push(reconstructed); - } - } -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - } - } - -@@ -256,8 +304,6 @@ ColumnLayout { - } - } - } -- -- implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 - } - } - } -@@ -270,56 +316,8 @@ ColumnLayout { - property string providerName: "" - property string displayName: "" - property string interfaceName: "" -- -- parent: Overlay.overlay -- x: Math.round((parent.width - width) / 2) -- y: Math.round((parent.height - height) / 2) -- implicitWidth: Math.min(400, parent.width - Appearance.padding.large * 2) -- padding: Appearance.padding.large * 1.5 -- -- modal: true -- closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside -- -- opacity: 0 -- scale: 0.7 -- -- enter: Transition { -- ParallelAnimation { -- Anim { -- property: "opacity" -- from: 0 -- to: 1 -- duration: Appearance.anim.durations.normal -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- Anim { -- property: "scale" -- from: 0.7 -- to: 1 -- duration: Appearance.anim.durations.normal -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- } -- } -- -- exit: Transition { -- ParallelAnimation { -- Anim { -- property: "opacity" -- from: 1 -- to: 0 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- Anim { -- property: "scale" -- from: 1 -- to: 0.7 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- } -- } -+ property string connectCmd: "" -+ property string disconnectCmd: "" - - function showProviderSelection(): void { - currentState = "selection"; -@@ -335,6 +333,8 @@ ColumnLayout { - providerName = providerType; - displayName = defaultDisplayName; - interfaceName = ""; -+ connectCmd = ""; -+ disconnectCmd = ""; - - if (currentState === "selection") { - transitionToForm.start(); -@@ -346,59 +346,77 @@ ColumnLayout { - } - - function showEditForm(index: int): void { -- const provider = Config.utilities.vpn.provider[index]; -+ const provider = GlobalConfig.utilities.vpn.provider[index]; - const isObject = typeof provider === "object"; - - editIndex = index; - providerName = isObject ? (provider.name || "custom") : String(provider); - displayName = isObject ? (provider.displayName || providerName) : providerName; - interfaceName = isObject ? (provider.interface || "") : ""; -+ connectCmd = isObject && provider.connectCmd ? provider.connectCmd.join(" ") : ""; -+ disconnectCmd = isObject && provider.disconnectCmd ? provider.disconnectCmd.join(" ") : ""; - - currentState = "form"; - open(); - } - -- Overlay.modal: Rectangle { -- color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) -- } -+ parent: Overlay.overlay -+ x: Math.round((parent.width - width) / 2) -+ y: Math.round((parent.height - height) / 2) -+ implicitWidth: Math.min(400, parent.width - Tokens.padding.large * 2) -+ padding: Tokens.padding.large * 1.5 - -- onClosed: { -- currentState = "selection"; -- } -+ modal: true -+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside - -- SequentialAnimation { -- id: transitionToForm -+ opacity: 0 -+ scale: 0.7 - -+ enter: Transition { - ParallelAnimation { - Anim { -- target: selectionContent - property: "opacity" -- to: 0 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ from: 0 -+ to: 1 -+ type: Anim.Emphasized - } -- } -- -- ScriptAction { -- script: { -- vpnDialog.currentState = "form"; -+ Anim { -+ property: "scale" -+ from: 0.7 -+ to: 1 -+ type: Anim.Emphasized - } - } -+ } - -+ exit: Transition { - ParallelAnimation { - Anim { -- target: formContent - property: "opacity" -- to: 1 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ from: 1 -+ to: 0 -+ type: Anim.EmphasizedSmall -+ } -+ Anim { -+ property: "scale" -+ from: 1 -+ to: 0.7 -+ type: Anim.EmphasizedSmall - } - } - } - -+ Overlay.modal: Rectangle { -+ color: Qt.rgba(0, 0, 0, 0.4 * vpnDialog.opacity) -+ } -+ -+ onClosed: { -+ currentState = "selection"; -+ } -+ - background: StyledRect { - color: Colours.palette.m3surfaceContainerHigh -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - - Elevation { - anchors.fill: parent -@@ -409,8 +427,7 @@ ColumnLayout { - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.normal -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - } -@@ -420,8 +437,7 @@ ColumnLayout { - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.normal -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - -@@ -429,20 +445,19 @@ ColumnLayout { - id: selectionContent - - anchors.fill: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - visible: vpnDialog.currentState === "selection" - opacity: vpnDialog.currentState === "selection" ? 1 : 0 - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.EmphasizedSmall - } - } - - StyledText { - text: qsTr("Add VPN Provider") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -451,27 +466,26 @@ ColumnLayout { - text: qsTr("Choose a provider to add") - wrapMode: Text.WordWrap - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - TextButton { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true - text: qsTr("NetBird") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - const providers = []; -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -- providers.push(Config.utilities.vpn.provider[i]); -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push({ - name: "netbird", - displayName: "NetBird", - interface: "wt0" - }); -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } -@@ -483,16 +497,15 @@ ColumnLayout { - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - const providers = []; -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -- providers.push(Config.utilities.vpn.provider[i]); -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push({ - name: "tailscale", - displayName: "Tailscale", - interface: "tailscale0" - }); -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } -@@ -504,23 +517,22 @@ ColumnLayout { - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { - const providers = []; -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -- providers.push(Config.utilities.vpn.provider[i]); -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push({ - name: "warp", - displayName: "Cloudflare WARP", - interface: "CloudflareWARP" - }); -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } - - TextButton { - Layout.fillWidth: true -- text: qsTr("WireGuard (Custom)") -+ text: qsTr("WireGuard") - inactiveColour: Colours.tPalette.m3surfaceContainerHigh - inactiveOnColour: Colours.palette.m3onSurface - onClicked: { -@@ -529,7 +541,17 @@ ColumnLayout { - } - - TextButton { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.fillWidth: true -+ text: qsTr("Custom") -+ inactiveColour: Colours.tPalette.m3surfaceContainerHigh -+ inactiveOnColour: Colours.palette.m3onSurface -+ onClicked: { -+ vpnDialog.showAddForm("custom", "Custom VPN"); -+ } -+ } -+ -+ TextButton { -+ Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true - text: qsTr("Cancel") - inactiveColour: Colours.palette.m3secondaryContainer -@@ -542,30 +564,29 @@ ColumnLayout { - id: formContent - - anchors.fill: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - visible: vpnDialog.currentState === "form" - opacity: vpnDialog.currentState === "form" ? 1 : 0 - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.EmphasizedSmall - } - } - - StyledText { - text: vpnDialog.editIndex >= 0 ? qsTr("Edit VPN Provider") : qsTr("Add %1 VPN").arg(vpnDialog.displayName) -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller / 2 -+ spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Display Name") -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - -@@ -573,7 +594,7 @@ ColumnLayout { - Layout.fillWidth: true - implicitHeight: 40 - color: displayNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - border.width: 1 - border.color: displayNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - -@@ -586,8 +607,9 @@ ColumnLayout { - - StyledTextField { - id: displayNameField -+ - anchors.centerIn: parent -- width: parent.width - Appearance.padding.normal -+ width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: vpnDialog.displayName - onTextChanged: vpnDialog.displayName = text -@@ -597,11 +619,11 @@ ColumnLayout { - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller / 2 -+ spacing: Tokens.spacing.smaller / 2 - - StyledText { - text: qsTr("Interface (e.g., wg0, torguard)") -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - -@@ -609,7 +631,7 @@ ColumnLayout { - Layout.fillWidth: true - implicitHeight: 40 - color: interfaceNameField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - border.width: 1 - border.color: interfaceNameField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) - -@@ -622,8 +644,9 @@ ColumnLayout { - - StyledTextField { - id: interfaceNameField -+ - anchors.centerIn: parent -- width: parent.width - Appearance.padding.normal -+ width: parent.width - Tokens.padding.normal - horizontalAlignment: TextInput.AlignLeft - text: vpnDialog.interfaceName - onTextChanged: vpnDialog.interfaceName = text -@@ -631,10 +654,86 @@ ColumnLayout { - } - } - -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.smaller / 2 -+ visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") -+ -+ StyledText { -+ text: qsTr("Connect Command (e.g., wg-quick up wg0)") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ -+ StyledRect { -+ Layout.fillWidth: true -+ implicitHeight: 40 -+ color: connectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -+ radius: Tokens.rounding.small -+ border.width: 1 -+ border.color: connectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) -+ -+ Behavior on color { -+ CAnim {} -+ } -+ Behavior on border.color { -+ CAnim {} -+ } -+ -+ StyledTextField { -+ id: connectCmdField -+ -+ anchors.centerIn: parent -+ width: parent.width - Tokens.padding.normal -+ horizontalAlignment: TextInput.AlignLeft -+ text: vpnDialog.connectCmd -+ onTextChanged: vpnDialog.connectCmd = text -+ } -+ } -+ } -+ -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.smaller / 2 -+ visible: vpnDialog.editIndex >= 0 ? (vpnDialog.connectCmd.length > 0) : (vpnDialog.providerName === "custom") -+ -+ StyledText { -+ text: qsTr("Disconnect Command (e.g., wg-quick down wg0)") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ -+ StyledRect { -+ Layout.fillWidth: true -+ implicitHeight: 40 -+ color: disconnectCmdField.activeFocus ? Colours.layer(Colours.palette.m3surfaceContainer, 3) : Colours.layer(Colours.palette.m3surfaceContainer, 2) -+ radius: Tokens.rounding.small -+ border.width: 1 -+ border.color: disconnectCmdField.activeFocus ? Colours.palette.m3primary : Qt.alpha(Colours.palette.m3outline, 0.3) -+ -+ Behavior on color { -+ CAnim {} -+ } -+ Behavior on border.color { -+ CAnim {} -+ } -+ -+ StyledTextField { -+ id: disconnectCmdField -+ -+ anchors.centerIn: parent -+ width: parent.width - Tokens.padding.normal -+ horizontalAlignment: TextInput.AlignLeft -+ text: vpnDialog.disconnectCmd -+ onTextChanged: vpnDialog.disconnectCmd = text -+ } -+ } -+ } -+ - RowLayout { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - TextButton { - Layout.fillWidth: true -@@ -647,40 +746,85 @@ ColumnLayout { - TextButton { - Layout.fillWidth: true - text: qsTr("Save") -- enabled: vpnDialog.interfaceName.length > 0 -+ enabled: { -+ const hasCommands = vpnDialog.connectCmd.length > 0 || vpnDialog.disconnectCmd.length > 0; -+ if (hasCommands) { -+ return vpnDialog.interfaceName.length > 0 && vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; -+ } -+ return vpnDialog.interfaceName.length > 0; -+ } - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer - - onClicked: { - const providers = []; -+ const hasCommands = vpnDialog.connectCmd.length > 0 && vpnDialog.disconnectCmd.length > 0; - const newProvider = { -- name: vpnDialog.providerName, - displayName: vpnDialog.displayName || vpnDialog.interfaceName, -- interface: vpnDialog.interfaceName -+ enabled: false, -+ interface: vpnDialog.interfaceName, -+ name: vpnDialog.providerName - }; - -+ if (hasCommands) { -+ newProvider.connectCmd = vpnDialog.connectCmd.split(" ").filter(s => s.length > 0); -+ newProvider.disconnectCmd = vpnDialog.disconnectCmd.split(" ").filter(s => s.length > 0); -+ } -+ - if (vpnDialog.editIndex >= 0) { -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -+ const oldProvider = GlobalConfig.utilities.vpn.provider[vpnDialog.editIndex]; -+ if (typeof oldProvider === "object" && oldProvider.enabled !== undefined) { -+ newProvider.enabled = oldProvider.enabled; -+ } -+ -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { - if (i === vpnDialog.editIndex) { - providers.push(newProvider); - } else { -- providers.push(Config.utilities.vpn.provider[i]); -+ providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - } - } else { -- for (let i = 0; i < Config.utilities.vpn.provider.length; i++) { -- providers.push(Config.utilities.vpn.provider[i]); -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ providers.push(GlobalConfig.utilities.vpn.provider[i]); - } - providers.push(newProvider); - } - -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - vpnDialog.closeWithAnimation(); - } - } - } - } - } -+ -+ SequentialAnimation { -+ id: transitionToForm -+ -+ ParallelAnimation { -+ Anim { -+ target: selectionContent -+ property: "opacity" -+ to: 0 -+ type: Anim.EmphasizedSmall -+ } -+ } -+ -+ ScriptAction { -+ script: { -+ vpnDialog.currentState = "form"; -+ } -+ } -+ -+ ParallelAnimation { -+ Anim { -+ target: formContent -+ property: "opacity" -+ to: 1 -+ type: Anim.EmphasizedSmall -+ } -+ } -+ } - } - } -diff --git a/modules/controlcenter/network/VpnSettings.qml b/modules/controlcenter/network/VpnSettings.qml -index 49d801d9..064f32a1 100644 ---- a/modules/controlcenter/network/VpnSettings.qml -+++ b/modules/controlcenter/network/VpnSettings.qml -@@ -2,23 +2,23 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Controls -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import Quickshell --import QtQuick --import QtQuick.Controls --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property Session session - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "vpn_key" -@@ -26,7 +26,7 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("General") - description: qsTr("VPN configuration") - } -@@ -34,32 +34,31 @@ ColumnLayout { - SectionContainer { - ToggleRow { - label: qsTr("VPN enabled") -- checked: Config.utilities.vpn.enabled -+ checked: GlobalConfig.utilities.vpn.enabled - toggle.onToggled: { -- Config.utilities.vpn.enabled = checked; -- Config.save(); -+ GlobalConfig.utilities.vpn.enabled = checked; - } - } - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Providers") - description: qsTr("Manage VPN providers") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ contentSpacing: Tokens.spacing.normal - - ListView { - Layout.fillWidth: true - Layout.preferredHeight: contentHeight - - interactive: false -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - model: ScriptModel { -- values: Config.utilities.vpn.provider.map((provider, index) => { -+ values: GlobalConfig.utilities.vpn.provider.map((provider, index) => { - const isObject = typeof provider === "object"; - const name = isObject ? (provider.name || "custom") : String(provider); - const displayName = isObject ? (provider.displayName || name) : name; -@@ -82,19 +81,20 @@ ColumnLayout { - required property int index - - width: ListView.view ? ListView.view.width : undefined -+ implicitHeight: 60 - color: Colours.tPalette.m3surfaceContainerHigh -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: modelData.isActive ? "vpn_key" : "vpn_key_off" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - color: modelData.isActive ? Colours.palette.m3primary : Colours.palette.m3outline - } - -@@ -109,53 +109,81 @@ ColumnLayout { - - StyledText { - text: qsTr("%1 • %2").arg(modelData.name).arg(modelData.interface || qsTr("No interface")) -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - } - } - - IconButton { - icon: modelData.isActive ? "arrow_downward" : "arrow_upward" -- visible: !modelData.isActive || Config.utilities.vpn.provider.length > 1 -+ visible: !modelData.isActive || GlobalConfig.utilities.vpn.provider.length > 1 - onClicked: { -- if (modelData.isActive && index < Config.utilities.vpn.provider.length - 1) { -+ const providers = []; -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ const p = GlobalConfig.utilities.vpn.provider[i]; -+ const reconstructed = { -+ name: p.name, -+ displayName: p.displayName, -+ interface: p.interface, -+ enabled: p.enabled -+ }; -+ if (p.connectCmd && p.connectCmd.length > 0) { -+ reconstructed.connectCmd = p.connectCmd; -+ } -+ if (p.disconnectCmd && p.disconnectCmd.length > 0) { -+ reconstructed.disconnectCmd = p.disconnectCmd; -+ } -+ providers.push(reconstructed); -+ } -+ -+ if (modelData.isActive && index < providers.length - 1) { - // Move down -- const providers = [...Config.utilities.vpn.provider]; - const temp = providers[index]; - providers[index] = providers[index + 1]; - providers[index + 1] = temp; -- Config.utilities.vpn.provider = providers; -- Config.save(); - } else if (!modelData.isActive) { - // Make active (move to top) -- const providers = [...Config.utilities.vpn.provider]; - const provider = providers.splice(index, 1)[0]; - providers.unshift(provider); -- Config.utilities.vpn.provider = providers; -- Config.save(); - } -+ -+ GlobalConfig.utilities.vpn.provider = providers; - } - } - - IconButton { - icon: "delete" - onClicked: { -- const providers = [...Config.utilities.vpn.provider]; -- providers.splice(index, 1); -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ const providers = []; -+ for (let i = 0; i < GlobalConfig.utilities.vpn.provider.length; i++) { -+ if (i !== index) { -+ const p = GlobalConfig.utilities.vpn.provider[i]; -+ const reconstructed = { -+ name: p.name, -+ displayName: p.displayName, -+ interface: p.interface, -+ enabled: p.enabled -+ }; -+ if (p.connectCmd && p.connectCmd.length > 0) { -+ reconstructed.connectCmd = p.connectCmd; -+ } -+ if (p.disconnectCmd && p.disconnectCmd.length > 0) { -+ reconstructed.disconnectCmd = p.disconnectCmd; -+ } -+ providers.push(reconstructed); -+ } -+ } -+ GlobalConfig.utilities.vpn.provider = providers; - } - } - } -- -- implicitHeight: 60 - } - } - } - - TextButton { - Layout.fillWidth: true -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - text: qsTr("+ Add Provider") - inactiveColour: Colours.palette.m3primaryContainer - inactiveOnColour: Colours.palette.m3onPrimaryContainer -@@ -167,13 +195,13 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Quick Add") - description: qsTr("Add common VPN providers") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.smaller -+ contentSpacing: Tokens.spacing.smaller - - TextButton { - Layout.fillWidth: true -@@ -182,14 +210,13 @@ ColumnLayout { - inactiveOnColour: Colours.palette.m3onSurface - - onClicked: { -- const providers = [...Config.utilities.vpn.provider]; -+ const providers = [...GlobalConfig.utilities.vpn.provider]; - providers.push({ - name: "netbird", - displayName: "NetBird", - interface: "wt0" - }); -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - } - } - -@@ -200,14 +227,13 @@ ColumnLayout { - inactiveOnColour: Colours.palette.m3onSurface - - onClicked: { -- const providers = [...Config.utilities.vpn.provider]; -+ const providers = [...GlobalConfig.utilities.vpn.provider]; - providers.push({ - name: "tailscale", - displayName: "Tailscale", - interface: "tailscale0" - }); -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - } - } - -@@ -218,14 +244,13 @@ ColumnLayout { - inactiveOnColour: Colours.palette.m3onSurface - - onClicked: { -- const providers = [...Config.utilities.vpn.provider]; -+ const providers = [...GlobalConfig.utilities.vpn.provider]; - providers.push({ - name: "warp", - displayName: "Cloudflare WARP", - interface: "CloudflareWARP" - }); -- Config.utilities.vpn.provider = providers; -- Config.save(); -+ GlobalConfig.utilities.vpn.provider = providers; - } - } - } -diff --git a/modules/controlcenter/network/WirelessDetails.qml b/modules/controlcenter/network/WirelessDetails.qml -index e8777cdf..e39744ca 100644 ---- a/modules/controlcenter/network/WirelessDetails.qml -+++ b/modules/controlcenter/network/WirelessDetails.qml -@@ -3,15 +3,15 @@ pragma ComponentBehavior: Bound - import ".." - import "../components" - import "." -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config - import qs.utils --import QtQuick --import QtQuick.Layouts - - DeviceDetails { - id: root -@@ -19,66 +19,12 @@ DeviceDetails { - required property Session session - readonly property var network: root.session.network.active - -- device: network -- -- Component.onCompleted: { -- updateDeviceDetails(); -- checkSavedProfile(); -- } -- -- onNetworkChanged: { -- connectionUpdateTimer.stop(); -- if (network && network.ssid) { -- connectionUpdateTimer.start(); -- } -- updateDeviceDetails(); -- checkSavedProfile(); -- } -- - function checkSavedProfile(): void { - if (network && network.ssid) { - Nmcli.loadSavedConnections(() => {}); - } - } - -- Connections { -- target: Nmcli -- function onActiveChanged() { -- updateDeviceDetails(); -- } -- function onWirelessDeviceDetailsChanged() { -- if (network && network.ssid) { -- const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); -- if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { -- connectionUpdateTimer.stop(); -- } -- } -- } -- } -- -- Timer { -- id: connectionUpdateTimer -- interval: 500 -- repeat: true -- running: network && network.ssid -- onTriggered: { -- if (network) { -- const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); -- if (isActive) { -- if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { -- Nmcli.getWirelessDeviceDetails("", () => {}); -- } else { -- connectionUpdateTimer.stop(); -- } -- } else { -- if (Nmcli.wirelessDeviceDetails !== null) { -- Nmcli.wirelessDeviceDetails = null; -- } -- } -- } -- } -- } -- - function updateDeviceDetails(): void { - if (network && network.ssid) { - const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); -@@ -92,6 +38,22 @@ DeviceDetails { - } - } - -+ device: network -+ -+ Component.onCompleted: { -+ updateDeviceDetails(); -+ checkSavedProfile(); -+ } -+ -+ onNetworkChanged: { -+ connectionUpdateTimer.stop(); -+ if (network && network.ssid) { -+ connectionUpdateTimer.start(); -+ } -+ updateDeviceDetails(); -+ checkSavedProfile(); -+ } -+ - headerComponent: Component { - ConnectionHeader { - icon: root.network?.isSecure ? "lock" : "wifi" -@@ -102,7 +64,7 @@ DeviceDetails { - sections: [ - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection status") -@@ -124,8 +86,8 @@ DeviceDetails { - - TextButton { - Layout.fillWidth: true -- Layout.topMargin: Appearance.spacing.normal -- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 -+ Layout.topMargin: Tokens.spacing.normal -+ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - visible: { - if (!root.network || !root.network.ssid) { - return false; -@@ -150,7 +112,7 @@ DeviceDetails { - }, - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Network properties") -@@ -158,7 +120,7 @@ DeviceDetails { - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("SSID") -@@ -193,7 +155,7 @@ DeviceDetails { - }, - Component { - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SectionHeader { - title: qsTr("Connection information") -@@ -208,4 +170,44 @@ DeviceDetails { - } - } - ] -+ -+ Connections { -+ function onActiveChanged() { -+ updateDeviceDetails(); -+ } -+ function onWirelessDeviceDetailsChanged() { -+ if (network && network.ssid) { -+ const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); -+ if (isActive && Nmcli.wirelessDeviceDetails && Nmcli.wirelessDeviceDetails !== null) { -+ connectionUpdateTimer.stop(); -+ } -+ } -+ } -+ -+ target: Nmcli -+ } -+ -+ Timer { -+ id: connectionUpdateTimer -+ -+ interval: 500 -+ repeat: true -+ running: network && network.ssid -+ onTriggered: { -+ if (network) { -+ const isActive = network.active || (Nmcli.active && Nmcli.active.ssid === network.ssid); -+ if (isActive) { -+ if (!Nmcli.wirelessDeviceDetails || Nmcli.wirelessDeviceDetails === null) { -+ Nmcli.getWirelessDeviceDetails("", () => {}); -+ } else { -+ connectionUpdateTimer.stop(); -+ } -+ } else { -+ if (Nmcli.wirelessDeviceDetails !== null) { -+ Nmcli.wirelessDeviceDetails = null; -+ } -+ } -+ } -+ } -+ } - } -diff --git a/modules/controlcenter/network/WirelessList.qml b/modules/controlcenter/network/WirelessList.qml -index 57a155fd..b6acd079 100644 ---- a/modules/controlcenter/network/WirelessList.qml -+++ b/modules/controlcenter/network/WirelessList.qml -@@ -3,22 +3,28 @@ pragma ComponentBehavior: Bound - import ".." - import "../components" - import "." -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.components.effects - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick --import QtQuick.Layouts - - DeviceList { - id: root - - required property Session session - -+ function checkSavedProfileForNetwork(ssid: string): void { -+ if (ssid && ssid.length > 0) { -+ Nmcli.loadSavedConnections(() => {}); -+ } -+ } -+ - title: qsTr("Networks (%1)").arg(Nmcli.networks.length) - description: qsTr("All available WiFi networks") - activeItem: session.network.active -@@ -28,7 +34,7 @@ DeviceList { - visible: Nmcli.scanning - text: qsTr("Scanning...") - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - -@@ -42,11 +48,11 @@ DeviceList { - - headerComponent: Component { - RowLayout { -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Settings") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -58,9 +64,9 @@ DeviceList { - toggled: Nmcli.wifiEnabled - icon: "wifi" - accent: "Tertiary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - - onClicked: { - Nmcli.toggleWifi(null); -@@ -71,9 +77,9 @@ DeviceList { - toggled: Nmcli.scanning - icon: "wifi_find" - accent: "Secondary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - - onClicked: { - Nmcli.rescanWifi(); -@@ -84,9 +90,9 @@ DeviceList { - toggled: !root.session.network.active - icon: "settings" - accent: "Primary" -- iconSize: Appearance.font.size.normal -- horizontalPadding: Appearance.padding.normal -- verticalPadding: Appearance.padding.smaller -+ iconSize: Tokens.font.size.normal -+ horizontalPadding: Tokens.padding.normal -+ verticalPadding: Tokens.padding.smaller - - onClicked: { - if (root.session.network.active) -@@ -104,12 +110,13 @@ DeviceList { - required property var modelData - - width: ListView.view ? ListView.view.width : undefined -+ implicitHeight: rowLayout.implicitHeight + Tokens.padding.normal * 2 - - color: Qt.alpha(Colours.tPalette.m3surfaceContainer, root.activeItem === modelData ? Colours.tPalette.m3surfaceContainer.a : 0) -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - StateLayer { -- function onClicked(): void { -+ onClicked: { - root.session.network.active = modelData; - if (modelData && modelData.ssid) { - root.checkSavedProfileForNetwork(modelData.ssid); -@@ -123,15 +130,15 @@ DeviceList { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: icon.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: modelData.active ? Colours.palette.m3primaryContainer : Colours.tPalette.m3surfaceContainerHigh - - MaterialIcon { -@@ -139,7 +146,7 @@ DeviceList { - - anchors.centerIn: parent - text: Icons.getNetworkIcon(modelData.strength, modelData.isSecure) -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: modelData.active ? 1 : 0 - color: modelData.active ? Colours.palette.m3onPrimaryContainer : Colours.palette.m3onSurface - } -@@ -160,7 +167,7 @@ DeviceList { - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true -@@ -175,7 +182,7 @@ DeviceList { - return qsTr("Open"); - } - color: modelData.active ? Colours.palette.m3primary : Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - font.weight: modelData.active ? 500 : 400 - elide: Text.ElideRight - } -@@ -184,13 +191,13 @@ DeviceList { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: connectIcon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: connectIcon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Qt.alpha(Colours.palette.m3primaryContainer, modelData.active ? 1 : 0) - - StateLayer { -- function onClicked(): void { -+ onClicked: { - if (modelData.active) { - Nmcli.disconnectFromNetwork(); - } else { -@@ -208,8 +215,6 @@ DeviceList { - } - } - } -- -- implicitHeight: rowLayout.implicitHeight + Appearance.padding.normal * 2 - } - } - -@@ -219,10 +224,4 @@ DeviceList { - checkSavedProfileForNetwork(item.ssid); - } - } -- -- function checkSavedProfileForNetwork(ssid: string): void { -- if (ssid && ssid.length > 0) { -- Nmcli.loadSavedConnections(() => {}); -- } -- } - } -diff --git a/modules/controlcenter/network/WirelessPane.qml b/modules/controlcenter/network/WirelessPane.qml -index 8150af9c..43689176 100644 ---- a/modules/controlcenter/network/WirelessPane.qml -+++ b/modules/controlcenter/network/WirelessPane.qml -@@ -2,11 +2,11 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components - import qs.components.containers --import qs.config --import Quickshell.Widgets --import QtQuick - - SplitPaneWithDetails { - id: root -diff --git a/modules/controlcenter/network/WirelessPasswordDialog.qml b/modules/controlcenter/network/WirelessPasswordDialog.qml -index 7ad5204a..ac88f9fc 100644 ---- a/modules/controlcenter/network/WirelessPasswordDialog.qml -+++ b/modules/controlcenter/network/WirelessPasswordDialog.qml -@@ -2,16 +2,16 @@ pragma ComponentBehavior: Bound - - import ".." - import "." -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick --import QtQuick.Layouts - - Item { - id: root -@@ -29,6 +29,47 @@ Item { - } - - property bool isClosing: false -+ -+ function checkConnectionStatus(): void { -+ if (!root.visible || !connectButton.connecting) { -+ return; -+ } -+ -+ const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); -+ -+ if (isConnected) { -+ connectionSuccessTimer.start(); -+ return; -+ } -+ -+ if (Nmcli.pendingConnection === null && connectButton.connecting) { -+ if (connectionMonitor.repeatCount > 10) { -+ connectionMonitor.stop(); -+ connectButton.connecting = false; -+ connectButton.hasError = true; -+ connectButton.enabled = true; -+ connectButton.text = qsTr("Connect"); -+ passwordContainer.passwordBuffer = ""; -+ if (root.network && root.network.ssid) { -+ Nmcli.forgetNetwork(root.network.ssid); -+ } -+ } -+ } -+ } -+ -+ function closeDialog(): void { -+ if (isClosing) { -+ return; -+ } -+ -+ isClosing = true; -+ passwordContainer.passwordBuffer = ""; -+ connectButton.connecting = false; -+ connectButton.hasError = false; -+ connectButton.text = qsTr("Connect"); -+ connectionMonitor.stop(); -+ } -+ - visible: session.network.showPasswordDialog || isClosing - enabled: session.network.showPasswordDialog && !isClosing - focus: enabled -@@ -58,12 +99,13 @@ Item { - anchors.centerIn: parent - - implicitWidth: 400 -- implicitHeight: content.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: content.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surface - opacity: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0 - scale: root.session.network.showPasswordDialog && !root.isClosing ? 1 : 0.7 -+ Keys.onEscapePressed: closeDialog() - - Behavior on opacity { - Anim {} -@@ -94,28 +136,26 @@ Item { - } - } - -- Keys.onEscapePressed: closeDialog() -- - ColumnLayout { - id: content - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "lock" -- font.pointSize: Appearance.font.size.extraLarge * 2 -+ font.pointSize: Tokens.font.size.extraLarge * 2 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Enter password") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -123,14 +163,14 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: root.network ? qsTr("Network: %1").arg(root.network.ssid) : "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { - id: statusText - - Layout.alignment: Qt.AlignHCenter -- Layout.topMargin: Appearance.spacing.small -+ Layout.topMargin: Tokens.spacing.small - visible: connectButton.connecting || connectButton.hasError - text: { - if (connectButton.hasError) { -@@ -142,24 +182,31 @@ Item { - return ""; - } - color: connectButton.hasError ? Colours.palette.m3error : Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - font.weight: 400 - wrapMode: Text.WordWrap -- Layout.maximumWidth: parent.width - Appearance.padding.large * 2 -+ Layout.maximumWidth: parent.width - Tokens.padding.large * 2 - } - - Item { - id: passwordContainer -- Layout.topMargin: Appearance.spacing.large -- Layout.fillWidth: true -- implicitHeight: Math.max(48, charList.implicitHeight + Appearance.padding.normal * 2) - -+ property string passwordBuffer: "" -+ -+ Layout.topMargin: Tokens.spacing.large -+ Layout.fillWidth: true -+ implicitHeight: Math.max(48, charList.implicitHeight + Tokens.padding.normal * 2) - focus: true - Keys.onPressed: event => { - if (!activeFocus) { - forceActiveFocus(); - } - -+ if (event.key === Qt.Key_Escape) { -+ event.accepted = false; -+ closeDialog(); -+ } -+ - if (connectButton.hasError && event.text && event.text.length > 0) { - connectButton.hasError = false; - } -@@ -177,15 +224,16 @@ Item { - } - event.accepted = true; - } else if (event.text && event.text.length > 0) { -+ if (event.key === Qt.Key_Tab) { -+ event.accepted = false; -+ return; -+ } - passwordBuffer += event.text; - event.accepted = true; - } - } - -- property string passwordBuffer: "" -- - Connections { -- target: root.session.network - function onShowPasswordDialogChanged(): void { - if (root.session.network.showPasswordDialog) { - Qt.callLater(() => { -@@ -195,10 +243,11 @@ Item { - }); - } - } -+ -+ target: root.session.network - } - - Connections { -- target: root - function onVisibleChanged(): void { - if (root.visible) { - Qt.callLater(() => { -@@ -206,11 +255,13 @@ Item { - }); - } - } -+ -+ target: root - } - - StyledRect { - anchors.fill: parent -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: passwordContainer.activeFocus ? Qt.lighter(Colours.tPalette.m3surfaceContainer, 1.05) : Colours.tPalette.m3surfaceContainer - border.width: passwordContainer.activeFocus || connectButton.hasError ? 4 : (root.visible ? 1 : 0) - border.color: { -@@ -237,21 +288,22 @@ Item { - } - - StateLayer { -- hoverEnabled: false -- cursorShape: Qt.IBeamCursor -- -- function onClicked(): void { -+ onClicked: { - passwordContainer.forceActiveFocus(); - } -+ -+ hoverEnabled: false -+ cursorShape: Qt.IBeamCursor - } - - StyledText { - id: placeholder -+ - anchors.centerIn: parent - text: qsTr("Password") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.normal -+ font.family: Tokens.font.family.mono - opacity: passwordContainer.passwordBuffer ? 0 : 1 - - Behavior on opacity { -@@ -266,10 +318,10 @@ Item { - - anchors.centerIn: parent - implicitWidth: fullWidth -- implicitHeight: Appearance.font.size.normal -+ implicitHeight: Tokens.font.size.normal - - orientation: Qt.Horizontal -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - interactive: false - - model: ScriptModel { -@@ -283,7 +335,7 @@ Item { - implicitHeight: charList.implicitHeight - - color: Colours.palette.m3onSurface -- radius: Appearance.rounding.small / 2 -+ radius: Tokens.rounding.small / 2 - - opacity: 0 - scale: 0 -@@ -326,8 +378,7 @@ Item { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -@@ -339,15 +390,15 @@ Item { - } - - RowLayout { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - TextButton { - id: cancelButton - - Layout.fillWidth: true -- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 -+ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - inactiveColour: Colours.palette.m3secondaryContainer - inactiveOnColour: Colours.palette.m3onSecondaryContainer - text: qsTr("Cancel") -@@ -362,7 +413,7 @@ Item { - property bool hasError: false - - Layout.fillWidth: true -- Layout.minimumHeight: Appearance.font.size.normal + Appearance.padding.normal * 2 -+ Layout.minimumHeight: Tokens.font.size.normal + Tokens.padding.normal * 2 - inactiveColour: Colours.palette.m3primary - inactiveOnColour: Colours.palette.m3onPrimary - text: qsTr("Connect") -@@ -414,40 +465,14 @@ Item { - } - } - -- function checkConnectionStatus(): void { -- if (!root.visible || !connectButton.connecting) { -- return; -- } -- -- const isConnected = root.network && Nmcli.active && Nmcli.active.ssid && Nmcli.active.ssid.toLowerCase().trim() === root.network.ssid.toLowerCase().trim(); -- -- if (isConnected) { -- connectionSuccessTimer.start(); -- return; -- } -- -- if (Nmcli.pendingConnection === null && connectButton.connecting) { -- if (connectionMonitor.repeatCount > 10) { -- connectionMonitor.stop(); -- connectButton.connecting = false; -- connectButton.hasError = true; -- connectButton.enabled = true; -- connectButton.text = qsTr("Connect"); -- passwordContainer.passwordBuffer = ""; -- if (root.network && root.network.ssid) { -- Nmcli.forgetNetwork(root.network.ssid); -- } -- } -- } -- } -- - Timer { - id: connectionMonitor -+ -+ property int repeatCount: 0 -+ - interval: 1000 - repeat: true - triggeredOnStart: false -- property int repeatCount: 0 -- - onTriggered: { - repeatCount++; - checkConnectionStatus(); -@@ -462,6 +487,7 @@ Item { - - Timer { - id: connectionSuccessTimer -+ - interval: 500 - onTriggered: { - if (root.visible && Nmcli.active && Nmcli.active.ssid) { -@@ -477,7 +503,6 @@ Item { - } - - Connections { -- target: Nmcli - function onActiveChanged() { - if (root.visible) { - checkConnectionStatus(); -@@ -494,18 +519,7 @@ Item { - Nmcli.forgetNetwork(ssid); - } - } -- } -- -- function closeDialog(): void { -- if (isClosing) { -- return; -- } - -- isClosing = true; -- passwordContainer.passwordBuffer = ""; -- connectButton.connecting = false; -- connectButton.hasError = false; -- connectButton.text = qsTr("Connect"); -- connectionMonitor.stop(); -+ target: Nmcli - } - } -diff --git a/modules/controlcenter/network/WirelessSettings.qml b/modules/controlcenter/network/WirelessSettings.qml -index b4eb391d..f3f1fd4b 100644 ---- a/modules/controlcenter/network/WirelessSettings.qml -+++ b/modules/controlcenter/network/WirelessSettings.qml -@@ -2,20 +2,20 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property Session session - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - SettingsHeader { - icon: "wifi" -@@ -23,7 +23,7 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("WiFi status") - description: qsTr("General WiFi settings") - } -@@ -39,13 +39,13 @@ ColumnLayout { - } - - SectionHeader { -- Layout.topMargin: Appearance.spacing.large -+ Layout.topMargin: Tokens.spacing.large - title: qsTr("Network information") - description: qsTr("Current network connection") - } - - SectionContainer { -- contentSpacing: Appearance.spacing.small / 2 -+ contentSpacing: Tokens.spacing.small / 2 - - PropertyRow { - label: qsTr("Connected network") -diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml -new file mode 100644 -index 00000000..8f04bc3b ---- /dev/null -+++ b/modules/controlcenter/notifications/NotificationsPane.qml -@@ -0,0 +1,607 @@ -+pragma ComponentBehavior: Bound -+ -+import ".." -+import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components -+import qs.components.containers -+import qs.components.controls -+import qs.components.effects -+import qs.services -+ -+Item { -+ id: root -+ -+ required property Session session -+ property string activeSection: "notifications" -+ -+ property bool notificationsExpire: GlobalConfig.notifs.expire ?? true -+ property string notificationsFullscreen: GlobalConfig.notifs.fullscreen ?? "on" -+ property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false -+ property int notificationsDefaultExpireTimeout: GlobalConfig.notifs.defaultExpireTimeout ?? 5000 -+ property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3 -+ -+ property int maxToasts: Config.utilities.maxToasts ?? 4 -+ property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off" -+ property bool chargingChanged: GlobalConfig.utilities.toasts.chargingChanged ?? true -+ property bool gameModeChanged: GlobalConfig.utilities.toasts.gameModeChanged ?? true -+ property bool dndChanged: GlobalConfig.utilities.toasts.dndChanged ?? true -+ property bool audioOutputChanged: GlobalConfig.utilities.toasts.audioOutputChanged ?? true -+ property bool audioInputChanged: GlobalConfig.utilities.toasts.audioInputChanged ?? true -+ property bool capsLockChanged: GlobalConfig.utilities.toasts.capsLockChanged ?? true -+ property bool numLockChanged: GlobalConfig.utilities.toasts.numLockChanged ?? true -+ property bool kbLayoutChanged: GlobalConfig.utilities.toasts.kbLayoutChanged ?? true -+ property bool vpnChanged: GlobalConfig.utilities.toasts.vpnChanged ?? true -+ property bool nowPlaying: GlobalConfig.utilities.toasts.nowPlaying ?? false -+ -+ readonly property var sections: [ -+ { -+ id: "notifications", -+ title: qsTr("Notifications"), -+ description: qsTr("Popup behavior"), -+ icon: "notifications" -+ }, -+ { -+ id: "toastSettings", -+ title: qsTr("Toast Settings"), -+ description: qsTr("Toast visibility"), -+ icon: "toast" -+ }, -+ { -+ id: "toastEvents", -+ title: qsTr("Toast Events"), -+ description: qsTr("Event triggers"), -+ icon: "rule" -+ } -+ ] -+ -+ function componentForSection(sectionId) { -+ switch (sectionId) { -+ case "toastSettings": -+ return toastSettingsComponent; -+ case "toastEvents": -+ return toastEventsComponent; -+ case "notifications": -+ default: -+ return notificationsComponent; -+ } -+ } -+ -+ function saveConfig(): void { -+ GlobalConfig.notifs.expire = root.notificationsExpire; -+ GlobalConfig.notifs.fullscreen = root.notificationsFullscreen; -+ GlobalConfig.notifs.openExpanded = root.notificationsOpenExpanded; -+ GlobalConfig.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; -+ GlobalConfig.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; -+ -+ GlobalConfig.utilities.maxToasts = root.maxToasts; -+ GlobalConfig.utilities.toasts.fullscreen = root.toastsFullscreen; -+ GlobalConfig.utilities.toasts.chargingChanged = root.chargingChanged; -+ GlobalConfig.utilities.toasts.gameModeChanged = root.gameModeChanged; -+ GlobalConfig.utilities.toasts.dndChanged = root.dndChanged; -+ GlobalConfig.utilities.toasts.audioOutputChanged = root.audioOutputChanged; -+ GlobalConfig.utilities.toasts.audioInputChanged = root.audioInputChanged; -+ GlobalConfig.utilities.toasts.capsLockChanged = root.capsLockChanged; -+ GlobalConfig.utilities.toasts.numLockChanged = root.numLockChanged; -+ GlobalConfig.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; -+ GlobalConfig.utilities.toasts.vpnChanged = root.vpnChanged; -+ GlobalConfig.utilities.toasts.nowPlaying = root.nowPlaying; -+ } -+ -+ anchors.fill: parent -+ -+ SplitPaneLayout { -+ anchors.fill: parent -+ leftWidthRatio: 0.32 -+ leftMinimumWidth: 300 -+ -+ leftContent: Component { -+ StyledFlickable { -+ id: leftFlickable -+ -+ flickableDirection: Flickable.VerticalFlick -+ contentHeight: leftContentLayout.height -+ -+ StyledScrollBar.vertical: StyledScrollBar { -+ flickable: leftFlickable -+ } -+ -+ ColumnLayout { -+ id: leftContentLayout -+ anchors.left: parent.left -+ anchors.right: parent.right -+ spacing: Tokens.spacing.normal -+ -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.smaller -+ -+ StyledText { -+ text: qsTr("Notifications") -+ font.pointSize: Tokens.font.size.large -+ font.weight: 500 -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ } -+ -+ Repeater { -+ model: root.sections -+ -+ delegate: SectionNavButton { -+ required property var modelData -+ -+ Layout.fillWidth: true -+ section: modelData -+ active: root.activeSection === modelData.id -+ onClicked: root.activeSection = modelData.id -+ } -+ } -+ } -+ } -+ } -+ -+ rightContent: Component { -+ Item { -+ id: rightPaneItem -+ -+ property string paneId: root.activeSection -+ property Component targetComponent: root.componentForSection(root.activeSection) -+ property Component nextComponent: root.componentForSection(root.activeSection) -+ -+ onPaneIdChanged: { -+ nextComponent = root.componentForSection(root.activeSection); -+ } -+ -+ Loader { -+ id: rightLoader -+ anchors.fill: parent -+ asynchronous: true -+ opacity: 1 -+ scale: 1 -+ transformOrigin: Item.Center -+ sourceComponent: rightPaneItem.targetComponent -+ } -+ -+ Behavior on paneId { -+ PaneTransition { -+ target: rightLoader -+ propertyActions: [ -+ PropertyAction { -+ target: rightPaneItem -+ property: "targetComponent" -+ value: rightPaneItem.nextComponent -+ } -+ ] -+ } -+ } -+ } -+ } -+ } -+ -+ Component { -+ id: notificationsComponent -+ -+ SectionPage { -+ title: qsTr("Notifications") -+ subtitle: qsTr("Configure notification popup behavior.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SplitButtonRow { -+ id: notificationsFullscreenSelector -+ -+ function syncActiveItem(): void { -+ active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem; -+ } -+ -+ label: qsTr("Show in fullscreen") -+ menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem] -+ -+ Component.onCompleted: syncActiveItem() -+ -+ Connections { -+ function onNotificationsFullscreenChanged(): void { -+ notificationsFullscreenSelector.syncActiveItem(); -+ } -+ -+ target: root -+ } -+ -+ MenuItem { -+ id: notificationsFullscreenOffItem -+ -+ text: qsTr("Off") -+ icon: "notifications_off" -+ activeText: qsTr("Off") -+ onClicked: { -+ root.notificationsFullscreen = "off"; -+ root.saveConfig(); -+ } -+ } -+ -+ MenuItem { -+ id: notificationsFullscreenOnItem -+ -+ text: qsTr("On") -+ icon: "notifications" -+ activeText: qsTr("On") -+ onClicked: { -+ root.notificationsFullscreen = "on"; -+ root.saveConfig(); -+ } -+ } -+ } -+ -+ SwitchRow { -+ label: qsTr("Expire automatically") -+ checked: root.notificationsExpire -+ onToggled: checked => { -+ root.notificationsExpire = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ label: qsTr("Open expanded") -+ checked: root.notificationsOpenExpanded -+ onToggled: checked => { -+ root.notificationsOpenExpanded = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SpinBoxRow { -+ label: qsTr("Default timeout") -+ value: root.notificationsDefaultExpireTimeout -+ min: 1000 -+ max: 60000 -+ step: 500 -+ onValueModified: value => { -+ root.notificationsDefaultExpireTimeout = value; -+ root.saveConfig(); -+ } -+ } -+ -+ SpinBoxRow { -+ label: qsTr("Group preview count") -+ value: root.notificationsGroupPreviewNum -+ min: 1 -+ max: 10 -+ step: 1 -+ onValueModified: value => { -+ root.notificationsGroupPreviewNum = value; -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } -+ -+ Component { -+ id: toastSettingsComponent -+ -+ SectionPage { -+ title: qsTr("Toast Settings") -+ subtitle: qsTr("Control when toast notifications are shown.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SplitButtonRow { -+ id: toastFullscreenSelector -+ -+ function syncActiveItem(): void { -+ if (root.toastsFullscreen === "all") { -+ active = toastFullscreenAllItem; -+ return; -+ } -+ -+ if (root.toastsFullscreen === "important") { -+ active = toastFullscreenImportantItem; -+ return; -+ } -+ -+ active = toastFullscreenOffItem; -+ } -+ -+ Layout.fillWidth: true -+ z: expanded ? 100 : 0 -+ label: qsTr("Show in fullscreen") -+ menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem] -+ -+ Component.onCompleted: syncActiveItem() -+ Connections { -+ function onToastsFullscreenChanged(): void { -+ toastFullscreenSelector.syncActiveItem(); -+ } -+ -+ target: root -+ } -+ -+ MenuItem { -+ id: toastFullscreenOffItem -+ text: qsTr("Off") -+ icon: "notifications_off" -+ activeText: qsTr("Off") -+ onClicked: { -+ root.toastsFullscreen = "off"; -+ root.saveConfig(); -+ } -+ } -+ -+ MenuItem { -+ id: toastFullscreenImportantItem -+ text: qsTr("Important") -+ icon: "priority_high" -+ activeText: qsTr("Important") -+ onClicked: { -+ root.toastsFullscreen = "important"; -+ root.saveConfig(); -+ } -+ } -+ -+ MenuItem { -+ id: toastFullscreenAllItem -+ text: qsTr("On") -+ icon: "notifications" -+ activeText: qsTr("On") -+ onClicked: { -+ root.toastsFullscreen = "all"; -+ root.saveConfig(); -+ } -+ } -+ } -+ -+ SpinBoxRow { -+ Layout.fillWidth: true -+ label: qsTr("Visible toasts") -+ value: root.maxToasts -+ min: 1 -+ max: 10 -+ step: 1 -+ onValueModified: value => { -+ root.maxToasts = value; -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } -+ -+ Component { -+ id: toastEventsComponent -+ -+ SectionPage { -+ title: qsTr("Toast Events") -+ subtitle: qsTr("Choose which system events create toasts.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ GridLayout { -+ Layout.fillWidth: true -+ columns: 2 -+ columnSpacing: Tokens.spacing.normal -+ rowSpacing: Tokens.spacing.normal -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Charging changes") -+ checked: root.chargingChanged -+ onToggled: checked => { -+ root.chargingChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Game mode changes") -+ checked: root.gameModeChanged -+ onToggled: checked => { -+ root.gameModeChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Do not disturb") -+ checked: root.dndChanged -+ onToggled: checked => { -+ root.dndChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Audio output changes") -+ checked: root.audioOutputChanged -+ onToggled: checked => { -+ root.audioOutputChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Audio input changes") -+ checked: root.audioInputChanged -+ onToggled: checked => { -+ root.audioInputChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Caps lock changes") -+ checked: root.capsLockChanged -+ onToggled: checked => { -+ root.capsLockChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Num lock changes") -+ checked: root.numLockChanged -+ onToggled: checked => { -+ root.numLockChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Keyboard layout changes") -+ checked: root.kbLayoutChanged -+ onToggled: checked => { -+ root.kbLayoutChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("VPN changes") -+ checked: root.vpnChanged -+ onToggled: checked => { -+ root.vpnChanged = checked; -+ root.saveConfig(); -+ } -+ } -+ -+ SwitchRow { -+ Layout.fillWidth: true -+ label: qsTr("Now playing") -+ checked: root.nowPlaying -+ onToggled: checked => { -+ root.nowPlaying = checked; -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ component SectionPage: StyledFlickable { -+ id: sectionPage -+ -+ required property string title -+ property string subtitle: "" -+ default property alias contentItems: contentLayout.data -+ -+ flickableDirection: Flickable.VerticalFlick -+ contentHeight: contentLayout.height -+ -+ StyledScrollBar.vertical: StyledScrollBar { -+ flickable: sectionPage -+ } -+ -+ ColumnLayout { -+ id: contentLayout -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.top: parent.top -+ spacing: Tokens.spacing.normal -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: sectionPage.title -+ font.pointSize: Tokens.font.size.extraLarge -+ font.weight: 600 -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ Layout.bottomMargin: Tokens.spacing.small -+ text: sectionPage.subtitle -+ color: Colours.palette.m3outline -+ visible: text.length > 0 -+ wrapMode: Text.WordWrap -+ } -+ } -+ } -+ -+ component SectionNavButton: StyledRect { -+ id: navButton -+ -+ required property var section -+ property bool active: false -+ -+ signal clicked -+ -+ implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 -+ color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" -+ radius: Tokens.rounding.normal -+ -+ Behavior on color { -+ CAnim {} -+ } -+ -+ StateLayer { -+ onClicked: navButton.clicked() -+ } -+ -+ RowLayout { -+ id: navRow -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.normal -+ -+ MaterialIcon { -+ Layout.alignment: Qt.AlignVCenter -+ text: navButton.section.icon -+ fill: navButton.active ? 1 : 0 -+ color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant -+ font.pointSize: Tokens.font.size.large -+ } -+ -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: 0 -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: navButton.section.title -+ font.weight: navButton.active ? 500 : 400 -+ elide: Text.ElideRight -+ maximumLineCount: 1 -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: navButton.section.description -+ color: Colours.palette.m3outline -+ font.pointSize: Tokens.font.size.small -+ elide: Text.ElideRight -+ maximumLineCount: 1 -+ } -+ } -+ -+ MaterialIcon { -+ Layout.alignment: Qt.AlignVCenter -+ text: "chevron_right" -+ opacity: navButton.active ? 1 : 0 -+ color: Colours.palette.m3primary -+ } -+ } -+ } -+} -diff --git a/modules/controlcenter/state/BluetoothState.qml b/modules/controlcenter/state/BluetoothState.qml -index 8678672d..9b16bfe5 100644 ---- a/modules/controlcenter/state/BluetoothState.qml -+++ b/modules/controlcenter/state/BluetoothState.qml -@@ -1,5 +1,5 @@ --import Quickshell.Bluetooth - import QtQuick -+import Quickshell.Bluetooth - - QtObject { - id: root -diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml -index d12d1744..c80769ac 100644 ---- a/modules/controlcenter/taskbar/TaskbarPane.qml -+++ b/modules/controlcenter/taskbar/TaskbarPane.qml -@@ -2,24 +2,29 @@ pragma ComponentBehavior: Bound - - import ".." - import "../components" -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components -+import qs.components.containers - import qs.components.controls - import qs.components.effects --import qs.components.containers - import qs.services --import qs.config - import qs.utils --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts - - Item { - id: root - - required property Session session - -+ property string activeSection: "statusIcons" -+ property bool activeWindowCompact: Config.bar.activeWindow.compact ?? false -+ property bool activeWindowInverted: Config.bar.activeWindow.inverted ?? false - property bool clockShowIcon: Config.bar.clock.showIcon ?? true -+ property bool clockBackground: Config.bar.clock.background ?? false -+ property bool clockShowDate: Config.bar.clock.showDate ?? false - property bool persistent: Config.bar.persistent ?? true - property bool showOnHover: Config.bar.showOnHover ?? true - property int dragThreshold: Config.bar.dragThreshold ?? 20 -@@ -38,56 +43,131 @@ Item { - property bool workspacesActiveIndicator: Config.bar.workspaces.activeIndicator ?? true - property bool workspacesOccupiedBg: Config.bar.workspaces.occupiedBg ?? false - property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false -- property bool workspacesPerMonitor: Config.bar.workspaces.perMonitorWorkspaces ?? true -+ property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 -+ property bool workspacesPerMonitor: GlobalConfig.bar.workspaces.perMonitorWorkspaces ?? true - property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true - property bool scrollVolume: Config.bar.scrollActions.volume ?? true - property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true - property bool popoutActiveWindow: Config.bar.popouts.activeWindow ?? true - property bool popoutTray: Config.bar.popouts.tray ?? true - property bool popoutStatusIcons: Config.bar.popouts.statusIcons ?? true -- -- anchors.fill: parent -- -- Component.onCompleted: { -- if (Config.bar.entries) { -- entriesModel.clear(); -- for (let i = 0; i < Config.bar.entries.length; i++) { -- const entry = Config.bar.entries[i]; -- entriesModel.append({ -- id: entry.id, -- enabled: entry.enabled !== false -- }); -- } -+ property list monitorNames: Hypr.monitorNames() -+ property list excludedScreens: Config.bar.excludedScreens ?? [] -+ -+ readonly property var sections: [ -+ { -+ id: "statusIcons", -+ title: qsTr("Status Icons"), -+ description: qsTr("Bar indicator buttons"), -+ icon: "info" -+ }, -+ { -+ id: "workspaces", -+ title: qsTr("Workspaces"), -+ description: qsTr("Workspace button layout"), -+ icon: "workspaces" -+ }, -+ { -+ id: "scrollActions", -+ title: qsTr("Scroll Actions"), -+ description: qsTr("Wheel shortcuts"), -+ icon: "swap_vert" -+ }, -+ { -+ id: "clock", -+ title: qsTr("Clock"), -+ description: qsTr("Date and icon display"), -+ icon: "schedule" -+ }, -+ { -+ id: "behavior", -+ title: qsTr("Bar Behavior"), -+ description: qsTr("Visibility and dragging"), -+ icon: "dock_to_left" -+ }, -+ { -+ id: "activeWindow", -+ title: qsTr("Active Window"), -+ description: qsTr("Window title entry"), -+ icon: "select_window" -+ }, -+ { -+ id: "popouts", -+ title: qsTr("Popouts"), -+ description: qsTr("Hover panels"), -+ icon: "open_in_new" -+ }, -+ { -+ id: "tray", -+ title: qsTr("Tray Settings"), -+ description: qsTr("System tray style"), -+ icon: "apps" -+ }, -+ { -+ id: "monitors", -+ title: qsTr("Monitors"), -+ description: qsTr("Per-screen visibility"), -+ icon: "monitor" -+ } -+ ] -+ -+ function componentForSection(sectionId) { -+ switch (sectionId) { -+ case "workspaces": -+ return workspacesComponent; -+ case "scrollActions": -+ return scrollActionsComponent; -+ case "clock": -+ return clockComponent; -+ case "behavior": -+ return behaviorComponent; -+ case "activeWindow": -+ return activeWindowComponent; -+ case "popouts": -+ return popoutsComponent; -+ case "tray": -+ return trayComponent; -+ case "monitors": -+ return monitorsComponent; -+ case "statusIcons": -+ default: -+ return statusIconsComponent; - } - } - - function saveConfig(entryIndex, entryEnabled) { -- Config.bar.clock.showIcon = root.clockShowIcon; -- Config.bar.persistent = root.persistent; -- Config.bar.showOnHover = root.showOnHover; -- Config.bar.dragThreshold = root.dragThreshold; -- Config.bar.status.showAudio = root.showAudio; -- Config.bar.status.showMicrophone = root.showMicrophone; -- Config.bar.status.showKbLayout = root.showKbLayout; -- Config.bar.status.showNetwork = root.showNetwork; -- Config.bar.status.showWifi = root.showWifi; -- Config.bar.status.showBluetooth = root.showBluetooth; -- Config.bar.status.showBattery = root.showBattery; -- Config.bar.status.showLockStatus = root.showLockStatus; -- Config.bar.tray.background = root.trayBackground; -- Config.bar.tray.compact = root.trayCompact; -- Config.bar.tray.recolour = root.trayRecolour; -- Config.bar.workspaces.shown = root.workspacesShown; -- Config.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; -- Config.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; -- Config.bar.workspaces.showWindows = root.workspacesShowWindows; -- Config.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; -- Config.bar.scrollActions.workspaces = root.scrollWorkspaces; -- Config.bar.scrollActions.volume = root.scrollVolume; -- Config.bar.scrollActions.brightness = root.scrollBrightness; -- Config.bar.popouts.activeWindow = root.popoutActiveWindow; -- Config.bar.popouts.tray = root.popoutTray; -- Config.bar.popouts.statusIcons = root.popoutStatusIcons; -+ GlobalConfig.bar.activeWindow.compact = root.activeWindowCompact; -+ GlobalConfig.bar.activeWindow.inverted = root.activeWindowInverted; -+ GlobalConfig.bar.clock.background = root.clockBackground; -+ GlobalConfig.bar.clock.showDate = root.clockShowDate; -+ GlobalConfig.bar.clock.showIcon = root.clockShowIcon; -+ GlobalConfig.bar.persistent = root.persistent; -+ GlobalConfig.bar.showOnHover = root.showOnHover; -+ GlobalConfig.bar.dragThreshold = root.dragThreshold; -+ GlobalConfig.bar.status.showAudio = root.showAudio; -+ GlobalConfig.bar.status.showMicrophone = root.showMicrophone; -+ GlobalConfig.bar.status.showKbLayout = root.showKbLayout; -+ GlobalConfig.bar.status.showNetwork = root.showNetwork; -+ GlobalConfig.bar.status.showWifi = root.showWifi; -+ GlobalConfig.bar.status.showBluetooth = root.showBluetooth; -+ GlobalConfig.bar.status.showBattery = root.showBattery; -+ GlobalConfig.bar.status.showLockStatus = root.showLockStatus; -+ GlobalConfig.bar.tray.background = root.trayBackground; -+ GlobalConfig.bar.tray.compact = root.trayCompact; -+ GlobalConfig.bar.tray.recolour = root.trayRecolour; -+ GlobalConfig.bar.workspaces.shown = root.workspacesShown; -+ GlobalConfig.bar.workspaces.activeIndicator = root.workspacesActiveIndicator; -+ GlobalConfig.bar.workspaces.occupiedBg = root.workspacesOccupiedBg; -+ GlobalConfig.bar.workspaces.showWindows = root.workspacesShowWindows; -+ GlobalConfig.bar.workspaces.maxWindowIcons = root.workspacesMaxWindowIcons; -+ GlobalConfig.bar.workspaces.perMonitorWorkspaces = root.workspacesPerMonitor; -+ GlobalConfig.bar.scrollActions.workspaces = root.scrollWorkspaces; -+ GlobalConfig.bar.scrollActions.volume = root.scrollVolume; -+ GlobalConfig.bar.scrollActions.brightness = root.scrollBrightness; -+ GlobalConfig.bar.popouts.activeWindow = root.popoutActiveWindow; -+ GlobalConfig.bar.popouts.tray = root.popoutTray; -+ GlobalConfig.bar.popouts.statusIcons = root.popoutStatusIcons; -+ GlobalConfig.bar.excludedScreens = root.excludedScreens; - - const entries = []; - for (let i = 0; i < entriesModel.count; i++) { -@@ -101,547 +181,718 @@ Item { - enabled: enabled - }); - } -- Config.bar.entries = entries; -- Config.save(); -+ GlobalConfig.bar.entries = entries; -+ } -+ -+ anchors.fill: parent -+ -+ Component.onCompleted: { -+ if (Config.bar.entries) { -+ entriesModel.clear(); -+ for (let i = 0; i < Config.bar.entries.length; i++) { -+ const entry = Config.bar.entries[i]; -+ entriesModel.append({ -+ id: entry.id, -+ enabled: entry.enabled !== false -+ }); -+ } -+ } - } - - ListModel { - id: entriesModel - } - -- ClippingRectangle { -- id: taskbarClippingRect -+ SplitPaneLayout { - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -- anchors.leftMargin: 0 -- anchors.rightMargin: Appearance.padding.normal -- -- radius: taskbarBorder.innerRadius -- color: "transparent" -+ leftWidthRatio: 0.32 -+ leftMinimumWidth: 300 - -- Loader { -- id: taskbarLoader -+ leftContent: Component { -+ StyledFlickable { -+ id: leftFlickable - -- anchors.fill: parent -- anchors.margins: Appearance.padding.large + Appearance.padding.normal -- anchors.leftMargin: Appearance.padding.large -- anchors.rightMargin: Appearance.padding.large -+ flickableDirection: Flickable.VerticalFlick -+ contentHeight: leftContentLayout.height - -- sourceComponent: taskbarContentComponent -- } -- } -+ StyledScrollBar.vertical: StyledScrollBar { -+ flickable: leftFlickable -+ } - -- InnerBorder { -- id: taskbarBorder -- leftThickness: 0 -- rightThickness: Appearance.padding.normal -- } -+ ColumnLayout { -+ id: leftContentLayout - -- Component { -- id: taskbarContentComponent -+ anchors.left: parent.left -+ anchors.right: parent.right -+ spacing: Tokens.spacing.normal - -- StyledFlickable { -- id: sidebarFlickable -- flickableDirection: Flickable.VerticalFlick -- contentHeight: sidebarLayout.height -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.smaller - -- StyledScrollBar.vertical: StyledScrollBar { -- flickable: sidebarFlickable -- } -+ StyledText { -+ text: qsTr("Taskbar") -+ font.pointSize: Tokens.font.size.large -+ font.weight: 500 -+ } - -- ColumnLayout { -- id: sidebarLayout -- anchors.left: parent.left -- anchors.right: parent.right -- anchors.top: parent.top -+ Item { -+ Layout.fillWidth: true -+ } -+ } - -- spacing: Appearance.spacing.normal -+ Repeater { -+ model: root.sections - -- RowLayout { -- spacing: Appearance.spacing.smaller -+ delegate: SectionNavButton { -+ required property var modelData - -- StyledText { -- text: qsTr("Taskbar") -- font.pointSize: Appearance.font.size.large -- font.weight: 500 -+ Layout.fillWidth: true -+ section: modelData -+ active: root.activeSection === modelData.id -+ onClicked: root.activeSection = modelData.id -+ } - } - } -+ } -+ } - -- SectionContainer { -- Layout.fillWidth: true -- alignTop: true -+ rightContent: Component { -+ Item { -+ id: rightPaneItem - -- StyledText { -- text: qsTr("Status Icons") -- font.pointSize: Appearance.font.size.normal -- } -+ property string paneId: root.activeSection -+ property Component targetComponent: root.componentForSection(root.activeSection) -+ property Component nextComponent: root.componentForSection(root.activeSection) - -- ConnectedButtonGroup { -- rootItem: root -+ onPaneIdChanged: { -+ nextComponent = root.componentForSection(root.activeSection); -+ } - -- options: [ -- { -- label: qsTr("Speakers"), -- propertyName: "showAudio", -- onToggled: function (checked) { -- root.showAudio = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Microphone"), -- propertyName: "showMicrophone", -- onToggled: function (checked) { -- root.showMicrophone = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Keyboard"), -- propertyName: "showKbLayout", -- onToggled: function (checked) { -- root.showKbLayout = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Network"), -- propertyName: "showNetwork", -- onToggled: function (checked) { -- root.showNetwork = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Wifi"), -- propertyName: "showWifi", -- onToggled: function (checked) { -- root.showWifi = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Bluetooth"), -- propertyName: "showBluetooth", -- onToggled: function (checked) { -- root.showBluetooth = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Battery"), -- propertyName: "showBattery", -- onToggled: function (checked) { -- root.showBattery = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Capslock"), -- propertyName: "showLockStatus", -- onToggled: function (checked) { -- root.showLockStatus = checked; -- root.saveConfig(); -- } -+ Loader { -+ id: rightLoader -+ -+ anchors.fill: parent -+ asynchronous: true -+ opacity: 1 -+ scale: 1 -+ transformOrigin: Item.Center -+ sourceComponent: rightPaneItem.targetComponent -+ } -+ -+ Behavior on paneId { -+ PaneTransition { -+ target: rightLoader -+ propertyActions: [ -+ PropertyAction { -+ target: rightPaneItem -+ property: "targetComponent" -+ value: rightPaneItem.nextComponent - } - ] - } - } -+ } -+ } -+ } - -- RowLayout { -- id: mainRowLayout -- Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ Component { -+ id: statusIconsComponent -+ -+ SectionPage { -+ title: qsTr("Status Icons") -+ subtitle: qsTr("Choose which status controls appear in the taskbar.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ ConnectedButtonGroup { -+ rootItem: root -+ options: [ -+ { -+ label: qsTr("Speakers"), -+ propertyName: "showAudio", -+ onToggled: function (checked) { -+ root.showAudio = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Microphone"), -+ propertyName: "showMicrophone", -+ onToggled: function (checked) { -+ root.showMicrophone = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Keyboard"), -+ propertyName: "showKbLayout", -+ onToggled: function (checked) { -+ root.showKbLayout = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Network"), -+ propertyName: "showNetwork", -+ onToggled: function (checked) { -+ root.showNetwork = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Wifi"), -+ propertyName: "showWifi", -+ onToggled: function (checked) { -+ root.showWifi = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Bluetooth"), -+ propertyName: "showBluetooth", -+ onToggled: function (checked) { -+ root.showBluetooth = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Battery"), -+ propertyName: "showBattery", -+ onToggled: function (checked) { -+ root.showBattery = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Capslock"), -+ propertyName: "showLockStatus", -+ onToggled: function (checked) { -+ root.showLockStatus = checked; -+ root.saveConfig(); -+ } -+ } -+ ] -+ } -+ } -+ } -+ } - -- ColumnLayout { -- id: leftColumnLayout -- Layout.fillWidth: true -- Layout.alignment: Qt.AlignTop -- spacing: Appearance.spacing.normal -+ Component { -+ id: workspacesComponent -+ -+ SectionPage { -+ title: qsTr("Workspaces") -+ subtitle: qsTr("Tune workspace buttons and window indicators.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SpinSettingRow { -+ label: qsTr("Shown") -+ min: 1 -+ max: 20 -+ value: root.workspacesShown -+ onModified: value => { -+ root.workspacesShown = value; -+ root.saveConfig(); -+ } -+ } - -- SectionContainer { -- Layout.fillWidth: true -- alignTop: true -+ SwitchRow { -+ label: qsTr("Active indicator") -+ checked: root.workspacesActiveIndicator -+ onToggled: checked => { -+ root.workspacesActiveIndicator = checked; -+ root.saveConfig(); -+ } -+ } - -- StyledText { -- text: qsTr("Workspaces") -- font.pointSize: Appearance.font.size.normal -- } -+ SwitchRow { -+ label: qsTr("Occupied background") -+ checked: root.workspacesOccupiedBg -+ onToggled: checked => { -+ root.workspacesOccupiedBg = checked; -+ root.saveConfig(); -+ } -+ } - -- StyledRect { -- Layout.fillWidth: true -- implicitHeight: workspacesShownRow.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- -- Behavior on implicitHeight { -- Anim {} -- } -- -- RowLayout { -- id: workspacesShownRow -- anchors.left: parent.left -- anchors.right: parent.right -- anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -- -- StyledText { -- Layout.fillWidth: true -- text: qsTr("Shown") -- } -+ SwitchRow { -+ label: qsTr("Show windows") -+ checked: root.workspacesShowWindows -+ onToggled: checked => { -+ root.workspacesShowWindows = checked; -+ root.saveConfig(); -+ } -+ } - -- CustomSpinBox { -- min: 1 -- max: 20 -- value: root.workspacesShown -- onValueModified: value => { -- root.workspacesShown = value; -- root.saveConfig(); -- } -- } -- } -- } -+ SpinSettingRow { -+ label: qsTr("Max window icons") -+ min: 0 -+ max: 20 -+ value: root.workspacesMaxWindowIcons -+ onModified: value => { -+ root.workspacesMaxWindowIcons = value; -+ root.saveConfig(); -+ } -+ } - -- StyledRect { -- Layout.fillWidth: true -- implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- -- Behavior on implicitHeight { -- Anim {} -- } -- -- RowLayout { -- id: workspacesActiveIndicatorRow -- anchors.left: parent.left -- anchors.right: parent.right -- anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -- -- StyledText { -- Layout.fillWidth: true -- text: qsTr("Active indicator") -- } -+ SwitchRow { -+ label: qsTr("Per monitor workspaces") -+ checked: root.workspacesPerMonitor -+ onToggled: checked => { -+ root.workspacesPerMonitor = checked; -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } - -- StyledSwitch { -- checked: root.workspacesActiveIndicator -- onToggled: { -- root.workspacesActiveIndicator = checked; -- root.saveConfig(); -- } -- } -- } -+ Component { -+ id: scrollActionsComponent -+ -+ SectionPage { -+ title: qsTr("Scroll Actions") -+ subtitle: qsTr("Choose what responds to wheel input on the bar.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ ConnectedButtonGroup { -+ rootItem: root -+ options: [ -+ { -+ label: qsTr("Workspaces"), -+ propertyName: "scrollWorkspaces", -+ onToggled: function (checked) { -+ root.scrollWorkspaces = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Volume"), -+ propertyName: "scrollVolume", -+ onToggled: function (checked) { -+ root.scrollVolume = checked; -+ root.saveConfig(); - } -+ }, -+ { -+ label: qsTr("Brightness"), -+ propertyName: "scrollBrightness", -+ onToggled: function (checked) { -+ root.scrollBrightness = checked; -+ root.saveConfig(); -+ } -+ } -+ ] -+ } -+ } -+ } -+ } - -- StyledRect { -- Layout.fillWidth: true -- implicitHeight: workspacesOccupiedBgRow.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- -- Behavior on implicitHeight { -- Anim {} -- } -- -- RowLayout { -- id: workspacesOccupiedBgRow -- anchors.left: parent.left -- anchors.right: parent.right -- anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -- -- StyledText { -- Layout.fillWidth: true -- text: qsTr("Occupied background") -- } -+ Component { -+ id: clockComponent -+ -+ SectionPage { -+ title: qsTr("Clock") -+ subtitle: qsTr("Adjust the clock entry shown on the bar.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SwitchRow { -+ label: qsTr("Background") -+ checked: root.clockBackground -+ onToggled: checked => { -+ root.clockBackground = checked; -+ root.saveConfig(); -+ } -+ } - -- StyledSwitch { -- checked: root.workspacesOccupiedBg -- onToggled: { -- root.workspacesOccupiedBg = checked; -- root.saveConfig(); -- } -- } -- } -- } -+ SwitchRow { -+ label: qsTr("Show date") -+ checked: root.clockShowDate -+ onToggled: checked => { -+ root.clockShowDate = checked; -+ root.saveConfig(); -+ } -+ } - -- StyledRect { -- Layout.fillWidth: true -- implicitHeight: workspacesShowWindowsRow.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- -- Behavior on implicitHeight { -- Anim {} -- } -- -- RowLayout { -- id: workspacesShowWindowsRow -- anchors.left: parent.left -- anchors.right: parent.right -- anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -- -- StyledText { -- Layout.fillWidth: true -- text: qsTr("Show windows") -- } -+ SwitchRow { -+ label: qsTr("Show clock icon") -+ checked: root.clockShowIcon -+ onToggled: checked => { -+ root.clockShowIcon = checked; -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } - -- StyledSwitch { -- checked: root.workspacesShowWindows -- onToggled: { -- root.workspacesShowWindows = checked; -- root.saveConfig(); -- } -- } -- } -- } -+ Component { -+ id: behaviorComponent -+ -+ SectionPage { -+ title: qsTr("Bar Behavior") -+ subtitle: qsTr("Control when the bar appears and how drag reveal feels.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SwitchRow { -+ label: qsTr("Persistent") -+ checked: root.persistent -+ onToggled: checked => { -+ root.persistent = checked; -+ root.saveConfig(); -+ } -+ } - -- StyledRect { -- Layout.fillWidth: true -- implicitHeight: workspacesPerMonitorRow.implicitHeight + Appearance.padding.large * 2 -- radius: Appearance.rounding.normal -- color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- -- Behavior on implicitHeight { -- Anim {} -- } -- -- RowLayout { -- id: workspacesPerMonitorRow -- anchors.left: parent.left -- anchors.right: parent.right -- anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -- -- StyledText { -- Layout.fillWidth: true -- text: qsTr("Per monitor workspaces") -- } -+ SwitchRow { -+ label: qsTr("Show on hover") -+ checked: root.showOnHover -+ onToggled: checked => { -+ root.showOnHover = checked; -+ root.saveConfig(); -+ } -+ } -+ } - -- StyledSwitch { -- checked: root.workspacesPerMonitor -- onToggled: { -- root.workspacesPerMonitor = checked; -- root.saveConfig(); -- } -- } -- } -- } -- } -+ SectionContainer { -+ Layout.fillWidth: true -+ contentSpacing: Tokens.spacing.normal - -- SectionContainer { -- Layout.fillWidth: true -- alignTop: true -+ SliderInput { -+ Layout.fillWidth: true -+ label: qsTr("Drag threshold") -+ value: root.dragThreshold -+ from: 0 -+ to: 100 -+ suffix: "px" -+ validator: IntValidator { -+ bottom: 0 -+ top: 100 -+ } -+ formatValueFunction: val => Math.round(val).toString() -+ parseValueFunction: text => parseInt(text) -+ onValueModified: newValue => { -+ root.dragThreshold = Math.round(newValue); -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } - -- StyledText { -- text: qsTr("Scroll Actions") -- font.pointSize: Appearance.font.size.normal -- } -+ Component { -+ id: activeWindowComponent -+ -+ SectionPage { -+ title: qsTr("Active Window") -+ subtitle: qsTr("Configure the active window entry in the taskbar.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SwitchRow { -+ label: qsTr("Compact") -+ checked: root.activeWindowCompact -+ onToggled: checked => { -+ root.activeWindowCompact = checked; -+ root.saveConfig(); -+ } -+ } - -- ConnectedButtonGroup { -- rootItem: root -- -- options: [ -- { -- label: qsTr("Workspaces"), -- propertyName: "scrollWorkspaces", -- onToggled: function (checked) { -- root.scrollWorkspaces = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Volume"), -- propertyName: "scrollVolume", -- onToggled: function (checked) { -- root.scrollVolume = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Brightness"), -- propertyName: "scrollBrightness", -- onToggled: function (checked) { -- root.scrollBrightness = checked; -- root.saveConfig(); -- } -- } -- ] -- } -- } -+ SwitchRow { -+ label: qsTr("Inverted") -+ checked: root.activeWindowInverted -+ onToggled: checked => { -+ root.activeWindowInverted = checked; -+ root.saveConfig(); - } -+ } -+ } -+ } -+ } - -- ColumnLayout { -- id: middleColumnLayout -- Layout.fillWidth: true -- Layout.alignment: Qt.AlignTop -- spacing: Appearance.spacing.normal -+ Component { -+ id: popoutsComponent -+ -+ SectionPage { -+ title: qsTr("Popouts") -+ subtitle: qsTr("Select which taskbar entries open hover popouts.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ SwitchRow { -+ label: qsTr("Active window") -+ checked: root.popoutActiveWindow -+ onToggled: checked => { -+ root.popoutActiveWindow = checked; -+ root.saveConfig(); -+ } -+ } - -- SectionContainer { -- Layout.fillWidth: true -- alignTop: true -+ SwitchRow { -+ label: qsTr("Tray") -+ checked: root.popoutTray -+ onToggled: checked => { -+ root.popoutTray = checked; -+ root.saveConfig(); -+ } -+ } - -- StyledText { -- text: qsTr("Clock") -- font.pointSize: Appearance.font.size.normal -- } -+ SwitchRow { -+ label: qsTr("Status icons") -+ checked: root.popoutStatusIcons -+ onToggled: checked => { -+ root.popoutStatusIcons = checked; -+ root.saveConfig(); -+ } -+ } -+ } -+ } -+ } - -- SwitchRow { -- label: qsTr("Show clock icon") -- checked: root.clockShowIcon -- onToggled: checked => { -- root.clockShowIcon = checked; -- root.saveConfig(); -- } -+ Component { -+ id: trayComponent -+ -+ SectionPage { -+ title: qsTr("Tray Settings") -+ subtitle: qsTr("Change the system tray presentation.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ ConnectedButtonGroup { -+ rootItem: root -+ options: [ -+ { -+ label: qsTr("Background"), -+ propertyName: "trayBackground", -+ onToggled: function (checked) { -+ root.trayBackground = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Compact"), -+ propertyName: "trayCompact", -+ onToggled: function (checked) { -+ root.trayCompact = checked; -+ root.saveConfig(); -+ } -+ }, -+ { -+ label: qsTr("Recolour"), -+ propertyName: "trayRecolour", -+ onToggled: function (checked) { -+ root.trayRecolour = checked; -+ root.saveConfig(); - } - } -+ ] -+ } -+ } -+ } -+ } - -- SectionContainer { -- Layout.fillWidth: true -- alignTop: true -- -- StyledText { -- text: qsTr("Bar Behavior") -- font.pointSize: Appearance.font.size.normal -- } -+ Component { -+ id: monitorsComponent -+ -+ SectionPage { -+ title: qsTr("Monitors") -+ subtitle: qsTr("Choose which monitors show the taskbar.") -+ -+ SectionContainer { -+ Layout.fillWidth: true -+ alignTop: true -+ -+ ConnectedButtonGroup { -+ rootItem: root -+ rows: Math.max(1, Math.ceil(root.monitorNames.length / 3)) -+ options: root.monitorNames.map(e => ({ -+ label: qsTr(e), -+ propertyName: `monitor${e}`, -+ onToggled: function (_) { -+ const screens = []; -+ for (const screen of root.excludedScreens) -+ screens.push(screen); -+ -+ const addedBack = screens.includes(e); -+ if (addedBack) { -+ const index = screens.indexOf(e); -+ if (index !== -1) -+ screens.splice(index, 1); -+ } else if (!screens.includes(e)) { -+ screens.push(e); -+ } - -- SwitchRow { -- label: qsTr("Persistent") -- checked: root.persistent -- onToggled: checked => { -- root.persistent = checked; -+ root.excludedScreens = screens; - root.saveConfig(); -- } -- } -+ }, -+ state: !Strings.testRegexList(root.excludedScreens, e) -+ })) -+ } -+ } -+ } -+ } - -- SwitchRow { -- label: qsTr("Show on hover") -- checked: root.showOnHover -- onToggled: checked => { -- root.showOnHover = checked; -- root.saveConfig(); -- } -- } -+ component SectionPage: StyledFlickable { -+ id: sectionPage - -- SectionContainer { -- contentSpacing: Appearance.spacing.normal -+ required property string title -+ property string subtitle: "" -+ default property alias contentItems: contentLayout.data - -- SliderInput { -- Layout.fillWidth: true -+ flickableDirection: Flickable.VerticalFlick -+ contentHeight: contentLayout.height - -- label: qsTr("Drag threshold") -- value: root.dragThreshold -- from: 0 -- to: 100 -- suffix: "px" -- validator: IntValidator { -- bottom: 0 -- top: 100 -- } -- formatValueFunction: val => Math.round(val).toString() -- parseValueFunction: text => parseInt(text) -+ StyledScrollBar.vertical: StyledScrollBar { -+ flickable: sectionPage -+ } - -- onValueModified: newValue => { -- root.dragThreshold = Math.round(newValue); -- root.saveConfig(); -- } -- } -- } -- } -- } -+ ColumnLayout { -+ id: contentLayout - -- ColumnLayout { -- id: rightColumnLayout -- Layout.fillWidth: true -- Layout.alignment: Qt.AlignTop -- spacing: Appearance.spacing.normal -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.top: parent.top -+ spacing: Tokens.spacing.normal - -- SectionContainer { -- Layout.fillWidth: true -- alignTop: true -+ StyledText { -+ Layout.fillWidth: true -+ text: sectionPage.title -+ font.pointSize: Tokens.font.size.extraLarge -+ font.weight: 600 -+ } - -- StyledText { -- text: qsTr("Popouts") -- font.pointSize: Appearance.font.size.normal -- } -+ StyledText { -+ Layout.fillWidth: true -+ Layout.bottomMargin: Tokens.spacing.small -+ text: sectionPage.subtitle -+ color: Colours.palette.m3outline -+ visible: text.length > 0 -+ wrapMode: Text.WordWrap -+ } -+ } -+ } - -- SwitchRow { -- label: qsTr("Active window") -- checked: root.popoutActiveWindow -- onToggled: checked => { -- root.popoutActiveWindow = checked; -- root.saveConfig(); -- } -- } -+ component SectionNavButton: StyledRect { -+ id: navButton - -- SwitchRow { -- label: qsTr("Tray") -- checked: root.popoutTray -- onToggled: checked => { -- root.popoutTray = checked; -- root.saveConfig(); -- } -- } -+ required property var section -+ property bool active: false - -- SwitchRow { -- label: qsTr("Status icons") -- checked: root.popoutStatusIcons -- onToggled: checked => { -- root.popoutStatusIcons = checked; -- root.saveConfig(); -- } -- } -- } -+ signal clicked - -- SectionContainer { -- Layout.fillWidth: true -- alignTop: true -+ implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 -+ color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" -+ radius: Tokens.rounding.normal - -- StyledText { -- text: qsTr("Tray Settings") -- font.pointSize: Appearance.font.size.normal -- } -+ Behavior on color { -+ CAnim {} -+ } - -- ConnectedButtonGroup { -- rootItem: root -- -- options: [ -- { -- label: qsTr("Background"), -- propertyName: "trayBackground", -- onToggled: function (checked) { -- root.trayBackground = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Compact"), -- propertyName: "trayCompact", -- onToggled: function (checked) { -- root.trayCompact = checked; -- root.saveConfig(); -- } -- }, -- { -- label: qsTr("Recolour"), -- propertyName: "trayRecolour", -- onToggled: function (checked) { -- root.trayRecolour = checked; -- root.saveConfig(); -- } -- } -- ] -- } -- } -- } -+ StateLayer { -+ onClicked: navButton.clicked() -+ } -+ -+ RowLayout { -+ id: navRow -+ -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.normal -+ -+ MaterialIcon { -+ Layout.alignment: Qt.AlignVCenter -+ text: navButton.section.icon -+ fill: navButton.active ? 1 : 0 -+ color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant -+ font.pointSize: Tokens.font.size.large -+ } -+ -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: 0 -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: navButton.section.title -+ font.weight: navButton.active ? 500 : 400 -+ elide: Text.ElideRight -+ maximumLineCount: 1 - } -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: navButton.section.description -+ color: Colours.palette.m3outline -+ font.pointSize: Tokens.font.size.small -+ elide: Text.ElideRight -+ maximumLineCount: 1 -+ } -+ } -+ -+ MaterialIcon { -+ Layout.alignment: Qt.AlignVCenter -+ text: "chevron_right" -+ opacity: navButton.active ? 1 : 0 -+ color: Colours.palette.m3primary -+ } -+ } -+ } -+ -+ component SpinSettingRow: StyledRect { -+ id: spinRow -+ -+ required property string label -+ property int min: 0 -+ property int max: 100 -+ property int value: 0 -+ -+ signal modified(int value) -+ -+ Layout.fillWidth: true -+ implicitHeight: rowLayout.implicitHeight + Tokens.padding.large * 2 -+ radius: Tokens.rounding.normal -+ color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -+ -+ RowLayout { -+ id: rowLayout -+ -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: spinRow.label -+ } -+ -+ CustomSpinBox { -+ min: spinRow.min -+ max: spinRow.max -+ value: spinRow.value -+ onValueModified: value => spinRow.modified(value) - } - } - } -diff --git a/modules/dashboard/Background.qml b/modules/dashboard/Background.qml -deleted file mode 100644 -index e2a91f74..00000000 ---- a/modules/dashboard/Background.qml -+++ /dev/null -@@ -1,66 +0,0 @@ --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Shapes -- --ShapePath { -- id: root -- -- required property Wrapper wrapper -- readonly property real rounding: Config.border.rounding -- readonly property bool flatten: wrapper.height < rounding * 2 -- readonly property real roundingY: flatten ? wrapper.height / 2 : rounding -- -- strokeWidth: -1 -- fillColor: Colours.palette.m3surface -- -- PathArc { -- relativeX: root.rounding -- relativeY: root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- } -- -- PathLine { -- relativeX: 0 -- relativeY: root.wrapper.height - root.roundingY * 2 -- } -- -- PathArc { -- relativeX: root.rounding -- relativeY: root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- direction: PathArc.Counterclockwise -- } -- -- PathLine { -- relativeX: root.wrapper.width - root.rounding * 2 -- relativeY: 0 -- } -- -- PathArc { -- relativeX: root.rounding -- relativeY: -root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- direction: PathArc.Counterclockwise -- } -- -- PathLine { -- relativeX: 0 -- relativeY: -(root.wrapper.height - root.roundingY * 2) -- } -- -- PathArc { -- relativeX: root.rounding -- relativeY: -root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- } -- -- Behavior on fillColor { -- CAnim {} -- } --} -diff --git a/modules/dashboard/Content.qml b/modules/dashboard/Content.qml -index f95b7d78..95d2a08a 100644 ---- a/modules/dashboard/Content.qml -+++ b/modules/dashboard/Content.qml -@@ -1,19 +1,59 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.components.filedialog --import qs.config --import Quickshell --import Quickshell.Widgets - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components -+import qs.components.filedialog - - Item { - id: root - -- required property PersistentProperties visibilities -- required property PersistentProperties state -+ required property DrawerVisibilities visibilities -+ readonly property bool needsKeyboard: { -+ const count = repeater.count; -+ for (let i = 0; i < count; i++) { -+ const item = repeater.itemAt(i) as Loader; -+ if (item?.sourceComponent === mediaComponent && (item?.item as MediaWrapper)?.needsKeyboard) -+ return true; -+ } -+ return false; -+ } -+ required property DashboardState dashState - required property FileDialog facePicker -+ -+ readonly property var dashboardTabs: { -+ const allTabs = [ -+ { -+ component: dashComponent, -+ iconName: "dashboard", -+ text: qsTr("Dashboard"), -+ enabled: Config.dashboard.showDashboard -+ }, -+ { -+ component: mediaComponent, -+ iconName: "queue_music", -+ text: qsTr("Media"), -+ enabled: Config.dashboard.showMedia -+ }, -+ { -+ component: performanceComponent, -+ iconName: "speed", -+ text: qsTr("Performance"), -+ enabled: Config.dashboard.showPerformance && (Config.dashboard.performance.showCpu || Config.dashboard.performance.showGpu || Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork || Config.dashboard.performance.showBattery) -+ }, -+ { -+ component: weatherComponent, -+ iconName: "cloud", -+ text: qsTr("Weather"), -+ enabled: Config.dashboard.showWeather -+ } -+ ]; -+ return allTabs.filter(tab => tab.enabled); -+ } -+ - readonly property real nonAnimWidth: view.implicitWidth + viewWrapper.anchors.margins * 2 - readonly property real nonAnimHeight: tabs.implicitHeight + tabs.anchors.topMargin + view.implicitHeight + viewWrapper.anchors.margins * 2 - -@@ -26,11 +66,12 @@ Item { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right -- anchors.topMargin: Appearance.padding.normal -- anchors.margins: Appearance.padding.large -+ anchors.topMargin: Tokens.padding.normal -+ anchors.margins: Tokens.padding.large - - nonAnimWidth: root.nonAnimWidth - anchors.margins * 2 -- state: root.state -+ dashState: root.dashState -+ tabs: root.dashboardTabs - } - - ClippingRectangle { -@@ -40,84 +81,116 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.bottom: parent.bottom -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: "transparent" - - Flickable { - id: view - -- readonly property int currentIndex: root.state.currentTab -- readonly property Item currentItem: row.children[currentIndex] -+ readonly property int currentIndex: root.dashState.currentTab -+ readonly property Item currentItem: { -+ repeater.count; // Trigger update on count change -+ return repeater.itemAt(currentIndex); -+ } - - anchors.fill: parent - - flickableDirection: Flickable.HorizontalFlick - -- implicitWidth: currentItem.implicitWidth -- implicitHeight: currentItem.implicitHeight -+ implicitWidth: currentItem?.implicitWidth ?? 0 -+ implicitHeight: currentItem?.implicitHeight ?? 0 - -- contentX: currentItem.x -+ contentX: currentItem?.x ?? 0 - contentWidth: row.implicitWidth - contentHeight: row.implicitHeight - - onContentXChanged: { -- if (!moving) -+ if (!moving || !currentItem) - return; - - const x = contentX - currentItem.x; - if (x > currentItem.implicitWidth / 2) -- root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); -+ root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); - else if (x < -currentItem.implicitWidth / 2) -- root.state.currentTab = Math.max(root.state.currentTab - 1, 0); -+ root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); - } - - onDragEnded: { -+ if (!currentItem) -+ return; -+ - const x = contentX - currentItem.x; - if (x > currentItem.implicitWidth / 10) -- root.state.currentTab = Math.min(root.state.currentTab + 1, tabs.count - 1); -+ root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, tabs.count - 1); - else if (x < -currentItem.implicitWidth / 10) -- root.state.currentTab = Math.max(root.state.currentTab - 1, 0); -+ root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); - else -- contentX = Qt.binding(() => currentItem.x); -+ contentX = Qt.binding(() => currentItem?.x ?? 0); - } - - RowLayout { - id: row - -- Pane { -- index: 0 -- sourceComponent: Dash { -- visibilities: root.visibilities -- state: root.state -- facePicker: root.facePicker -+ Repeater { -+ id: repeater -+ -+ model: ScriptModel { -+ values: root.dashboardTabs - } -- } - -- Pane { -- index: 1 -- sourceComponent: Media { -- visibilities: root.visibilities -+ delegate: Loader { -+ id: paneLoader -+ -+ required property int index -+ required property var modelData -+ -+ Layout.alignment: Qt.AlignTop -+ -+ sourceComponent: modelData.component -+ -+ Component.onCompleted: active = Qt.binding(() => { -+ if (index === view.currentIndex) -+ return true; -+ const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); -+ const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); -+ return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); -+ }) - } - } -+ } - -- Pane { -- index: 2 -- sourceComponent: Performance {} -- } -+ Component { -+ id: dashComponent - -- Pane { -- index: 3 -- sourceComponent: Weather {} -+ Dash { -+ visibilities: root.visibilities -+ dashState: root.dashState -+ facePicker: root.facePicker - } -+ } -+ -+ Component { -+ id: mediaComponent - -- Pane { -- index: 4 -- sourceComponent: Monitors {} -+ MediaWrapper { -+ visibilities: root.visibilities - } - } - -+ Component { -+ id: performanceComponent -+ -+ Performance {} -+ } -+ -+ Component { -+ id: weatherComponent -+ -+ WeatherTab {} -+ } -+ - Behavior on contentX { - Anim {} - } -@@ -126,32 +199,13 @@ Item { - - Behavior on implicitWidth { - Anim { -- duration: Appearance.anim.durations.large -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.EmphasizedLarge - } - } - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.large -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.EmphasizedLarge - } - } -- -- component Pane: Loader { -- id: pane -- -- required property int index -- -- Layout.alignment: Qt.AlignTop -- -- Component.onCompleted: active = Qt.binding(() => { -- // Always keep current tab loaded -- if (pane.index === view.currentIndex) -- return true; -- const vx = Math.floor(view.visibleArea.xPosition * view.contentWidth); -- const vex = Math.floor(vx + view.visibleArea.widthRatio * view.contentWidth); -- return (vx >= x && vx <= x + implicitWidth) || (vex >= x && vex <= x + implicitWidth); -- }) -- } - } -diff --git a/modules/dashboard/Dash.qml b/modules/dashboard/Dash.qml -index 71e224fb..6785ede8 100644 ---- a/modules/dashboard/Dash.qml -+++ b/modules/dashboard/Dash.qml -@@ -1,20 +1,19 @@ -+import "dash" -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.filedialog - import qs.services --import qs.config --import "dash" --import Quickshell --import QtQuick.Layouts - - GridLayout { - id: root - -- required property PersistentProperties visibilities -- required property PersistentProperties state -+ required property DrawerVisibilities visibilities -+ required property DashboardState dashState - required property FileDialog facePicker - -- rowSpacing: Appearance.spacing.normal -- columnSpacing: Appearance.spacing.normal -+ rowSpacing: Tokens.spacing.normal -+ columnSpacing: Tokens.spacing.normal - - Rect { - Layout.column: 2 -@@ -22,13 +21,12 @@ GridLayout { - Layout.preferredWidth: user.implicitWidth - Layout.preferredHeight: user.implicitHeight - -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - - User { - id: user - - visibilities: root.visibilities -- state: root.state - facePicker: root.facePicker - } - } -@@ -36,12 +34,12 @@ GridLayout { - Rect { - Layout.row: 0 - Layout.columnSpan: 2 -- Layout.preferredWidth: Config.dashboard.sizes.weatherWidth -+ Layout.preferredWidth: Tokens.sizes.dashboard.weatherWidth - Layout.fillHeight: true - -- radius: Appearance.rounding.large * 1.5 -+ radius: Tokens.rounding.large * 1.5 - -- Weather {} -+ SmallWeather {} - } - - Rect { -@@ -49,7 +47,7 @@ GridLayout { - Layout.preferredWidth: dateTime.implicitWidth - Layout.fillHeight: true - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - DateTime { - id: dateTime -@@ -63,12 +61,12 @@ GridLayout { - Layout.fillWidth: true - Layout.preferredHeight: calendar.implicitHeight - -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - - Calendar { - id: calendar - -- state: root.state -+ dashState: root.dashState - } - } - -@@ -78,7 +76,7 @@ GridLayout { - Layout.preferredWidth: resources.implicitWidth - Layout.fillHeight: true - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - Resources { - id: resources -@@ -92,7 +90,7 @@ GridLayout { - Layout.preferredWidth: media.implicitWidth - Layout.fillHeight: true - -- radius: Appearance.rounding.large * 2 -+ radius: Tokens.rounding.large * 2 - - Media { - id: media -diff --git a/modules/dashboard/LyricMenu.qml b/modules/dashboard/LyricMenu.qml -new file mode 100644 -index 00000000..1e5461f5 ---- /dev/null -+++ b/modules/dashboard/LyricMenu.qml -@@ -0,0 +1,412 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.components.controls -+import qs.services -+ -+StyledRect { -+ id: root -+ -+ required property real contentHeight -+ -+ function searchCandidates(title, artist) { -+ LyricsService.currentRequestId++; -+ LyricsService.fetchNetEaseCandidates(title, artist, LyricsService.currentRequestId); -+ } -+ -+ implicitHeight: contentHeight -+ -+ radius: Tokens.rounding.large -+ color: Colours.tPalette.m3surfaceContainer -+ -+ Loader { -+ asynchronous: true -+ anchors.fill: parent -+ active: root.height > 0 -+ -+ sourceComponent: ColumnLayout { -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal -+ -+ // Header: icon, backend selector, refresh, toggle -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.padding.small -+ -+ MaterialIcon { -+ text: "lyrics" -+ fill: 1 -+ color: Colours.palette.m3primary -+ font.pointSize: Tokens.spacing.large -+ } -+ -+ Rectangle { -+ Layout.preferredHeight: 24 -+ Layout.preferredWidth: 80 -+ radius: Tokens.rounding.small -+ color: Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.15) -+ -+ StyledText { -+ anchors.centerIn: parent -+ text: LyricsService.preferredBackend -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3primary -+ } -+ -+ MouseArea { -+ anchors.fill: parent -+ cursorShape: Qt.PointingHandCursor -+ onClicked: { -+ const backends = ["Auto", "Local", "NetEase"]; -+ const currentIndex = backends.indexOf(LyricsService.preferredBackend); -+ const nextIndex = (currentIndex + 1) % backends.length; -+ LyricsService.preferredBackend = backends[nextIndex]; -+ LyricsService.loadLyrics(); -+ } -+ } -+ } -+ -+ Rectangle { -+ Layout.preferredHeight: 24 -+ Layout.preferredWidth: 60 -+ radius: Tokens.rounding.small -+ visible: LyricsService.preferredBackend === "Auto" -+ color: LyricsService.backend === "Local" ? Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.15) : Qt.rgba(Colours.palette.m3secondary.r, Colours.palette.m3secondary.g, Colours.palette.m3secondary.b, 0.15) -+ -+ StyledText { -+ anchors.centerIn: parent -+ text: LyricsService.backend -+ font.pointSize: Tokens.font.size.small -+ color: LyricsService.backend === "Local" ? Colours.palette.m3tertiary : Colours.palette.m3secondary -+ } -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ -+ IconButton { -+ icon: "refresh" -+ type: IconButton.Text -+ onClicked: LyricsService.loadLyrics() -+ } -+ -+ StyledSwitch { -+ checked: LyricsService.lyricsVisible -+ onToggled: LyricsService.toggleVisibility() -+ } -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: LyricsService.preferredBackend === "Local" ? "Loaded File:" : "Fetched Candidates:" -+ color: Colours.palette.m3outline -+ font.pointSize: Tokens.font.size.small -+ elide: Text.ElideRight -+ visible: LyricsService.preferredBackend === "Local" ? LyricsService.loadedLocalFile.length > 0 : LyricsService.candidatesModel.count > 0 -+ } -+ -+ // Local file info (shown in Local mode) -+ Rectangle { -+ Layout.fillWidth: true -+ Layout.preferredHeight: 48 -+ visible: LyricsService.preferredBackend === "Local" && LyricsService.loadedLocalFile.length > 0 -+ radius: Tokens.rounding.small -+ color: Qt.rgba(Colours.palette.m3tertiary.r, Colours.palette.m3tertiary.g, Colours.palette.m3tertiary.b, 0.1) -+ -+ ColumnLayout { -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.small -+ spacing: 0 -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: { -+ const path = LyricsService.loadedLocalFile; -+ const parts = path.split('/'); -+ return parts[parts.length - 1]; -+ } -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3tertiary -+ elide: Text.ElideMiddle -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: { -+ const path = LyricsService.loadedLocalFile; -+ const parts = path.split('/'); -+ if (parts.length > 2) { -+ return parts.slice(-3, -1).join('/'); -+ } -+ return ""; -+ } -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3outline -+ elide: Text.ElideMiddle -+ } -+ } -+ } -+ -+ // Candidates list -+ Loader { -+ Layout.fillWidth: true -+ Layout.fillHeight: true -+ -+ active: LyricsService.preferredBackend !== "Local" -+ -+ sourceComponent: ListView { -+ id: candidatesView -+ -+ model: LyricsService.candidatesModel -+ clip: true -+ spacing: Tokens.spacing.small -+ visible: LyricsService.candidatesModel.count > 0 -+ opacity: visible ? 1 : 0 -+ -+ delegate: Item { -+ id: delegateRoot -+ -+ required property real id -+ required property string title -+ required property string artist -+ -+ property bool hovered: false -+ property bool pressed: false -+ -+ width: ListView.view.width * 0.98 -+ height: 70 -+ -+ anchors.horizontalCenter: parent?.horizontalCenter -+ scale: hovered ? 1.02 : 1.0 -+ -+ Behavior on scale { -+ NumberAnimation { -+ duration: Tokens.anim.durations.small -+ easing.type: Easing.OutCubic -+ } -+ } -+ -+ Rectangle { -+ id: background -+ -+ anchors.fill: parent -+ radius: Tokens.rounding.small -+ -+ color: delegateRoot.pressed ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.25) : delegateRoot.hovered ? Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.06) : Qt.rgba(Colours.palette.m3primary.r, Colours.palette.m3primary.g, Colours.palette.m3primary.b, 0.03) -+ -+ border.width: delegateRoot.hovered ? 1 : 0 -+ border.color: Colours.palette.m3primary -+ -+ Behavior on color { -+ ColorAnimation { -+ duration: Tokens.anim.durations.small -+ } -+ } -+ Behavior on border.width { -+ NumberAnimation { -+ duration: Tokens.anim.durations.small -+ } -+ } -+ } -+ -+ MouseArea { -+ anchors.fill: parent -+ hoverEnabled: true -+ cursorShape: Qt.PointingHandCursor -+ -+ onEntered: delegateRoot.hovered = true -+ onExited: delegateRoot.hovered = false -+ onPressed: delegateRoot.pressed = true -+ onReleased: delegateRoot.pressed = false -+ onClicked: LyricsService.selectCandidate(delegateRoot.id) -+ } -+ -+ Row { -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.normal -+ spacing: Tokens.spacing.small -+ -+ // Active indicator bar -+ Rectangle { -+ width: 4 -+ height: parent.height * 0.6 -+ radius: 2 -+ anchors.verticalCenter: parent.verticalCenter -+ color: LyricsService.currentSongId === delegateRoot.id ? Colours.palette.m3primary : "transparent" -+ -+ Behavior on color { -+ ColorAnimation { -+ duration: Tokens.anim.durations.small -+ } -+ } -+ } -+ -+ Column { -+ anchors.verticalCenter: parent.verticalCenter -+ width: parent.width - 30 -+ spacing: 4 -+ -+ Text { -+ text: delegateRoot.title -+ font.pointSize: Tokens.font.size.normal -+ font.bold: true -+ color: delegateRoot.hovered ? Colours.palette.m3primary : Colours.palette.m3onSurface -+ width: parent.width -+ elide: Text.ElideRight -+ -+ Behavior on color { -+ ColorAnimation { -+ duration: Tokens.anim.durations.small -+ } -+ } -+ } -+ -+ Text { -+ text: delegateRoot.artist -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ elide: Text.ElideRight -+ } -+ } -+ } -+ } -+ } -+ } -+ -+ Item { -+ Layout.fillHeight: true -+ visible: LyricsService.candidatesModel.count == 0 && LyricsService.preferredBackend !== "Local" -+ } -+ -+ // Manual search -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.padding.small -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: "Manual Search" -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ elide: Text.ElideRight -+ } -+ -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.padding.small -+ -+ StyledInputField { -+ id: searchTitle -+ -+ Layout.fillWidth: true -+ horizontalAlignment: TextInput.AlignLeft -+ -+ Binding { -+ target: searchTitle -+ property: "text" -+ value: (Players.active?.trackTitle ?? qsTr("title")) || qsTr("title") -+ } -+ } -+ -+ StyledInputField { -+ id: searchArtist -+ -+ Layout.fillWidth: true -+ horizontalAlignment: TextInput.AlignLeft -+ -+ Binding { -+ target: searchArtist -+ property: "text" -+ value: (Players.active?.trackArtist ?? qsTr("artist")) || qsTr("artist") -+ } -+ } -+ -+ IconButton { -+ icon: "search" -+ onClicked: root.searchCandidates(searchTitle.text, searchArtist.text) -+ } -+ } -+ } -+ -+ // Offset controls -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.padding.small -+ -+ MaterialIcon { -+ text: "contrast_square" -+ font.pointSize: Tokens.font.size.large -+ color: Colours.palette.m3secondary -+ } -+ -+ StyledText { -+ text: "Offset" -+ color: Colours.palette.m3outline -+ font.pointSize: Tokens.font.size.normal -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ -+ IconButton { -+ icon: "remove" -+ type: IconButton.Text -+ onClicked: { -+ LyricsService.offset = parseFloat((LyricsService.offset - 0.1).toFixed(1)); -+ LyricsService.savePrefs(); -+ } -+ } -+ -+ TextInput { -+ id: offsetInput -+ -+ horizontalAlignment: TextInput.AlignHCenter -+ color: Colours.palette.m3secondary -+ font.pointSize: Tokens.font.size.normal -+ selectByMouse: true -+ text: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" -+ onEditingFinished: { -+ let cleaned = offsetInput.text.replace(/[+s]/g, "").trim(); -+ let val = parseFloat(cleaned); -+ if (!isNaN(val)) { -+ LyricsService.offset = parseFloat(val.toFixed(1)); -+ LyricsService.savePrefs(); -+ } else { -+ offsetInput.text = (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s"; -+ } -+ } -+ -+ Binding { -+ target: offsetInput -+ property: "text" -+ value: (LyricsService.offset >= 0 ? "+" : "") + LyricsService.offset.toFixed(1) + "s" -+ when: !offsetInput.activeFocus -+ } -+ -+ Connections { -+ function onCurrentRequestIdChanged() { -+ offsetInput.focus = false; -+ } -+ -+ target: LyricsService -+ } -+ } -+ -+ IconButton { -+ icon: "add" -+ type: IconButton.Text -+ onClicked: { -+ LyricsService.offset = parseFloat((LyricsService.offset + 0.1).toFixed(1)); -+ LyricsService.savePrefs(); -+ } -+ } -+ } -+ } -+ } -+} -diff --git a/modules/dashboard/LyricsView.qml b/modules/dashboard/LyricsView.qml -new file mode 100644 -index 00000000..c0188304 ---- /dev/null -+++ b/modules/dashboard/LyricsView.qml -@@ -0,0 +1,112 @@ -+import QtQuick -+import QtQuick.Effects -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.components.containers -+import qs.services -+ -+StyledListView { -+ id: root -+ -+ readonly property bool lyricsActuallyVisible: LyricsService.lyricsVisible && LyricsService.model.count != 0 -+ -+ clip: true -+ model: LyricsService.model -+ currentIndex: LyricsService.currentIndex -+ visible: lyricsActuallyVisible || hideTimer.running -+ preferredHighlightBegin: height / 2 - 30 -+ preferredHighlightEnd: height / 2 + 30 -+ highlightRangeMode: ListView.ApplyRange -+ highlightFollowsCurrentItem: true -+ highlightMoveDuration: LyricsService.isManualSeeking ? 0 : Tokens.anim.durations.normal -+ layer.enabled: true -+ layer.effect: ShaderEffect { -+ required property Item source -+ property real fadeMargin: 0.5 -+ -+ fragmentShader: Quickshell.shellPath("assets/shaders/fade.frag.qsb") -+ } -+ onLyricsActuallyVisibleChanged: { -+ if (!lyricsActuallyVisible) -+ hideTimer.restart(); -+ } -+ onModelChanged: { -+ if (model && model.count > 0) { -+ Qt.callLater(() => positionViewAtIndex(currentIndex, ListView.Center)); -+ } -+ } -+ delegate: Item { -+ id: delegateRoot -+ -+ required property string lyricLine -+ required property real time -+ required property int index -+ readonly property bool hasContent: lyricLine && lyricLine.trim().length > 0 -+ property bool isCurrent: ListView.isCurrentItem -+ -+ width: ListView.view.width -+ height: hasContent ? (lyricText.contentHeight + Tokens.spacing.large) : 0 -+ -+ MultiEffect { -+ id: effect -+ -+ anchors.fill: lyricText -+ source: lyricText -+ scale: lyricText.scale -+ enabled: delegateRoot.isCurrent -+ visible: delegateRoot.isCurrent -+ -+ blurEnabled: true -+ blur: 0.4 -+ -+ shadowEnabled: true -+ shadowColor: Colours.palette.m3primary -+ shadowOpacity: 0.5 -+ shadowBlur: 0.6 -+ shadowHorizontalOffset: 0 -+ shadowVerticalOffset: 0 -+ -+ autoPaddingEnabled: true -+ } -+ -+ MouseArea { -+ anchors.fill: parent -+ cursorShape: Qt.PointingHandCursor -+ onClicked: LyricsService.jumpTo(delegateRoot.index, delegateRoot.time) -+ } -+ -+ Text { -+ id: lyricText -+ -+ text: delegateRoot.lyricLine ? delegateRoot.lyricLine.replace(/\u00A0/g, " ") : "" -+ width: parent.width * 0.85 -+ anchors.centerIn: parent -+ horizontalAlignment: Text.AlignHCenter -+ wrapMode: Text.WordWrap -+ font.pointSize: Tokens.font.size.normal -+ color: delegateRoot.isCurrent ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant -+ font.bold: delegateRoot.isCurrent -+ scale: delegateRoot.isCurrent ? 1.15 : 1.0 -+ -+ Behavior on color { -+ CAnim { -+ duration: Tokens.anim.durations.small -+ } -+ } -+ Behavior on scale { -+ Anim { -+ type: Anim.StandardSmall -+ } -+ } -+ } -+ } -+ -+ Timer { -+ id: hideTimer -+ -+ interval: 300 // long enough to bridge the track switch gap -+ running: false -+ repeat: false -+ } -+} -diff --git a/modules/dashboard/Media.qml b/modules/dashboard/Media.qml -index 37d12263..367d9e98 100644 ---- a/modules/dashboard/Media.qml -+++ b/modules/dashboard/Media.qml -@@ -1,27 +1,33 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import QtQuick.Shapes -+import Quickshell -+import Quickshell.Services.Mpris -+import Caelestia.Config -+import Caelestia.Services - import qs.components --import qs.components.effects - import qs.components.controls - import qs.services - import qs.utils --import qs.config --import Caelestia.Services --import Quickshell --import Quickshell.Widgets --import Quickshell.Services.Mpris --import QtQuick --import QtQuick.Layouts --import QtQuick.Shapes - - Item { - id: root - -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities -+ readonly property bool needsKeyboard: lyricMenuOpen -+ -+ readonly property real nonAnimHeight: Math.max(cover.implicitHeight + Tokens.sizes.dashboard.mediaVisualiserSize * 2, lyricMenuOpen ? lyricMenu.implicitHeight : details.implicitHeight, bongocat.implicitHeight) + Tokens.padding.large * 2 -+ readonly property real detailsHeightWithoutLyrics: details.implicitHeight - lyricsViewInDetails.implicitHeight -+ -+ property bool lyricMenuOpen: false -+ property bool lyricsShowing: LyricsService.lyricsVisible && LyricsService.model.count != 0 -+ property bool lyricsShowingDebounced: false - - property real playerProgress: { - const active = Players.active; -- return active?.length ? active.position / active.length : 0; -+ return active?.length ? (active.position % active.length) / active.length : 0; - } - - function lengthStr(length: int): string { -@@ -37,21 +43,54 @@ Item { - return `${mins}:${secs}`; - } - -- implicitWidth: cover.implicitWidth + Config.dashboard.sizes.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Appearance.padding.large * 2 -- implicitHeight: Math.max(cover.implicitHeight + Config.dashboard.sizes.mediaVisualiserSize * 2, details.implicitHeight, bongocat.implicitHeight) + Appearance.padding.large * 2 -+ onLyricsShowingChanged: { -+ if (lyricsShowing) { -+ lyricsHideDelay.stop(); -+ lyricsShowingDebounced = true; -+ } else { -+ lyricsHideDelay.restart(); -+ } -+ } -+ -+ implicitWidth: cover.implicitWidth + Tokens.sizes.dashboard.mediaVisualiserSize * 2 + details.implicitWidth + details.anchors.leftMargin + bongocat.implicitWidth + bongocat.anchors.leftMargin * 2 + Tokens.padding.large * 2 -+ implicitHeight: nonAnimHeight -+ -+ Behavior on implicitHeight { -+ Anim {} -+ } - - Behavior on playerProgress { - Anim { -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - } - - Timer { - running: Players.active?.isPlaying ?? false -- interval: Config.dashboard.mediaUpdateInterval -+ interval: GlobalConfig.dashboard.mediaUpdateInterval - triggeredOnStart: true - repeat: true -- onTriggered: Players.active?.positionChanged() -+ onTriggered: { -+ if (!Players.active) -+ return; -+ LyricsService.updatePosition(); -+ Players.active?.positionChanged(); -+ } -+ } -+ -+ Timer { -+ id: lyricsHideDelay -+ -+ interval: 300 -+ repeat: false -+ } -+ -+ Connections { -+ function onTriggered() { -+ root.lyricsShowingDebounced = false; -+ } -+ -+ target: lyricsHideDelay - } - - ServiceRef { -@@ -67,12 +106,12 @@ Item { - - readonly property real centerX: width / 2 - readonly property real centerY: height / 2 -- readonly property real innerX: cover.implicitWidth / 2 + Appearance.spacing.small -- readonly property real innerY: cover.implicitHeight / 2 + Appearance.spacing.small -+ readonly property real innerX: cover.implicitWidth / 2 + Tokens.spacing.small -+ readonly property real innerY: cover.implicitHeight / 2 + Tokens.spacing.small - property color colour: Colours.palette.m3primary - - anchors.fill: cover -- anchors.margins: -Config.dashboard.sizes.mediaVisualiserSize -+ anchors.margins: -Tokens.sizes.dashboard.mediaVisualiserSize - - asynchronous: true - preferredRendererType: Shape.CurveRenderer -@@ -83,7 +122,7 @@ Item { - id: visualiserBars - - model: Array.from({ -- length: Config.services.visualiserBars -+ length: GlobalConfig.services.visualiserBars - }, (_, i) => i) - - ShapePath { -@@ -92,13 +131,13 @@ Item { - required property int modelData - readonly property real value: Math.max(1e-3, Math.min(1, Audio.cava.values[modelData])) - -- readonly property real angle: modelData * 2 * Math.PI / Config.services.visualiserBars -- readonly property real magnitude: value * Config.dashboard.sizes.mediaVisualiserSize -+ readonly property real angle: modelData * 2 * Math.PI / GlobalConfig.services.visualiserBars -+ readonly property real magnitude: value * root.Tokens.sizes.dashboard.mediaVisualiserSize - readonly property real cos: Math.cos(angle) - readonly property real sin: Math.sin(angle) - -- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap -- strokeWidth: 360 / Config.services.visualiserBars - Appearance.spacing.small / 4 -+ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap -+ strokeWidth: 360 / GlobalConfig.services.visualiserBars - root.Tokens.spacing.small / 4 - strokeColor: Colours.palette.m3primary - - startX: visualiser.centerX + (visualiser.innerX + strokeWidth / 2) * cos -@@ -120,10 +159,10 @@ Item { - - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left -- anchors.leftMargin: Appearance.padding.large + Config.dashboard.sizes.mediaVisualiserSize -+ anchors.leftMargin: Tokens.padding.large + Tokens.sizes.dashboard.mediaVisualiserSize - -- implicitWidth: Config.dashboard.sizes.mediaCoverArtSize -- implicitHeight: Config.dashboard.sizes.mediaCoverArtSize -+ implicitWidth: Tokens.sizes.dashboard.mediaCoverArtSize -+ implicitHeight: Tokens.sizes.dashboard.mediaCoverArtSize - - color: Colours.tPalette.m3surfaceContainerHigh - radius: Infinity -@@ -142,11 +181,20 @@ Item { - - anchors.fill: parent - -- source: Players.active?.trackArtUrl ?? "" -+ source: Players.getArtUrl(Players.active) - asynchronous: true - fillMode: Image.PreserveAspectCrop -- sourceSize.width: width -- sourceSize.height: height -+ sourceSize: { -+ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; -+ return Qt.size(width * dpr, height * dpr); -+ } -+ -+ MouseArea { -+ anchors.fill: parent -+ onClicked: { -+ LyricsService.toggleVisibility(); -+ } -+ } - } - } - -@@ -155,9 +203,9 @@ Item { - - anchors.verticalCenter: parent.verticalCenter - anchors.left: visualiser.right -- anchors.leftMargin: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.spacing.normal - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - id: title -@@ -169,7 +217,7 @@ Item { - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") - color: Players.active ? Colours.palette.m3primary : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - -@@ -184,7 +232,7 @@ Item { - visible: !!Players.active - text: Players.active?.trackAlbum || qsTr("Unknown album") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - -@@ -202,19 +250,34 @@ Item { - wrapMode: Players.active ? Text.NoWrap : Text.WordWrap - } - -+ LyricsView { -+ id: lyricsViewInDetails -+ -+ Layout.fillWidth: true -+ Layout.preferredHeight: 200 -+ } -+ - RowLayout { - id: controls - - Layout.alignment: Qt.AlignHCenter -- Layout.topMargin: Appearance.spacing.small -- Layout.bottomMargin: Appearance.spacing.smaller -+ Layout.topMargin: Tokens.spacing.small -+ Layout.bottomMargin: Tokens.spacing.smaller - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small -+ -+ PlayerControl { -+ type: IconButton.Text -+ icon: Players.active?.shuffle ? "shuffle_on" : "shuffle" -+ font.pointSize: Math.round(Tokens.font.size.large) -+ disabled: !Players.active?.shuffleSupported -+ onClicked: Players.active.shuffle = !Players.active?.shuffle -+ } - - PlayerControl { - type: IconButton.Text - icon: "skip_previous" -- font.pointSize: Math.round(Appearance.font.size.large * 1.5) -+ font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canGoPrevious - onClicked: Players.active?.previous() - } -@@ -223,9 +286,9 @@ Item { - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - label.animate: true - toggle: true -- padding: Appearance.padding.small / 2 -+ padding: Tokens.padding.small / 2 - checked: Players.active?.isPlaying ?? false -- font.pointSize: Math.round(Appearance.font.size.large * 1.5) -+ font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canTogglePlaying - onClicked: Players.active?.togglePlaying() - } -@@ -233,10 +296,17 @@ Item { - PlayerControl { - type: IconButton.Text - icon: "skip_next" -- font.pointSize: Math.round(Appearance.font.size.large * 1.5) -+ font.pointSize: Math.round(Tokens.font.size.large * 1.5) - disabled: !Players.active?.canGoNext - onClicked: Players.active?.next() - } -+ -+ PlayerControl { -+ type: IconButton.Text -+ icon: "lyrics" -+ font.pointSize: Math.round(Tokens.font.size.large) -+ onClicked: root.lyricMenuOpen = !root.lyricMenuOpen -+ } - } - - StyledSlider { -@@ -244,7 +314,7 @@ Item { - - enabled: !!Players.active - implicitWidth: 280 -- implicitHeight: Appearance.padding.normal * 3 -+ implicitHeight: Tokens.padding.normal * 3 - - onMoved: { - const active = Players.active; -@@ -260,9 +330,6 @@ Item { - } - - CustomMouseArea { -- anchors.fill: parent -- acceptedButtons: Qt.NoButton -- - function onWheel(event: WheelEvent) { - const active = Players.active; - if (!active?.canSeek || !active?.positionSupported) -@@ -274,6 +341,9 @@ Item { - active.position = Math.max(0, Math.min(active.length, active.position + delta)); - }); - } -+ -+ anchors.fill: parent -+ acceptedButtons: Qt.NoButton - } - } - -@@ -286,9 +356,9 @@ Item { - - anchors.left: parent.left - -- text: root.lengthStr(Players.active?.position ?? -1) -+ text: root.lengthStr(Players.active ? Players.active.position % Players.active.length : -1) - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledText { -@@ -298,105 +368,146 @@ Item { - - text: root.lengthStr(Players.active?.length ?? -1) - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } -+ } - -- RowLayout { -- Layout.alignment: Qt.AlignHCenter -- spacing: Appearance.spacing.small -+ ColumnLayout { -+ id: leftSection - -- PlayerControl { -- type: IconButton.Text -- icon: "move_up" -- inactiveOnColour: Colours.palette.m3secondary -- padding: Appearance.padding.small -- font.pointSize: Appearance.font.size.large -- disabled: !Players.active?.canRaise -- onClicked: { -- Players.active?.raise(); -- root.visibilities.dashboard = false; -- } -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.verticalCenterOffset: playerChanger.parent == leftSection ? -playerChanger.height : 0 -+ anchors.left: details.right -+ anchors.leftMargin: Tokens.spacing.normal -+ -+ visible: lyricMenu.height === 0 || opacity > 0 -+ opacity: lyricMenu.height === 0 ? 1 : 0 -+ -+ Behavior on opacity { -+ NumberAnimation { -+ duration: Tokens.anim.durations.normal -+ easing.type: Easing.OutCubic - } -+ } -+ -+ Item { -+ id: bongocat - -- SplitButton { -- id: playerSelector -+ implicitWidth: visualiser.width -+ implicitHeight: visualiser.height - -- disabled: !Players.list.length -- active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null -- menu.onItemSelected: item => Players.manualActive = item.modelData -+ AnimatedImage { -+ anchors.centerIn: parent - -- menuItems: playerList.instances -- fallbackIcon: "music_off" -- fallbackText: qsTr("No players") -+ width: visualiser.width * 0.75 -+ height: visualiser.height * 0.75 - -- label.Layout.maximumWidth: slider.implicitWidth * 0.28 -- label.elide: Text.ElideRight -+ playing: Players.active?.isPlaying ?? false -+ speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type -+ source: Paths.absolutePath(Config.paths.mediaGif) -+ asynchronous: true -+ fillMode: AnimatedImage.PreserveAspectFit -+ } -+ } -+ } - -- stateLayer.disabled: true -- menuOnTop: true -+ LyricMenu { -+ id: lyricMenu - -- Variants { -- id: playerList -+ anchors.top: parent.top -+ anchors.left: details.right -+ anchors.right: parent.right -+ anchors.leftMargin: Tokens.spacing.normal - -- model: Players.list -+ contentHeight: !root.lyricsShowingDebounced ? root.detailsHeightWithoutLyrics + Tokens.padding.large * 5 : root.detailsHeightWithoutLyrics + lyricsViewInDetails.implicitHeight - -- MenuItem { -- required property MprisPlayer modelData -+ visible: root.lyricMenuOpen || height > 0 -+ height: root.lyricMenuOpen ? implicitHeight : 0 -+ clip: true - -- icon: modelData === Players.active ? "check" : "" -- text: Players.getIdentity(modelData) -- activeIcon: "animated_images" -- } -- } -+ Behavior on height { -+ NumberAnimation { -+ duration: Tokens.anim.durations.normal -+ easing.type: Easing.OutCubic - } -+ } -+ } - -- PlayerControl { -- type: IconButton.Text -- icon: "delete" -- inactiveOnColour: Colours.palette.m3error -- padding: Appearance.padding.small -- font.pointSize: Appearance.font.size.large -- disabled: !Players.active?.canQuit -- onClicked: Players.active?.quit() -+ RowLayout { -+ id: playerChanger -+ -+ parent: !root.lyricsShowingDebounced ? details : leftSection -+ Layout.alignment: Qt.AlignHCenter -+ spacing: Tokens.spacing.small -+ -+ PlayerControl { -+ type: IconButton.Text -+ icon: "move_up" -+ inactiveOnColour: Colours.palette.m3secondary -+ padding: Tokens.padding.small -+ font.pointSize: Tokens.font.size.large -+ disabled: !Players.active?.canRaise -+ onClicked: { -+ Players.active?.raise(); -+ root.visibilities.dashboard = false; - } - } -- } - -- Item { -- id: bongocat -+ SplitButton { -+ id: playerSelector - -- anchors.verticalCenter: parent.verticalCenter -- anchors.left: details.right -- anchors.leftMargin: Appearance.spacing.normal -+ disabled: !Players.list.length -+ active: menuItems.find(m => m.modelData === Players.active) ?? menuItems[0] ?? null -+ menu.onItemSelected: item => Players.manualActive = (item as PlayerItem).modelData - -- implicitWidth: visualiser.width -- implicitHeight: visualiser.height -+ menuItems: playerList.instances -+ fallbackIcon: "music_off" -+ fallbackText: qsTr("No players") - -- AnimatedImage { -- anchors.centerIn: parent -+ label.Layout.maximumWidth: slider.implicitWidth * 0.28 -+ label.elide: Text.ElideRight - -- width: visualiser.width * 0.75 -- height: visualiser.height * 0.75 -+ stateLayer.disabled: true -+ menuOnTop: true - -- playing: Players.active?.isPlaying ?? false -- speed: Audio.beatTracker.bpm / 300 -- source: Paths.absolutePath(Config.paths.mediaGif) -- asynchronous: true -- fillMode: AnimatedImage.PreserveAspectFit -+ Variants { -+ id: playerList -+ -+ model: Players.list -+ -+ PlayerItem {} -+ } - } -+ -+ PlayerControl { -+ type: IconButton.Text -+ icon: "delete" -+ inactiveOnColour: Colours.palette.m3error -+ padding: Tokens.padding.small -+ font.pointSize: Tokens.font.size.large -+ disabled: !Players.active?.canQuit -+ onClicked: Players.active?.quit() -+ } -+ } -+ -+ component PlayerItem: MenuItem { -+ required property MprisPlayer modelData -+ -+ icon: modelData === Players.active ? "check" : "" -+ text: Players.getIdentity(modelData) -+ activeIcon: "animated_images" - } - - component PlayerControl: IconButton { -- Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) -- radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : implicitHeight / 2 -- radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial -- radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) -+ radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : implicitHeight / 2 -+ radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial -+ radiusAnim.easing: Tokens.anim.expressiveFastSpatial - - Behavior on Layout.preferredWidth { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -diff --git a/modules/dashboard/MediaWrapper.qml b/modules/dashboard/MediaWrapper.qml -new file mode 100644 -index 00000000..b03a11ee ---- /dev/null -+++ b/modules/dashboard/MediaWrapper.qml -@@ -0,0 +1,13 @@ -+import QtQuick -+ -+Item { -+ property alias visibilities: media.visibilities -+ readonly property alias needsKeyboard: media.needsKeyboard -+ -+ implicitWidth: media.implicitWidth -+ implicitHeight: media.nonAnimHeight -+ -+ Media { -+ id: media -+ } -+} -diff --git a/modules/dashboard/Monitors.qml b/modules/dashboard/Monitors.qml -index e1bfd4a0..8af11306 100644 ---- a/modules/dashboard/Monitors.qml -+++ b/modules/dashboard/Monitors.qml -@@ -1,7 +1,7 @@ - import qs.components - import qs.components.controls - import qs.services --import qs.config -+import Caelestia.Config - import Quickshell - import QtQuick - import QtQuick.Layouts -@@ -9,15 +9,15 @@ import QtQuick.Layouts - ColumnLayout { - id: root - -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - RowLayout { - Layout.fillWidth: true -- Layout.margins: Appearance.padding.normal -+ Layout.margins: Tokens.padding.normal - - StyledText { - text: qsTr("Monitors") -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - Layout.fillWidth: true - } - -@@ -40,7 +40,7 @@ ColumnLayout { - id: monitorsLayout - anchors.left: parent.left - anchors.right: parent.right -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Repeater { - model: Hyprctl.monitors -@@ -48,9 +48,9 @@ ColumnLayout { - delegate: StyledRect { - id: monitorDelegate - Layout.fillWidth: true -- implicitHeight: monitorContent.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: monitorContent.implicitHeight + Tokens.padding.large * 2 - color: Colours.tPalette.m3surfaceContainerHigh -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - - readonly property var mon: modelData - readonly property var brightnessMon: Brightness.getMonitor(mon.name) -@@ -58,8 +58,8 @@ ColumnLayout { - ColumnLayout { - id: monitorContent - anchors.fill: parent -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.medium -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.medium - - RowLayout { - Layout.fillWidth: true -@@ -72,13 +72,13 @@ ColumnLayout { - spacing: 0 - StyledText { - text: `${mon.name} - ${mon.make} ${mon.model}` -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - Layout.fillWidth: true - } - StyledText { - text: `${mon.width}x${mon.height}@${(mon.refreshRate ?? 0).toFixed(2)}Hz` - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - StyledText { -@@ -94,7 +94,7 @@ ColumnLayout { - - MaterialIcon { - text: "brightness_medium" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledSlider { -@@ -115,7 +115,7 @@ ColumnLayout { - - MaterialIcon { - text: "zoom_in" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledSlider { -@@ -135,7 +135,7 @@ ColumnLayout { - // Refresh Rate - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Refresh Rate") -@@ -155,7 +155,7 @@ ColumnLayout { - // Rotation - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Rotation") -@@ -182,7 +182,7 @@ ColumnLayout { - // Arrangement - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - text: qsTr("Position relative to:") -diff --git a/modules/dashboard/Performance.qml b/modules/dashboard/Performance.qml -index 5e00d892..c93499ca 100644 ---- a/modules/dashboard/Performance.qml -+++ b/modules/dashboard/Performance.qml -@@ -1,227 +1,826 @@ -+import QtQuick -+import QtQuick.Controls -+import QtQuick.Layouts -+import Quickshell.Services.UPower -+import Caelestia.Config -+import Caelestia.Internal - import qs.components - import qs.components.misc - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - --RowLayout { -+Item { - id: root - -- readonly property int padding: Appearance.padding.large -+ readonly property int minWidth: 400 + 400 + Tokens.spacing.normal + 120 + Tokens.padding.large * 2 - - function displayTemp(temp: real): string { -- return `${Math.ceil(Config.services.useFahrenheit ? temp * 1.8 + 32 : temp)}°${Config.services.useFahrenheit ? "F" : "C"}`; -+ return `${Math.ceil(GlobalConfig.services.useFahrenheitPerformance ? temp * 1.8 + 32 : temp)}°${GlobalConfig.services.useFahrenheitPerformance ? "F" : "C"}`; - } - -- spacing: Appearance.spacing.large * 3 -+ implicitWidth: Math.max(minWidth, content.implicitWidth) -+ implicitHeight: placeholder.visible ? placeholder.height : content.implicitHeight - -- Ref { -- service: SystemUsage -- } -+ StyledRect { -+ id: placeholder -+ -+ anchors.centerIn: parent -+ width: 400 -+ height: 350 -+ radius: Tokens.rounding.large -+ color: Colours.tPalette.m3surfaceContainer -+ visible: !Config.dashboard.performance.showCpu && !(Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") && !Config.dashboard.performance.showMemory && !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork && !(UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery) - -- Resource { -- Layout.alignment: Qt.AlignVCenter -- Layout.topMargin: root.padding -- Layout.bottomMargin: root.padding -- Layout.leftMargin: root.padding * 2 -+ ColumnLayout { -+ anchors.centerIn: parent -+ spacing: Tokens.spacing.normal - -- value1: Math.min(1, SystemUsage.gpuTemp / 90) -- value2: SystemUsage.gpuPerc -+ MaterialIcon { -+ Layout.alignment: Qt.AlignHCenter -+ text: "tune" -+ font.pointSize: Tokens.font.size.extraLarge * 2 -+ color: Colours.palette.m3onSurfaceVariant -+ } - -- label1: root.displayTemp(SystemUsage.gpuTemp) -- label2: `${Math.round(SystemUsage.gpuPerc * 100)}%` -+ StyledText { -+ Layout.alignment: Qt.AlignHCenter -+ text: qsTr("No widgets enabled") -+ font.pointSize: Tokens.font.size.large -+ color: Colours.palette.m3onSurface -+ } - -- sublabel1: qsTr("GPU temp") -- sublabel2: qsTr("Usage") -+ StyledText { -+ Layout.alignment: Qt.AlignHCenter -+ text: qsTr("Enable widgets in dashboard settings") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ } - } - -- Resource { -- Layout.alignment: Qt.AlignVCenter -- Layout.topMargin: root.padding -- Layout.bottomMargin: root.padding -+ RowLayout { -+ id: content - -- primary: true -+ anchors.left: parent.left -+ anchors.right: parent.right -+ spacing: Tokens.spacing.normal -+ visible: !placeholder.visible - -- value1: Math.min(1, SystemUsage.cpuTemp / 90) -- value2: SystemUsage.cpuPerc -+ Ref { -+ service: SystemUsage -+ } -+ -+ ColumnLayout { -+ id: mainColumn -+ -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ visible: Config.dashboard.performance.showCpu || (Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE") -+ -+ HeroCard { -+ Layout.fillWidth: true -+ Layout.minimumWidth: 400 -+ Layout.preferredHeight: 150 -+ visible: Config.dashboard.performance.showCpu -+ icon: "memory" -+ title: SystemUsage.cpuName ? `CPU - ${SystemUsage.cpuName}` : qsTr("CPU") -+ mainValue: `${Math.round(SystemUsage.cpuPerc * 100)}%` -+ mainLabel: qsTr("Usage") -+ secondaryValue: root.displayTemp(SystemUsage.cpuTemp) -+ secondaryLabel: qsTr("Temp") -+ usage: SystemUsage.cpuPerc -+ temperature: SystemUsage.cpuTemp -+ accentColor: Colours.palette.m3primary -+ } -+ -+ HeroCard { -+ Layout.fillWidth: true -+ Layout.minimumWidth: 400 -+ Layout.preferredHeight: 150 -+ visible: Config.dashboard.performance.showGpu && SystemUsage.gpuType !== "NONE" -+ icon: "desktop_windows" -+ title: SystemUsage.gpuName ? `GPU - ${SystemUsage.gpuName}` : qsTr("GPU") -+ mainValue: `${Math.round(SystemUsage.gpuPerc * 100)}%` -+ mainLabel: qsTr("Usage") -+ secondaryValue: root.displayTemp(SystemUsage.gpuTemp) -+ secondaryLabel: qsTr("Temp") -+ usage: SystemUsage.gpuPerc -+ temperature: SystemUsage.gpuTemp -+ accentColor: Colours.palette.m3secondary -+ } -+ } - -- label1: root.displayTemp(SystemUsage.cpuTemp) -- label2: `${Math.round(SystemUsage.cpuPerc * 100)}%` -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ visible: Config.dashboard.performance.showMemory || Config.dashboard.performance.showStorage || Config.dashboard.performance.showNetwork -+ -+ GaugeCard { -+ Layout.minimumWidth: 250 -+ Layout.preferredHeight: 220 -+ Layout.fillWidth: !Config.dashboard.performance.showStorage && !Config.dashboard.performance.showNetwork -+ icon: "memory_alt" -+ title: qsTr("Memory") -+ percentage: SystemUsage.memPerc -+ subtitle: { -+ const usedFmt = SystemUsage.formatKib(SystemUsage.memUsed); -+ const totalFmt = SystemUsage.formatKib(SystemUsage.memTotal); -+ return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; -+ } -+ accentColor: Colours.palette.m3tertiary -+ visible: Config.dashboard.performance.showMemory -+ } -+ -+ StorageGaugeCard { -+ Layout.minimumWidth: 250 -+ Layout.preferredHeight: 220 -+ Layout.fillWidth: !Config.dashboard.performance.showNetwork -+ visible: Config.dashboard.performance.showStorage -+ } -+ -+ NetworkCard { -+ Layout.fillWidth: true -+ Layout.minimumWidth: 200 -+ Layout.preferredHeight: 220 -+ visible: Config.dashboard.performance.showNetwork -+ } -+ } -+ } - -- sublabel1: qsTr("CPU temp") -- sublabel2: qsTr("Usage") -+ BatteryTank { -+ Layout.preferredWidth: 120 -+ Layout.preferredHeight: mainColumn.implicitHeight -+ visible: UPower.displayDevice.isLaptopBattery && Config.dashboard.performance.showBattery -+ } - } - -- Resource { -- Layout.alignment: Qt.AlignVCenter -- Layout.topMargin: root.padding -- Layout.bottomMargin: root.padding -- Layout.rightMargin: root.padding * 3 -+ component BatteryTank: StyledClippingRect { -+ id: batteryTank -+ -+ property real percentage: UPower.displayDevice.percentage -+ property bool isCharging: UPower.displayDevice.state === UPowerDeviceState.Charging -+ property color accentColor: Colours.palette.m3primary -+ property real animatedPercentage: 0 -+ -+ color: Colours.tPalette.m3surfaceContainer -+ radius: Tokens.rounding.large -+ Component.onCompleted: animatedPercentage = percentage -+ onPercentageChanged: animatedPercentage = percentage -+ -+ // Background Fill -+ StyledRect { -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.bottom: parent.bottom -+ height: parent.height * batteryTank.animatedPercentage -+ color: Qt.alpha(batteryTank.accentColor, 0.15) -+ } - -- value1: SystemUsage.memPerc -- value2: SystemUsage.storagePerc -+ ColumnLayout { -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.small -+ -+ // Header Section -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.small -+ -+ MaterialIcon { -+ text: { -+ if (!UPower.displayDevice.isLaptopBattery) { -+ if (PowerProfiles.profile === PowerProfile.PowerSaver) -+ return "energy_savings_leaf"; -+ -+ if (PowerProfiles.profile === PowerProfile.Performance) -+ return "rocket_launch"; -+ -+ return "balance"; -+ } -+ if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) -+ return "battery_full"; -+ -+ const perc = UPower.displayDevice.percentage; -+ const charging = [UPowerDeviceState.Charging, UPowerDeviceState.PendingCharge].includes(UPower.displayDevice.state); -+ if (perc >= 0.99) -+ return "battery_full"; -+ -+ let level = Math.floor(perc * 7); -+ if (charging && (level === 4 || level === 1)) -+ level--; -+ -+ return charging ? `battery_charging_${(level + 3) * 10}` : `battery_${level}_bar`; -+ } -+ font.pointSize: Tokens.font.size.large -+ color: batteryTank.accentColor -+ } -+ -+ StyledText { -+ Layout.fillWidth: true -+ text: qsTr("Battery") -+ font.pointSize: Tokens.font.size.normal -+ color: Colours.palette.m3onSurface -+ } -+ } - -- label1: { -- const fmt = SystemUsage.formatKib(SystemUsage.memUsed); -- return `${+fmt.value.toFixed(1)}${fmt.unit}`; -+ Item { -+ Layout.fillHeight: true -+ } -+ -+ // Bottom Info Section -+ ColumnLayout { -+ Layout.fillWidth: true -+ spacing: -4 -+ -+ StyledText { -+ Layout.alignment: Qt.AlignRight -+ text: `${Math.round(batteryTank.percentage * 100)}%` -+ font.pointSize: Tokens.font.size.extraLarge -+ font.weight: Font.Medium -+ color: batteryTank.accentColor -+ } -+ -+ StyledText { -+ Layout.alignment: Qt.AlignRight -+ text: { -+ if (UPower.displayDevice.state === UPowerDeviceState.FullyCharged) -+ return qsTr("Full"); -+ -+ if (batteryTank.isCharging) -+ return qsTr("Charging"); -+ -+ const s = UPower.displayDevice.timeToEmpty; -+ if (s === 0) -+ return qsTr("..."); -+ -+ const hr = Math.floor(s / 3600); -+ const min = Math.floor((s % 3600) / 60); -+ if (hr > 0) -+ return `${hr}h ${min}m`; -+ -+ return `${min}m`; -+ } -+ font.pointSize: Tokens.font.size.smaller -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ } -+ } -+ -+ Behavior on animatedPercentage { -+ Anim { -+ type: Anim.StandardLarge -+ } - } -- label2: { -- const fmt = SystemUsage.formatKib(SystemUsage.storageUsed); -- return `${Math.floor(fmt.value)}${fmt.unit}`; -+ } -+ -+ component CardHeader: RowLayout { -+ property string icon -+ property string title -+ property color accentColor: Colours.palette.m3primary -+ -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.small -+ -+ MaterialIcon { -+ text: parent.icon -+ fill: 1 -+ color: parent.accentColor -+ font.pointSize: Tokens.spacing.large - } - -- sublabel1: qsTr("Memory") -- sublabel2: qsTr("Storage") -+ StyledText { -+ Layout.fillWidth: true -+ text: parent.title -+ font.pointSize: Tokens.font.size.normal -+ elide: Text.ElideRight -+ } - } - -- component Resource: Item { -- id: res -+ component ProgressBar: StyledRect { -+ id: progressBar -+ -+ property real value: 0 -+ property color fgColor: Colours.palette.m3primary -+ property color bgColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -+ property real animatedValue: 0 -+ -+ color: bgColor -+ radius: Tokens.rounding.full -+ Component.onCompleted: animatedValue = value -+ onValueChanged: animatedValue = value -+ -+ StyledRect { -+ anchors.left: parent.left -+ anchors.top: parent.top -+ anchors.bottom: parent.bottom -+ width: parent.width * progressBar.animatedValue -+ color: progressBar.fgColor -+ radius: Tokens.rounding.full -+ } - -- required property real value1 -- required property real value2 -- required property string sublabel1 -- required property string sublabel2 -- required property string label1 -- required property string label2 -+ Behavior on animatedValue { -+ Anim { -+ type: Anim.StandardLarge -+ } -+ } -+ } - -- property bool primary -- readonly property real primaryMult: primary ? 1.2 : 1 -+ component HeroCard: StyledClippingRect { -+ id: heroCard -+ -+ property string icon -+ property string title -+ property string mainValue -+ property string mainLabel -+ property string secondaryValue -+ property string secondaryLabel -+ property real usage: 0 -+ property real temperature: 0 -+ property color accentColor: Colours.palette.m3primary -+ readonly property real maxTemp: 100 -+ readonly property real tempProgress: Math.min(1, Math.max(0, temperature / maxTemp)) -+ property real animatedUsage: 0 -+ property real animatedTemp: 0 -+ -+ color: Colours.tPalette.m3surfaceContainer -+ radius: Tokens.rounding.large -+ Component.onCompleted: { -+ animatedUsage = usage; -+ animatedTemp = tempProgress; -+ } -+ onUsageChanged: animatedUsage = usage -+ onTempProgressChanged: animatedTemp = tempProgress -+ -+ StyledRect { -+ anchors.left: parent.left -+ anchors.top: parent.top -+ anchors.bottom: parent.bottom -+ implicitWidth: parent.width * heroCard.animatedUsage -+ color: Qt.alpha(heroCard.accentColor, 0.15) -+ } - -- readonly property real thickness: Config.dashboard.sizes.resourceProgessThickness * primaryMult -+ CardHeader { -+ anchors.left: parent.left -+ anchors.top: parent.top -+ anchors.leftMargin: Tokens.padding.large -+ anchors.topMargin: Math.round(Tokens.padding.large * 1.2) - -- property color fg1: Colours.palette.m3primary -- property color fg2: Colours.palette.m3secondary -- property color bg1: Colours.palette.m3primaryContainer -- property color bg2: Colours.palette.m3secondaryContainer -+ width: parent.width - anchors.leftMargin - usageColumn.anchors.rightMargin - usageLabel.width - Tokens.spacing.normal -+ icon: heroCard.icon -+ title: heroCard.title -+ accentColor: heroCard.accentColor -+ } - -- implicitWidth: Config.dashboard.sizes.resourceSize * primaryMult -- implicitHeight: Config.dashboard.sizes.resourceSize * primaryMult -+ Column { -+ anchors.left: parent.left -+ anchors.right: parent.right -+ anchors.bottom: parent.bottom -+ anchors.margins: Math.round(Tokens.padding.large * 1.2) -+ anchors.bottomMargin: Math.round(Tokens.padding.large * 1.3) -+ -+ spacing: Tokens.spacing.small -+ -+ Row { -+ spacing: Tokens.spacing.small -+ -+ StyledText { -+ text: heroCard.secondaryValue -+ font.pointSize: Tokens.font.size.normal -+ font.weight: Font.Medium -+ } -+ -+ StyledText { -+ text: heroCard.secondaryLabel -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ anchors.baseline: parent.children[0].baseline -+ } -+ } - -- onValue1Changed: canvas.requestPaint() -- onValue2Changed: canvas.requestPaint() -- onFg1Changed: canvas.requestPaint() -- onFg2Changed: canvas.requestPaint() -- onBg1Changed: canvas.requestPaint() -- onBg2Changed: canvas.requestPaint() -+ ProgressBar { -+ implicitWidth: parent.width * 0.5 -+ implicitHeight: 6 -+ value: heroCard.tempProgress -+ fgColor: heroCard.accentColor -+ bgColor: Qt.alpha(heroCard.accentColor, 0.2) -+ } -+ } - - Column { -- anchors.centerIn: parent -+ id: usageColumn -+ -+ anchors.right: parent.right -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.margins: Tokens.padding.large -+ anchors.rightMargin: 32 -+ spacing: 0 - - StyledText { -- anchors.horizontalCenter: parent.horizontalCenter -+ id: usageLabel - -- text: res.label1 -- font.pointSize: Appearance.font.size.extraLarge * res.primaryMult -+ anchors.right: parent.right -+ text: heroCard.mainLabel -+ font.pointSize: Tokens.font.size.normal -+ color: Colours.palette.m3onSurfaceVariant - } - - StyledText { -- anchors.horizontalCenter: parent.horizontalCenter -+ anchors.right: parent.right -+ text: heroCard.mainValue -+ font.pointSize: Tokens.font.size.extraLarge -+ font.weight: Font.Medium -+ color: heroCard.accentColor -+ } -+ } - -- text: res.sublabel1 -- color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.smaller * res.primaryMult -+ Behavior on animatedUsage { -+ Anim { -+ type: Anim.StandardLarge - } - } - -- Column { -- anchors.horizontalCenter: parent.right -- anchors.top: parent.verticalCenter -- anchors.horizontalCenterOffset: -res.thickness / 2 -- anchors.topMargin: res.thickness / 2 + Appearance.spacing.small -+ Behavior on animatedTemp { -+ Anim { -+ type: Anim.StandardLarge -+ } -+ } -+ } - -- StyledText { -- anchors.horizontalCenter: parent.horizontalCenter -+ component GaugeCard: StyledRect { -+ id: gaugeCard -+ -+ property string icon -+ property string title -+ property real percentage: 0 -+ property string subtitle -+ property color accentColor: Colours.palette.m3primary -+ readonly property real arcStartAngle: 0.75 * Math.PI -+ readonly property real arcSweep: 1.5 * Math.PI -+ property real animatedPercentage: 0 -+ -+ color: Colours.tPalette.m3surfaceContainer -+ radius: Tokens.rounding.large -+ clip: true -+ Component.onCompleted: animatedPercentage = percentage -+ onPercentageChanged: animatedPercentage = percentage -+ -+ ColumnLayout { -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.smaller - -- text: res.label2 -- font.pointSize: Appearance.font.size.smaller * res.primaryMult -+ CardHeader { -+ icon: gaugeCard.icon -+ title: gaugeCard.title -+ accentColor: gaugeCard.accentColor - } - -- StyledText { -- anchors.horizontalCenter: parent.horizontalCenter -+ Item { -+ Layout.fillWidth: true -+ Layout.fillHeight: true -+ -+ ArcGauge { -+ anchors.centerIn: parent -+ width: Math.min(parent.width, parent.height) -+ height: width -+ percentage: gaugeCard.animatedPercentage -+ accentColor: gaugeCard.accentColor -+ trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -+ startAngle: gaugeCard.arcStartAngle -+ sweepAngle: gaugeCard.arcSweep -+ } -+ -+ StyledText { -+ anchors.centerIn: parent -+ text: `${Math.round(gaugeCard.percentage * 100)}%` -+ font.pointSize: Tokens.font.size.extraLarge -+ font.weight: Font.Medium -+ color: gaugeCard.accentColor -+ } -+ } - -- text: res.sublabel2 -+ StyledText { -+ Layout.alignment: Qt.AlignHCenter -+ text: gaugeCard.subtitle -+ font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small * res.primaryMult - } - } - -- Canvas { -- id: canvas -+ Behavior on animatedPercentage { -+ Anim { -+ type: Anim.StandardLarge -+ } -+ } -+ } - -- readonly property real centerX: width / 2 -- readonly property real centerY: height / 2 -+ component StorageGaugeCard: StyledRect { -+ id: storageGaugeCard -+ -+ property int currentDiskIndex: 0 -+ readonly property var currentDisk: SystemUsage.disks.length > 0 ? SystemUsage.disks[currentDiskIndex] : null -+ property int diskCount: 0 -+ readonly property real arcStartAngle: 0.75 * Math.PI -+ readonly property real arcSweep: 1.5 * Math.PI -+ property real animatedPercentage: 0 -+ property color accentColor: Colours.palette.m3secondary -+ -+ color: Colours.tPalette.m3surfaceContainer -+ radius: Tokens.rounding.large -+ clip: true -+ Component.onCompleted: { -+ diskCount = SystemUsage.disks.length; -+ if (currentDisk) -+ animatedPercentage = currentDisk.perc; -+ } -+ onCurrentDiskChanged: { -+ if (currentDisk) -+ animatedPercentage = currentDisk.perc; -+ } -+ -+ // Update diskCount and animatedPercentage when disks data changes -+ Connections { -+ function onDisksChanged() { -+ if (SystemUsage.disks.length !== storageGaugeCard.diskCount) -+ storageGaugeCard.diskCount = SystemUsage.disks.length; - -- readonly property real arc1Start: degToRad(45) -- readonly property real arc1End: degToRad(220) -- readonly property real arc2Start: degToRad(230) -- readonly property real arc2End: degToRad(360) -+ // Update animated percentage when disk data refreshes -+ if (storageGaugeCard.currentDisk) -+ storageGaugeCard.animatedPercentage = storageGaugeCard.currentDisk.perc; -+ } -+ -+ target: SystemUsage -+ } - -- function degToRad(deg: int): real { -- return deg * Math.PI / 180; -+ MouseArea { -+ anchors.fill: parent -+ onWheel: wheel => { -+ if (wheel.angleDelta.y > 0) -+ storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex - 1 + storageGaugeCard.diskCount) % storageGaugeCard.diskCount; -+ else if (wheel.angleDelta.y < 0) -+ storageGaugeCard.currentDiskIndex = (storageGaugeCard.currentDiskIndex + 1) % storageGaugeCard.diskCount; - } -+ } - -+ ColumnLayout { - anchors.fill: parent -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.smaller -+ -+ CardHeader { -+ icon: "hard_disk" -+ title: { -+ const base = qsTr("Storage"); -+ if (!storageGaugeCard.currentDisk) -+ return base; -+ -+ return `${base} - ${storageGaugeCard.currentDisk.mount}`; -+ } -+ accentColor: storageGaugeCard.accentColor -+ -+ // Scroll hint icon -+ MaterialIcon { -+ text: "unfold_more" -+ color: Colours.palette.m3onSurfaceVariant -+ font.pointSize: Tokens.font.size.normal -+ visible: storageGaugeCard.diskCount > 1 -+ opacity: 0.7 -+ ToolTip.visible: hintHover.hovered -+ ToolTip.text: qsTr("Scroll to switch disks") -+ ToolTip.delay: 500 -+ -+ HoverHandler { -+ id: hintHover -+ } -+ } -+ } - -- onPaint: { -- const ctx = getContext("2d"); -- ctx.reset(); -+ Item { -+ Layout.fillWidth: true -+ Layout.fillHeight: true -+ -+ ArcGauge { -+ anchors.centerIn: parent -+ width: Math.min(parent.width, parent.height) -+ height: width -+ percentage: storageGaugeCard.animatedPercentage -+ accentColor: storageGaugeCard.accentColor -+ trackColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -+ startAngle: storageGaugeCard.arcStartAngle -+ sweepAngle: storageGaugeCard.arcSweep -+ } -+ -+ StyledText { -+ anchors.centerIn: parent -+ text: storageGaugeCard.currentDisk ? `${Math.round(storageGaugeCard.currentDisk.perc * 100)}%` : "—" -+ font.pointSize: Tokens.font.size.extraLarge -+ font.weight: Font.Medium -+ color: storageGaugeCard.accentColor -+ } -+ } - -- ctx.lineWidth = res.thickness; -- ctx.lineCap = Appearance.rounding.scale === 0 ? "square" : "round"; -+ StyledText { -+ Layout.alignment: Qt.AlignHCenter -+ text: { -+ if (!storageGaugeCard.currentDisk) -+ return "—"; -+ -+ const usedFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.used); -+ const totalFmt = SystemUsage.formatKib(storageGaugeCard.currentDisk.total); -+ return `${usedFmt.value.toFixed(1)} / ${Math.floor(totalFmt.value)} ${totalFmt.unit}`; -+ } -+ font.pointSize: Tokens.font.size.smaller -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ } - -- const radius = (Math.min(width, height) - ctx.lineWidth) / 2; -- const cx = centerX; -- const cy = centerY; -- const a1s = arc1Start; -- const a1e = arc1End; -- const a2s = arc2Start; -- const a2e = arc2End; -+ Behavior on animatedPercentage { -+ Anim { -+ type: Anim.StandardLarge -+ } -+ } -+ } - -- ctx.beginPath(); -- ctx.arc(cx, cy, radius, a1s, a1e, false); -- ctx.strokeStyle = res.bg1; -- ctx.stroke(); -+ component NetworkCard: StyledRect { -+ id: networkCard - -- ctx.beginPath(); -- ctx.arc(cx, cy, radius, a1s, (a1e - a1s) * res.value1 + a1s, false); -- ctx.strokeStyle = res.fg1; -- ctx.stroke(); -+ property color accentColor: Colours.palette.m3primary - -- ctx.beginPath(); -- ctx.arc(cx, cy, radius, a2s, a2e, false); -- ctx.strokeStyle = res.bg2; -- ctx.stroke(); -+ color: Colours.tPalette.m3surfaceContainer -+ radius: Tokens.rounding.large -+ clip: true - -- ctx.beginPath(); -- ctx.arc(cx, cy, radius, a2s, (a2e - a2s) * res.value2 + a2s, false); -- ctx.strokeStyle = res.fg2; -- ctx.stroke(); -- } -+ Ref { -+ service: NetworkUsage - } - -- Behavior on value1 { -- Anim {} -- } -+ ColumnLayout { -+ anchors.fill: parent -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.small - -- Behavior on value2 { -- Anim {} -- } -+ CardHeader { -+ icon: "swap_vert" -+ title: qsTr("Network") -+ accentColor: networkCard.accentColor -+ } - -- Behavior on fg1 { -- CAnim {} -- } -+ // Sparkline graph -+ Item { -+ Layout.fillWidth: true -+ Layout.fillHeight: true -+ -+ SparklineItem { -+ id: sparkline -+ -+ property real targetMax: 1024 -+ property real smoothMax: targetMax -+ -+ anchors.fill: parent -+ line1: NetworkUsage.uploadBuffer // qmllint disable missing-type -+ line1Color: Colours.palette.m3secondary -+ line1FillAlpha: 0.15 -+ line2: NetworkUsage.downloadBuffer // qmllint disable missing-type -+ line2Color: Colours.palette.m3tertiary -+ line2FillAlpha: 0.2 -+ maxValue: smoothMax -+ historyLength: NetworkUsage.historyLength -+ -+ Connections { -+ function onValuesChanged(): void { -+ sparkline.targetMax = Math.max(NetworkUsage.downloadBuffer.maximum, NetworkUsage.uploadBuffer.maximum, 1024); -+ slideAnim.restart(); -+ } -+ -+ target: NetworkUsage.downloadBuffer -+ } -+ -+ NumberAnimation { -+ id: slideAnim -+ -+ target: sparkline -+ property: "slideProgress" -+ from: 0 -+ to: 1 -+ duration: GlobalConfig.dashboard.resourceUpdateInterval -+ } -+ -+ Behavior on smoothMax { -+ Anim { -+ type: Anim.StandardLarge -+ } -+ } -+ } -+ -+ // "No data" placeholder -+ StyledText { -+ anchors.centerIn: parent -+ text: qsTr("Collecting data...") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ visible: NetworkUsage.downloadBuffer.count < 2 -+ opacity: 0.6 -+ } -+ } - -- Behavior on fg2 { -- CAnim {} -- } -+ // Download row -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ -+ MaterialIcon { -+ text: "download" -+ color: Colours.palette.m3tertiary -+ font.pointSize: Tokens.font.size.normal -+ } -+ -+ StyledText { -+ text: qsTr("Download") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ -+ StyledText { -+ text: { -+ const fmt = NetworkUsage.formatBytes(NetworkUsage.downloadSpeed ?? 0); -+ return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; -+ } -+ font.pointSize: Tokens.font.size.normal -+ font.weight: Font.Medium -+ color: Colours.palette.m3tertiary -+ } -+ } - -- Behavior on bg1 { -- CAnim {} -- } -+ // Upload row -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ -+ MaterialIcon { -+ text: "upload" -+ color: Colours.palette.m3secondary -+ font.pointSize: Tokens.font.size.normal -+ } -+ -+ StyledText { -+ text: qsTr("Upload") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ -+ StyledText { -+ text: { -+ const fmt = NetworkUsage.formatBytes(NetworkUsage.uploadSpeed ?? 0); -+ return fmt ? `${fmt.value.toFixed(1)} ${fmt.unit}` : "0.0 B/s"; -+ } -+ font.pointSize: Tokens.font.size.normal -+ font.weight: Font.Medium -+ color: Colours.palette.m3secondary -+ } -+ } - -- Behavior on bg2 { -- CAnim {} -+ // Session totals -+ RowLayout { -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.normal -+ -+ MaterialIcon { -+ text: "history" -+ color: Colours.palette.m3onSurfaceVariant -+ font.pointSize: Tokens.font.size.normal -+ } -+ -+ StyledText { -+ text: qsTr("Total") -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ -+ Item { -+ Layout.fillWidth: true -+ } -+ -+ StyledText { -+ text: { -+ const down = NetworkUsage.formatBytesTotal(NetworkUsage.downloadTotal ?? 0); -+ const up = NetworkUsage.formatBytesTotal(NetworkUsage.uploadTotal ?? 0); -+ return (down && up) ? `↓${down.value.toFixed(1)}${down.unit} ↑${up.value.toFixed(1)}${up.unit}` : "↓0.0B ↑0.0B"; -+ } -+ font.pointSize: Tokens.font.size.small -+ color: Colours.palette.m3onSurfaceVariant -+ } -+ } - } - } - } -diff --git a/modules/dashboard/Tabs.qml b/modules/dashboard/Tabs.qml -index eef01be7..5bf99ca3 100644 ---- a/modules/dashboard/Tabs.qml -+++ b/modules/dashboard/Tabs.qml -@@ -1,19 +1,21 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Controls -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Controls - - Item { - id: root - - required property real nonAnimWidth -- required property PersistentProperties state -+ required property DashboardState dashState -+ required property var tabs -+ - readonly property alias count: bar.count - - implicitHeight: bar.implicitHeight + indicator.implicitHeight + indicator.anchors.topMargin + separator.implicitHeight -@@ -25,55 +27,45 @@ Item { - anchors.right: parent.right - anchors.top: parent.top - -- currentIndex: root.state.currentTab -+ currentIndex: root.dashState.currentTab - background: null - -- onCurrentIndexChanged: root.state.currentTab = currentIndex -- -- Tab { -- iconName: "dashboard" -- text: qsTr("Dashboard") -- } -- -- Tab { -- iconName: "queue_music" -- text: qsTr("Media") -- } -+ onCurrentIndexChanged: root.dashState.currentTab = currentIndex - -- Tab { -- iconName: "speed" -- text: qsTr("Performance") -- } -+ Repeater { -+ model: ScriptModel { -+ values: root.tabs -+ } - -- Tab { -- iconName: "cloud" -- text: qsTr("Weather") -- } -+ delegate: Tab { -+ required property var modelData - -- Tab { -- iconName: "monitor" -- text: qsTr("Monitors") -+ iconName: modelData.iconName -+ text: modelData.text -+ } - } -- -- // Tab { -- // iconName: "workspaces" -- // text: qsTr("Workspaces") -- // } - } - - Item { - id: indicator - - anchors.top: bar.bottom -- anchors.topMargin: Config.dashboard.sizes.tabIndicatorSpacing -+ anchors.topMargin: 5 - -- implicitWidth: bar.currentItem.implicitWidth -- implicitHeight: Config.dashboard.sizes.tabIndicatorHeight -+ implicitWidth: { -+ const tab = bar.currentItem; -+ if (tab) -+ return tab.implicitWidth; -+ const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; -+ return width; -+ } -+ implicitHeight: 3 - - x: { - const tab = bar.currentItem; - const width = (root.nonAnimWidth - bar.spacing * (bar.count - 1)) / bar.count; -- return width * tab.TabBar.index + (width - tab.implicitWidth) / 2; -+ const tabWidth = tab?.implicitWidth ?? width; -+ return width * bar.currentIndex + (width - tabWidth) / 2; - } - - clip: true -@@ -85,7 +77,7 @@ Item { - implicitHeight: parent.implicitHeight * 2 - - color: Colours.palette.m3primary -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - } - - Behavior on x { -@@ -119,13 +111,21 @@ Item { - contentItem: CustomMouseArea { - id: mouse - -+ function onWheel(event: WheelEvent): void { -+ if (event.angleDelta.y < 0) -+ root.dashState.currentTab = Math.min(root.dashState.currentTab + 1, bar.count - 1); -+ else if (event.angleDelta.y > 0) -+ root.dashState.currentTab = Math.max(root.dashState.currentTab - 1, 0); -+ } -+ - implicitWidth: Math.max(icon.width, label.width) - implicitHeight: icon.height + label.height - -+ hoverEnabled: true - cursorShape: Qt.PointingHandCursor - - onPressed: event => { -- root.state.currentTab = tab.TabBar.index; -+ root.dashState.currentTab = tab.TabBar.index; - - const stateY = stateWrapper.y; - rippleAnim.x = event.x; -@@ -137,13 +137,6 @@ Item { - rippleAnim.restart(); - } - -- function onWheel(event: WheelEvent): void { -- if (event.angleDelta.y < 0) -- root.state.currentTab = Math.min(root.state.currentTab + 1, bar.count - 1); -- else if (event.angleDelta.y > 0) -- root.state.currentTab = Math.max(root.state.currentTab - 1, 0); -- } -- - SequentialAnimation { - id: rippleAnim - -@@ -171,16 +164,13 @@ Item { - properties: "implicitWidth,implicitHeight" - from: 0 - to: rippleAnim.radius * 2 -- duration: Appearance.anim.durations.normal -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ duration: Tokens.anim.durations.normal -+ easing: Tokens.anim.standardDecel - } - Anim { - target: ripple - property: "opacity" - to: 0 -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard - } - } - -@@ -190,10 +180,10 @@ Item { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- implicitHeight: parent.height + Config.dashboard.sizes.tabIndicatorSpacing * 2 -+ implicitHeight: parent.height + Tokens.sizes.dashboard.tabIndicatorSpacing * 2 - - color: "transparent" -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - - StyledRect { - id: stateLayer -@@ -201,7 +191,7 @@ Item { - anchors.fill: parent - - color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface -- opacity: mouse.pressed ? 0.1 : tab.hovered ? 0.08 : 0 -+ opacity: mouse.pressed ? 0.1 : mouse.containsMouse ? 0.08 : 0 - - Behavior on opacity { - Anim {} -@@ -211,7 +201,7 @@ Item { - StyledRect { - id: ripple - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurface - opacity: 0 - -@@ -231,7 +221,7 @@ Item { - text: tab.iconName - color: tab.current ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant - fill: tab.current ? 1 : 0 -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - - Behavior on fill { - Anim {} -diff --git a/modules/dashboard/Weather.qml b/modules/dashboard/WeatherTab.qml -similarity index 74% -rename from modules/dashboard/Weather.qml -rename to modules/dashboard/WeatherTab.qml -index 3981633a..3cf8d3b7 100644 ---- a/modules/dashboard/Weather.qml -+++ b/modules/dashboard/WeatherTab.qml -@@ -1,43 +1,42 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root - -- implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 -- implicitHeight: layout.implicitHeight -- - readonly property var today: Weather.forecast && Weather.forecast.length > 0 ? Weather.forecast[0] : null - -+ implicitWidth: layout.implicitWidth > 800 ? layout.implicitWidth : 840 -+ implicitHeight: layout.implicitHeight - Component.onCompleted: Weather.reload() - - ColumnLayout { - id: layout - - anchors.fill: parent -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - RowLayout { -- Layout.leftMargin: Appearance.padding.large -- Layout.rightMargin: Appearance.padding.large -+ Layout.leftMargin: Tokens.padding.large -+ Layout.rightMargin: Tokens.padding.large - Layout.fillWidth: true - - Column { -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { - text: Weather.city || qsTr("Loading...") -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - font.weight: 600 - color: Colours.palette.m3onSurface - } - - StyledText { - text: new Date().toLocaleDateString(Qt.locale(), "dddd, MMMM d") -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3onSurfaceVariant - } - } -@@ -47,7 +46,7 @@ Item { - } - - Row { -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - WeatherStat { - icon: "wb_twilight" -@@ -67,40 +66,40 @@ Item { - - StyledRect { - Layout.fillWidth: true -- implicitHeight: bigInfoRow.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: bigInfoRow.implicitHeight + Tokens.padding.small * 2 - -- radius: Appearance.rounding.large * 2 -+ radius: Tokens.rounding.large * 2 - color: Colours.tPalette.m3surfaceContainer - - RowLayout { - id: bigInfoRow - - anchors.centerIn: parent -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - text: Weather.icon -- font.pointSize: Appearance.font.size.extraLarge * 3 -+ font.pointSize: Tokens.font.size.extraLarge * 3 - color: Colours.palette.m3secondary - animate: true - } - - ColumnLayout { - Layout.alignment: Qt.AlignVCenter -- spacing: -Appearance.spacing.small -+ spacing: -Tokens.spacing.small - - StyledText { - text: Weather.temp -- font.pointSize: Appearance.font.size.extraLarge * 2 -+ font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 500 - color: Colours.palette.m3primary - } - - StyledText { -- Layout.leftMargin: Appearance.padding.small -+ Layout.leftMargin: Tokens.padding.small - text: Weather.description -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - color: Colours.palette.m3onSurfaceVariant - } - } -@@ -109,7 +108,7 @@ Item { - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - DetailCard { - icon: "water_drop" -@@ -132,18 +131,18 @@ Item { - } - - StyledText { -- Layout.topMargin: Appearance.spacing.normal -- Layout.leftMargin: Appearance.padding.normal -+ Layout.topMargin: Tokens.spacing.normal -+ Layout.leftMargin: Tokens.padding.normal - visible: forecastRepeater.count > 0 - text: qsTr("7-Day Forecast") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 600 - color: Colours.palette.m3onSurface - } - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - Repeater { - id: forecastRepeater -@@ -157,30 +156,30 @@ Item { - required property var modelData - - Layout.fillWidth: true -- implicitHeight: forecastItemColumn.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: forecastItemColumn.implicitHeight + Tokens.padding.normal * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: forecastItemColumn - - anchors.centerIn: parent -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: forecastItem.index === 0 ? qsTr("Today") : new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "ddd") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 600 - color: Colours.palette.m3primary - } - - StyledText { -- Layout.topMargin: -Appearance.spacing.small / 2 -+ Layout.topMargin: -Tokens.spacing.small / 2 - Layout.alignment: Qt.AlignHCenter - text: new Date(forecastItem.modelData.date).toLocaleDateString(Qt.locale(), "MMM d") -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - opacity: 0.7 - color: Colours.palette.m3onSurfaceVariant - } -@@ -188,13 +187,13 @@ Item { - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: forecastItem.modelData.icon -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - color: Colours.palette.m3secondary - } - - StyledText { - Layout.alignment: Qt.AlignHCenter -- text: Config.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" -+ text: GlobalConfig.services.useFahrenheit ? forecastItem.modelData.maxTempF + "°" + " / " + forecastItem.modelData.minTempF + "°" : forecastItem.modelData.maxTempC + "°" + " / " + forecastItem.modelData.minTempC + "°" - font.weight: 600 - color: Colours.palette.m3tertiary - } -@@ -214,17 +213,17 @@ Item { - - Layout.fillWidth: true - Layout.preferredHeight: 60 -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Row { - anchors.centerIn: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: detailRoot.icon - color: detailRoot.colour -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - anchors.verticalCenter: parent.verticalCenter - } - -@@ -234,7 +233,7 @@ Item { - - StyledText { - text: detailRoot.label -- font.pointSize: Appearance.font.size.smaller -+ font.pointSize: Tokens.font.size.smaller - opacity: 0.7 - horizontalAlignment: Text.AlignLeft - } -@@ -255,23 +254,23 @@ Item { - property string value - property color colour - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - MaterialIcon { - text: weatherStat.icon -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - color: weatherStat.colour - } - - Column { - StyledText { - text: weatherStat.label -- font.pointSize: Appearance.font.size.smaller -+ font.pointSize: Tokens.font.size.smaller - color: Colours.palette.m3onSurfaceVariant - } - StyledText { - text: weatherStat.value -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - font.weight: 600 - color: Colours.palette.m3onSurface - } -diff --git a/modules/dashboard/Wrapper.qml b/modules/dashboard/Wrapper.qml -index 0e37909e..f7f03742 100644 ---- a/modules/dashboard/Wrapper.qml -+++ b/modules/dashboard/Wrapper.qml -@@ -1,21 +1,19 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Caelestia -+import Caelestia.Config - import qs.components - import qs.components.filedialog --import qs.config - import qs.utils --import Caelestia --import Quickshell --import QtQuick - - Item { - id: root - -- required property PersistentProperties visibilities -- readonly property PersistentProperties dashState: PersistentProperties { -- property int currentTab -- property date currentDate: new Date() -- -+ required property DrawerVisibilities visibilities -+ readonly property bool needsKeyboard: (content.item as Content)?.needsKeyboard ?? false -+ readonly property DashboardState dashState: DashboardState { - reloadableId: "dashboardState" - } - readonly property FileDialog facePicker: FileDialog { -@@ -30,60 +28,19 @@ Item { - } - } - -- readonly property real nonAnimHeight: state === "visible" ? (content.item?.nonAnimHeight ?? 0) : 0 -- -- visible: height > 0 -- implicitHeight: 0 -- implicitWidth: content.implicitWidth -- -- onStateChanged: { -- if (state === "visible" && timer.running) { -- timer.triggered(); -- timer.stop(); -- } -- } -- -- states: State { -- name: "visible" -- when: root.visibilities.dashboard && Config.dashboard.enabled -- -- PropertyChanges { -- root.implicitHeight: content.implicitHeight -- } -- } -- -- transitions: [ -- Transition { -- from: "" -- to: "visible" -- -- Anim { -- target: root -- property: "implicitHeight" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- }, -- Transition { -- from: "visible" -- to: "" -- -- Anim { -- target: root -- property: "implicitHeight" -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- } -- ] -+ readonly property real nonAnimHeight: state === "visible" ? ((content.item as Content)?.nonAnimHeight ?? 0) : 0 -+ readonly property bool shouldBeActive: visibilities.dashboard && Config.dashboard.enabled -+ property real offsetScale: shouldBeActive ? 0 : 1 - -- Timer { -- id: timer -+ visible: offsetScale < 1 -+ anchors.topMargin: (-implicitHeight - 5) * offsetScale -+ implicitHeight: content.implicitHeight -+ implicitWidth: content.implicitWidth || 854 // Hard coded fallback for first open -+ opacity: 1 - offsetScale - -- running: true -- interval: Appearance.anim.durations.extraLarge -- onTriggered: { -- content.active = Qt.binding(() => (root.visibilities.dashboard && Config.dashboard.enabled) || root.visible); -- content.visible = true; -+ Behavior on offsetScale { -+ Anim { -+ type: Anim.DefaultSpatial - } - } - -@@ -93,12 +50,11 @@ Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom - -- visible: false -- active: true -+ active: root.shouldBeActive || root.visible - - sourceComponent: Content { - visibilities: root.visibilities -- state: root.dashState -+ dashState: root.dashState - facePicker: root.facePicker - } - } -diff --git a/modules/dashboard/dash/Calendar.qml b/modules/dashboard/dash/Calendar.qml -index 56c04938..e2af3c01 100644 ---- a/modules/dashboard/dash/Calendar.qml -+++ b/modules/dashboard/dash/Calendar.qml -@@ -1,61 +1,58 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.components.effects --import qs.components.controls --import qs.services --import qs.config - import QtQuick - import QtQuick.Controls - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.components.controls -+import qs.components.effects -+import qs.services - - CustomMouseArea { - id: root - -- required property var state -+ required property DashboardState dashState -+ -+ readonly property int currMonth: dashState.currentDate.getMonth() -+ readonly property int currYear: dashState.currentDate.getFullYear() - -- readonly property int currMonth: state.currentDate.getMonth() -- readonly property int currYear: state.currentDate.getFullYear() -+ function onWheel(event: WheelEvent): void { -+ if (event.angleDelta.y > 0) -+ root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1); -+ else if (event.angleDelta.y < 0) -+ root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); -+ } - - anchors.left: parent.left - anchors.right: parent.right - implicitHeight: inner.implicitHeight + inner.anchors.margins * 2 - - acceptedButtons: Qt.MiddleButton -- onClicked: root.state.currentDate = new Date() -- -- function onWheel(event: WheelEvent): void { -- if (event.angleDelta.y > 0) -- root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); -- else if (event.angleDelta.y < 0) -- root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); -- } -+ onClicked: root.dashState.currentDate = new Date() - - ColumnLayout { - id: inner - - anchors.fill: parent -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.small -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.small - - RowLayout { - id: monthNavigationRow - - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Item { - implicitWidth: implicitHeight -- implicitHeight: prevMonthText.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: prevMonthText.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: prevMonthStateLayer - -- radius: Appearance.rounding.full -- -- function onClicked(): void { -- root.state.currentDate = new Date(root.currYear, root.currMonth - 1, 1); -- } -+ radius: Tokens.rounding.full -+ onClicked: root.dashState.currentDate = new Date(root.currYear, root.currMonth - 1, 1) - } - - MaterialIcon { -@@ -64,7 +61,7 @@ CustomMouseArea { - anchors.centerIn: parent - text: "chevron_left" - color: Colours.palette.m3tertiary -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 700 - } - } -@@ -72,24 +69,24 @@ CustomMouseArea { - Item { - Layout.fillWidth: true - -- implicitWidth: monthYearDisplay.implicitWidth + Appearance.padding.small * 2 -- implicitHeight: monthYearDisplay.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: monthYearDisplay.implicitWidth + Tokens.padding.small * 2 -+ implicitHeight: monthYearDisplay.implicitHeight + Tokens.padding.small * 2 - - StateLayer { -+ onClicked: { -+ root.dashState.currentDate = new Date(); -+ } -+ - anchors.fill: monthYearDisplay -- anchors.margins: -Appearance.padding.small -- anchors.leftMargin: -Appearance.padding.normal -- anchors.rightMargin: -Appearance.padding.normal -+ anchors.margins: -Tokens.padding.small -+ anchors.leftMargin: -Tokens.padding.normal -+ anchors.rightMargin: -Tokens.padding.normal - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - disabled: { - const now = new Date(); - return root.currMonth === now.getMonth() && root.currYear === now.getFullYear(); - } -- -- function onClicked(): void { -- root.state.currentDate = new Date(); -- } - } - - StyledText { -@@ -98,7 +95,7 @@ CustomMouseArea { - anchors.centerIn: parent - text: grid.title - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - font.capitalization: Font.Capitalize - } -@@ -106,16 +103,16 @@ CustomMouseArea { - - Item { - implicitWidth: implicitHeight -- implicitHeight: nextMonthText.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: nextMonthText.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: nextMonthStateLayer - -- radius: Appearance.rounding.full -- -- function onClicked(): void { -- root.state.currentDate = new Date(root.currYear, root.currMonth + 1, 1); -+ onClicked: { -+ root.dashState.currentDate = new Date(root.currYear, root.currMonth + 1, 1); - } -+ -+ radius: Tokens.rounding.full - } - - MaterialIcon { -@@ -124,7 +121,7 @@ CustomMouseArea { - anchors.centerIn: parent - text: "chevron_right" - color: Colours.palette.m3tertiary -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 700 - } - } -@@ -167,7 +164,7 @@ CustomMouseArea { - required property var model - - implicitWidth: implicitHeight -- implicitHeight: text.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: text.implicitHeight + Tokens.padding.small * 2 - - StyledText { - id: text -@@ -184,7 +181,7 @@ CustomMouseArea { - return Colours.palette.m3onSurfaceVariant; - } - opacity: dayItem.model.today || dayItem.model.month === grid.month ? 1 : 0.4 -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - font.weight: 500 - } - } -@@ -208,7 +205,7 @@ CustomMouseArea { - implicitHeight: today?.implicitHeight ?? 0 - - clip: true -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3primary - - opacity: todayItem ? 1 : 0 -@@ -236,15 +233,13 @@ CustomMouseArea { - - Behavior on x { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - Behavior on y { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -diff --git a/modules/dashboard/dash/DateTime.qml b/modules/dashboard/dash/DateTime.qml -index e7404488..82bee151 100644 ---- a/modules/dashboard/dash/DateTime.qml -+++ b/modules/dashboard/dash/DateTime.qml -@@ -1,17 +1,17 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root - - anchors.top: parent.top - anchors.bottom: parent.bottom -- implicitWidth: Config.dashboard.sizes.dateTimeWidth -+ implicitWidth: Tokens.sizes.dashboard.dateTimeWidth - - ColumnLayout { - anchors.left: parent.left -@@ -24,8 +24,8 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: Time.hourStr - color: Colours.palette.m3secondary -- font.pointSize: Appearance.font.size.extraLarge -- font.family: Appearance.font.family.clock -+ font.pointSize: Tokens.font.size.extraLarge -+ font.family: Tokens.font.family.clock - font.weight: 600 - } - -@@ -33,8 +33,8 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: "•••" - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.extraLarge * 0.9 -- font.family: Appearance.font.family.clock -+ font.pointSize: Tokens.font.size.extraLarge * 0.9 -+ font.family: Tokens.font.family.clock - } - - StyledText { -@@ -42,22 +42,23 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: Time.minuteStr - color: Colours.palette.m3secondary -- font.pointSize: Appearance.font.size.extraLarge -- font.family: Appearance.font.family.clock -+ font.pointSize: Tokens.font.size.extraLarge -+ font.family: Tokens.font.family.clock - font.weight: 600 - } - - Loader { -+ asynchronous: true - Layout.alignment: Qt.AlignHCenter - -- active: Config.services.useTwelveHourClock -+ active: GlobalConfig.services.useTwelveHourClock - visible: active - - sourceComponent: StyledText { - text: Time.amPmStr - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.large -- font.family: Appearance.font.family.clock -+ font.pointSize: Tokens.font.size.large -+ font.family: Tokens.font.family.clock - font.weight: 600 - } - } -diff --git a/modules/dashboard/dash/Media.qml b/modules/dashboard/dash/Media.qml -index 3a2b685e..a14e5e38 100644 ---- a/modules/dashboard/dash/Media.qml -+++ b/modules/dashboard/dash/Media.qml -@@ -1,32 +1,33 @@ -+import QtQuick -+import QtQuick.Shapes -+import Quickshell -+import Caelestia.Config -+import Caelestia.Services - import qs.components - import qs.services --import qs.config - import qs.utils --import Caelestia.Services --import QtQuick --import QtQuick.Shapes - - Item { - id: root - - property real playerProgress: { - const active = Players.active; -- return active?.length ? active.position / active.length : 0; -+ return active?.length ? (active.position % active.length) / active.length : 0; - } - - anchors.top: parent.top - anchors.bottom: parent.bottom -- implicitWidth: Config.dashboard.sizes.mediaWidth -+ implicitWidth: Tokens.sizes.dashboard.mediaWidth - - Behavior on playerProgress { - Anim { -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - } - - Timer { - running: Players.active?.isPlaying ?? false -- interval: Config.dashboard.mediaUpdateInterval -+ interval: GlobalConfig.dashboard.mediaUpdateInterval - triggeredOnStart: true - repeat: true - onTriggered: Players.active?.positionChanged() -@@ -42,16 +43,16 @@ Item { - ShapePath { - fillColor: "transparent" - strokeColor: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -- strokeWidth: Config.dashboard.sizes.mediaProgressThickness -- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap -+ strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness -+ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 -- radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small -- radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small -- startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 -- sweepAngle: Config.dashboard.sizes.mediaProgressSweep -+ radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small -+ radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small -+ startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 -+ sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep - } - - Behavior on strokeColor { -@@ -62,16 +63,16 @@ Item { - ShapePath { - fillColor: "transparent" - strokeColor: Colours.palette.m3primary -- strokeWidth: Config.dashboard.sizes.mediaProgressThickness -- capStyle: Appearance.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap -+ strokeWidth: root.Tokens.sizes.dashboard.mediaProgressThickness -+ capStyle: root.Tokens.rounding.scale === 0 ? ShapePath.SquareCap : ShapePath.RoundCap - - PathAngleArc { - centerX: cover.x + cover.width / 2 - centerY: cover.y + cover.height / 2 -- radiusX: (cover.width + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small -- radiusY: (cover.height + Config.dashboard.sizes.mediaProgressThickness) / 2 + Appearance.spacing.small -- startAngle: -90 - Config.dashboard.sizes.mediaProgressSweep / 2 -- sweepAngle: Config.dashboard.sizes.mediaProgressSweep * root.playerProgress -+ radiusX: (cover.width + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small -+ radiusY: (cover.height + root.Tokens.sizes.dashboard.mediaProgressThickness) / 2 + root.Tokens.spacing.small -+ startAngle: -90 - root.Tokens.sizes.dashboard.mediaProgressSweep / 2 -+ sweepAngle: root.Tokens.sizes.dashboard.mediaProgressSweep * root.playerProgress - } - - Behavior on strokeColor { -@@ -86,7 +87,7 @@ Item { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right -- anchors.margins: Appearance.padding.large + Config.dashboard.sizes.mediaProgressThickness + Appearance.spacing.small -+ anchors.margins: Tokens.padding.large + Tokens.sizes.dashboard.mediaProgressThickness + Tokens.spacing.small - - implicitHeight: width - color: Colours.tPalette.m3surfaceContainerHigh -@@ -106,11 +107,13 @@ Item { - - anchors.fill: parent - -- source: Players.active?.trackArtUrl ?? "" -+ source: Players.getArtUrl(Players.active) - asynchronous: true - fillMode: Image.PreserveAspectCrop -- sourceSize.width: width -- sourceSize.height: height -+ sourceSize: { -+ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; -+ return Qt.size(width * dpr, height * dpr); -+ } - } - } - -@@ -119,15 +122,15 @@ Item { - - anchors.top: cover.bottom - anchors.horizontalCenter: parent.horizontalCenter -- anchors.topMargin: Appearance.spacing.normal -+ anchors.topMargin: Tokens.spacing.normal - - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackTitle ?? qsTr("No media")) || qsTr("Unknown title") - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - -- width: parent.implicitWidth - Appearance.padding.large * 2 -+ width: parent.implicitWidth - Tokens.padding.large * 2 - elide: Text.ElideRight - } - -@@ -136,15 +139,15 @@ Item { - - anchors.top: title.bottom - anchors.horizontalCenter: parent.horizontalCenter -- anchors.topMargin: Appearance.spacing.small -+ anchors.topMargin: Tokens.spacing.small - - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackAlbum ?? qsTr("No media")) || qsTr("Unknown album") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - -- width: parent.implicitWidth - Appearance.padding.large * 2 -+ width: parent.implicitWidth - Tokens.padding.large * 2 - elide: Text.ElideRight - } - -@@ -153,14 +156,14 @@ Item { - - anchors.top: album.bottom - anchors.horizontalCenter: parent.horizontalCenter -- anchors.topMargin: Appearance.spacing.small -+ anchors.topMargin: Tokens.spacing.small - - animate: true - horizontalAlignment: Text.AlignHCenter - text: (Players.active?.trackArtist ?? qsTr("No media")) || qsTr("Unknown artist") - color: Colours.palette.m3secondary - -- width: parent.implicitWidth - Appearance.padding.large * 2 -+ width: parent.implicitWidth - Tokens.padding.large * 2 - elide: Text.ElideRight - } - -@@ -169,35 +172,26 @@ Item { - - anchors.top: artist.bottom - anchors.horizontalCenter: parent.horizontalCenter -- anchors.topMargin: Appearance.spacing.smaller -+ anchors.topMargin: Tokens.spacing.smaller - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - -- Control { -+ PlayerControl { - icon: "skip_previous" - canUse: Players.active?.canGoPrevious ?? false -- -- function onClicked(): void { -- Players.active?.previous(); -- } -+ onClicked: Players.active?.previous() - } - -- Control { -+ PlayerControl { - icon: Players.active?.isPlaying ? "pause" : "play_arrow" - canUse: Players.active?.canTogglePlaying ?? false -- -- function onClicked(): void { -- Players.active?.togglePlaying(); -- } -+ onClicked: Players.active?.togglePlaying() - } - -- Control { -+ PlayerControl { - icon: "skip_next" - canUse: Players.active?.canGoNext ?? false -- -- function onClicked(): void { -- Players.active?.next(); -- } -+ onClicked: Players.active?.next() - } - } - -@@ -208,35 +202,32 @@ Item { - anchors.bottom: parent.bottom - anchors.left: parent.left - anchors.right: parent.right -- anchors.topMargin: Appearance.spacing.small -- anchors.bottomMargin: Appearance.padding.large -- anchors.margins: Appearance.padding.large * 2 -+ anchors.topMargin: Tokens.spacing.small -+ anchors.bottomMargin: Tokens.padding.large -+ anchors.margins: Tokens.padding.large * 2 - - playing: Players.active?.isPlaying ?? false -- speed: Audio.beatTracker.bpm / 300 -+ speed: Audio.beatTracker.bpm / Config.general.mediaGifSpeedAdjustment // qmllint disable unresolved-type - source: Paths.absolutePath(Config.paths.mediaGif) - asynchronous: true - fillMode: AnimatedImage.PreserveAspectFit - } - -- component Control: StyledRect { -+ component PlayerControl: StyledRect { - id: control - - required property string icon - required property bool canUse -- function onClicked(): void { -- } - -- implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Appearance.padding.small -+ signal clicked -+ -+ implicitWidth: Math.max(icon.implicitHeight, icon.implicitHeight) + Tokens.padding.small - implicitHeight: implicitWidth - - StateLayer { - disabled: !control.canUse -- radius: Appearance.rounding.full -- -- function onClicked(): void { -- control.onClicked(); -- } -+ radius: Tokens.rounding.full -+ onClicked: control.clicked() - } - - MaterialIcon { -@@ -248,7 +239,7 @@ Item { - animate: true - text: control.icon - color: control.canUse ? Colours.palette.m3onSurface : Colours.palette.m3outline -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -diff --git a/modules/dashboard/dash/Resources.qml b/modules/dashboard/dash/Resources.qml -index 7f44a9d0..2e0f085b 100644 ---- a/modules/dashboard/dash/Resources.qml -+++ b/modules/dashboard/dash/Resources.qml -@@ -1,8 +1,8 @@ -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.components.misc - import qs.services --import qs.config --import QtQuick - - Row { - id: root -@@ -10,8 +10,8 @@ Row { - anchors.top: parent.top - anchors.bottom: parent.bottom - -- padding: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ padding: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - Ref { - service: SystemUsage -@@ -44,19 +44,19 @@ Row { - - anchors.top: parent.top - anchors.bottom: parent.bottom -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - implicitWidth: icon.implicitWidth - - StyledRect { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: icon.top -- anchors.bottomMargin: Appearance.spacing.small -+ anchors.bottomMargin: Tokens.spacing.small - -- implicitWidth: Config.dashboard.sizes.resourceProgessThickness -+ implicitWidth: Tokens.sizes.dashboard.resourceProgressThickness - - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - StyledRect { - anchors.left: parent.left -@@ -65,7 +65,7 @@ Row { - implicitHeight: res.value * parent.height - - color: res.colour -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - } - } - -@@ -80,7 +80,7 @@ Row { - - Behavior on value { - Anim { -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - } - } -diff --git a/modules/dashboard/dash/Weather.qml b/modules/dashboard/dash/SmallWeather.qml -similarity index 76% -rename from modules/dashboard/dash/Weather.qml -rename to modules/dashboard/dash/SmallWeather.qml -index c90ccf0a..998c7125 100644 ---- a/modules/dashboard/dash/Weather.qml -+++ b/modules/dashboard/dash/SmallWeather.qml -@@ -1,8 +1,7 @@ -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import qs.utils --import QtQuick - - Item { - id: root -@@ -22,7 +21,7 @@ Item { - animate: true - text: Weather.icon - color: Colours.palette.m3secondary -- font.pointSize: Appearance.font.size.extraLarge * 2 -+ font.pointSize: Tokens.font.size.extraLarge * 2 - } - - Column { -@@ -30,9 +29,9 @@ Item { - - anchors.verticalCenter: parent.verticalCenter - anchors.left: icon.right -- anchors.leftMargin: Appearance.spacing.large -+ anchors.leftMargin: Tokens.spacing.large - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - anchors.horizontalCenter: parent.horizontalCenter -@@ -40,7 +39,7 @@ Item { - animate: true - text: Weather.temp - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } - -@@ -51,7 +50,7 @@ Item { - text: Weather.description - - elide: Text.ElideRight -- width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Appearance.padding.large * 2) -+ width: Math.min(implicitWidth, root.parent.width - icon.implicitWidth - info.anchors.leftMargin - Tokens.padding.large * 2) - } - } - } -diff --git a/modules/dashboard/dash/User.qml b/modules/dashboard/dash/User.qml -index b66b1f9a..79787de5 100644 ---- a/modules/dashboard/dash/User.qml -+++ b/modules/dashboard/dash/User.qml -@@ -1,28 +1,26 @@ -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.components.effects --import qs.components.images - import qs.components.filedialog -+import qs.components.images - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick - - Row { - id: root - -- required property PersistentProperties visibilities -- required property PersistentProperties state -+ required property DrawerVisibilities visibilities - required property FileDialog facePicker - -- padding: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ padding: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - StyledClippingRect { - implicitWidth: info.implicitHeight - implicitHeight: info.implicitHeight - -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - - MaterialIcon { -@@ -32,6 +30,7 @@ Row { - fill: 1 - grade: 200 - font.pointSize: Math.floor(info.implicitHeight / 2) || 1 -+ visible: pfp.status !== Image.Ready - } - - CachingImage { -@@ -53,7 +52,7 @@ Row { - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -+ duration: Tokens.anim.durations.expressiveFastSpatial - } - } - } -@@ -61,18 +60,17 @@ Row { - StyledRect { - anchors.centerIn: parent - -- implicitWidth: selectIcon.implicitHeight + Appearance.padding.small * 2 -- implicitHeight: selectIcon.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: selectIcon.implicitHeight + Tokens.padding.small * 2 -+ implicitHeight: selectIcon.implicitHeight + Tokens.padding.small * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.palette.m3primary - scale: parent.containsMouse ? 1 : 0.5 - opacity: parent.containsMouse ? 1 : 0 - - StateLayer { - color: Colours.palette.m3onPrimary -- -- function onClicked(): void { -+ onClicked: { - root.visibilities.launcher = false; - root.facePicker.open(); - } -@@ -86,19 +84,18 @@ Row { - - text: "frame_person" - color: Colours.palette.m3onPrimary -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - } - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -+ duration: Tokens.anim.durations.expressiveFastSpatial - } - } - } -@@ -109,7 +106,7 @@ Row { - id: info - - anchors.verticalCenter: parent.verticalCenter -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Item { - id: line -@@ -121,10 +118,10 @@ Row { - id: icon - - anchors.left: parent.left -- anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 -+ anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 - - source: SysInfo.osLogo -- implicitSize: Math.floor(Appearance.font.size.normal * 1.34) -+ implicitSize: Math.floor(Tokens.font.size.normal * 1.34) - colour: Colours.palette.m3primary - } - -@@ -135,9 +132,9 @@ Row { - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${SysInfo.osPrettyName || SysInfo.osName}` -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - -- width: Config.dashboard.sizes.infoWidth -+ width: Tokens.sizes.dashboard.infoWidth - elide: Text.ElideRight - } - } -@@ -171,12 +168,12 @@ Row { - id: icon - - anchors.left: parent.left -- anchors.leftMargin: (Config.dashboard.sizes.infoIconSize - implicitWidth) / 2 -+ anchors.leftMargin: (Tokens.sizes.dashboard.infoIconSize - implicitWidth) / 2 - - fill: 1 - text: line.icon - color: line.colour -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledText { -@@ -186,9 +183,9 @@ Row { - anchors.left: icon.right - anchors.leftMargin: icon.anchors.leftMargin - text: `: ${line.text}` -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - -- width: Config.dashboard.sizes.infoWidth -+ width: Tokens.sizes.dashboard.infoWidth - elide: Text.ElideRight - } - } -diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml -deleted file mode 100644 -index 7fa2ca17..00000000 ---- a/modules/drawers/Backgrounds.qml -+++ /dev/null -@@ -1,86 +0,0 @@ --import qs.services --import qs.config --import qs.modules.osd as Osd --import qs.modules.notifications as Notifications --import qs.modules.session as Session --import qs.modules.launcher as Launcher --import qs.modules.dashboard as Dashboard --import qs.modules.bar.popouts as BarPopouts --import qs.modules.utilities as Utilities --import qs.modules.sidebar as Sidebar --import QtQuick --import QtQuick.Shapes -- --Shape { -- id: root -- -- required property Panels panels -- required property Item bar -- -- anchors.fill: parent -- anchors.margins: Config.border.thickness -- anchors.leftMargin: bar.implicitWidth -- preferredRendererType: Shape.CurveRenderer -- -- Osd.Background { -- wrapper: root.panels.osd -- -- startX: root.width - root.panels.session.width - root.panels.sidebar.width -- startY: (root.height - wrapper.height) / 2 - rounding -- } -- -- Notifications.Background { -- wrapper: root.panels.notifications -- sidebar: sidebar -- -- startX: root.width -- startY: 0 -- } -- -- Session.Background { -- wrapper: root.panels.session -- -- startX: root.width - root.panels.sidebar.width -- startY: (root.height - wrapper.height) / 2 - rounding -- } -- -- Launcher.Background { -- wrapper: root.panels.launcher -- -- startX: (root.width - wrapper.width) / 2 - rounding -- startY: root.height -- } -- -- Dashboard.Background { -- wrapper: root.panels.dashboard -- -- startX: (root.width - wrapper.width) / 2 - rounding -- startY: 0 -- } -- -- BarPopouts.Background { -- wrapper: root.panels.popouts -- invertBottomRounding: wrapper.y + wrapper.height + 1 >= root.height -- -- startX: wrapper.x -- startY: wrapper.y - rounding * sideRounding -- } -- -- Utilities.Background { -- wrapper: root.panels.utilities -- sidebar: sidebar -- -- startX: root.width -- startY: root.height -- } -- -- Sidebar.Background { -- id: sidebar -- -- wrapper: root.panels.sidebar -- panels: root.panels -- -- startX: root.width -- startY: root.panels.notifications.height -- } --} -diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml -deleted file mode 100644 -index 6fdd73bd..00000000 ---- a/modules/drawers/Border.qml -+++ /dev/null -@@ -1,44 +0,0 @@ --pragma ComponentBehavior: Bound -- --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Effects -- --Item { -- id: root -- -- required property Item bar -- -- anchors.fill: parent -- -- StyledRect { -- anchors.fill: parent -- color: Colours.palette.m3surface -- -- layer.enabled: true -- layer.effect: MultiEffect { -- maskSource: mask -- maskEnabled: true -- maskInverted: true -- maskThresholdMin: 0.5 -- maskSpreadAtMin: 1 -- } -- } -- -- Item { -- id: mask -- -- anchors.fill: parent -- layer.enabled: true -- visible: false -- -- Rectangle { -- anchors.fill: parent -- anchors.margins: Config.border.thickness -- anchors.leftMargin: root.bar.implicitWidth -- radius: Config.border.rounding -- } -- } --} -diff --git a/modules/drawers/ContentWindow.qml b/modules/drawers/ContentWindow.qml -new file mode 100644 -index 00000000..677c84a0 ---- /dev/null -+++ b/modules/drawers/ContentWindow.qml -@@ -0,0 +1,318 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import QtQuick.Controls -+import QtQuick.Effects -+import Quickshell -+import Quickshell.Hyprland -+import Quickshell.Wayland -+import Caelestia.Blobs -+import Caelestia.Config -+import qs.components -+import qs.components.containers -+import qs.services -+import qs.modules.bar -+ -+StyledWindow { -+ id: root -+ -+ readonly property alias bar: bar -+ readonly property alias interactionWrapper: interactions -+ -+ readonly property HyprlandMonitor monitor: Hypr.monitorFor(screen) -+ readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject.specialWorkspace?.name.length ?? 0) > 0 -+ readonly property bool hasFullscreen: { -+ if (hasSpecialWorkspace) { -+ const specialName = monitor?.lastIpcObject.specialWorkspace?.name; -+ if (!specialName) -+ return false; -+ const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); -+ return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; -+ } -+ return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; -+ } -+ -+ property real fsTransitionProg: hasFullscreen ? 1 : 0 -+ readonly property real sdfBorderOffset: 2 * fsTransitionProg // SDFs joins are not exact, so offset by 2px to ensure nothing shows -+ readonly property real borderThickness: contentItem.Config.border.thickness * (1 - fsTransitionProg) -+ readonly property real borderRounding: contentItem.Config.border.rounding * (1 - fsTransitionProg) -+ readonly property real shadowOpacity: 0.7 * (1 - fsTransitionProg) -+ readonly property real borderLayoutThickness: hasFullscreen ? 0 : contentItem.Config.border.thickness -+ -+ readonly property int dragMaskPadding: { -+ if (focusGrab.active || panels.popouts.isDetached) -+ return 0; -+ -+ if (monitor?.lastIpcObject.specialWorkspace?.name || monitor?.activeWorkspace.lastIpcObject.windows > 0) -+ return 0; -+ -+ const thresholds = []; -+ for (const panel of ["dashboard", "launcher", "session", "sidebar"]) -+ if (contentItem.Config[panel].enabled) -+ thresholds.push(contentItem.Config[panel].dragThreshold); -+ return Math.max(...thresholds); -+ } -+ -+ onHasFullscreenChanged: { -+ visibilities.launcher = false; -+ visibilities.session = false; -+ visibilities.dashboard = false; -+ panels.popouts.close(); -+ } -+ -+ name: "drawers" -+ WlrLayershell.exclusionMode: ExclusionMode.Ignore -+ WlrLayershell.layer: fsTransitionProg > 0 && contentItem.Config.general.showOverFullscreen ? WlrLayer.Overlay : WlrLayer.Top -+ WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None -+ -+ mask: hasFullscreen ? emptyRegion : regions -+ -+ anchors.top: true -+ anchors.bottom: true -+ anchors.left: true -+ anchors.right: true -+ -+ Behavior on fsTransitionProg { -+ Anim {} -+ } -+ -+ Region { -+ id: emptyRegion -+ -+ x: panels.notifications.x + bar.implicitWidth -+ y: panels.notifications.y + root.borderThickness -+ width: panels.notifications.width -+ height: panels.notifications.height -+ -+ Region { -+ x: root.width - width -+ y: panels.osdWrapper.y + root.borderThickness -+ width: panels.osdWrapper.width * (1 - panels.osd.offsetScale) + root.borderThickness -+ height: panels.osd.height -+ } -+ } -+ -+ Regions { -+ id: regions -+ -+ bar: bar -+ panels: panels -+ win: root -+ } -+ -+ HyprlandFocusGrab { -+ id: focusGrab -+ -+ active: (visibilities.launcher && root.contentItem.Config.launcher.enabled) || (visibilities.session && root.contentItem.Config.session.enabled) || (visibilities.sidebar && root.contentItem.Config.sidebar.enabled) || (!root.contentItem.Config.dashboard.showOnHover && visibilities.dashboard && root.contentItem.Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && (panels.popouts.current as StackView)?.depth > 1) -+ windows: [root] -+ onCleared: { -+ visibilities.launcher = false; -+ visibilities.session = false; -+ visibilities.sidebar = false; -+ visibilities.dashboard = false; -+ panels.popouts.hasCurrent = false; -+ bar.closeTray(); -+ } -+ } -+ -+ StyledRect { -+ anchors.fill: parent -+ opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 -+ color: Colours.palette.m3scrim -+ -+ Behavior on opacity { -+ Anim {} -+ } -+ } -+ -+ Item { -+ anchors.fill: parent -+ opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 -+ layer.enabled: true -+ layer.effect: MultiEffect { -+ shadowEnabled: true -+ blurMax: 15 -+ shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, root.shadowOpacity)) -+ } -+ -+ BlobGroup { -+ id: blobGroup -+ -+ color: Colours.palette.m3surface -+ smoothing: root.contentItem.Config.border.smoothing -+ -+ Behavior on color { -+ CAnim {} -+ } -+ } -+ -+ BlobInvertedRect { -+ anchors.fill: parent -+ anchors.margins: -50 // Make border thicker to smooth out bulge from closed drawers -+ group: blobGroup -+ radius: root.borderRounding -+ borderLeft: bar.implicitWidth - anchors.margins - root.sdfBorderOffset -+ borderRight: root.borderThickness - anchors.margins - root.sdfBorderOffset -+ borderTop: root.borderThickness - anchors.margins - root.sdfBorderOffset -+ borderBottom: root.borderThickness - anchors.margins - root.sdfBorderOffset -+ } -+ -+ PanelBg { -+ id: dashBg -+ -+ panel: panels.dashboard -+ deformAmount: 0.1 -+ } -+ -+ PanelBg { -+ id: launcherBg -+ -+ panel: panels.launcher -+ deformAmount: 0.1 -+ } -+ -+ PanelBg { -+ id: sessionBg -+ -+ panel: panels.sessionWrapper -+ deformAmount: 0.2 -+ x: panels.sessionWrapper.x + panels.session.x + bar.implicitWidth -+ implicitWidth: panels.session.width -+ } -+ -+ PanelBg { -+ id: sidebarBg -+ -+ panel: panels.sidebar -+ deformAmount: 0.03 -+ implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 -+ exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] -+ bottomLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius -+ } -+ -+ PanelBg { -+ id: osdBg -+ -+ panel: panels.osdWrapper -+ deformAmount: 0.25 -+ x: panels.osdWrapper.x + panels.osd.x + bar.implicitWidth -+ implicitWidth: panels.osd.width -+ } -+ -+ PanelBg { -+ id: notifsBg -+ -+ panel: panels.notifications -+ } -+ -+ PanelBg { -+ id: utilsBg -+ -+ panel: panels.utilities -+ deformAmount: panels.sidebar.visible ? 0.1 : 0.15 -+ exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] -+ topLeftRadius: Math.max(0, Math.min(1, panels.sidebar.offsetScale / 0.3)) * radius -+ } -+ -+ PanelBg { -+ id: popoutBg -+ -+ // Extra width to prevent vertical movement deformation partially detaching panel from bar -+ property real extraWidth: panels.popouts.isDetached ? 0 : 0.2 -+ -+ panel: panels.popoutsWrapper -+ deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1 -+ x: panels.popoutsWrapper.x + panels.popouts.x + bar.implicitWidth - panels.popouts.width * extraWidth -+ implicitWidth: panels.popouts.width * (1 + extraWidth) -+ -+ Behavior on extraWidth { -+ Anim { -+ type: Anim.DefaultSpatial -+ } -+ } -+ } -+ } -+ -+ DrawerVisibilities { -+ id: visibilities -+ -+ Component.onCompleted: Visibilities.load(root.screen, this) -+ } -+ -+ Interactions { -+ id: interactions -+ -+ screen: root.screen -+ popouts: panels.popouts -+ visibilities: visibilities -+ panels: panels -+ bar: bar -+ borderThickness: root.borderLayoutThickness -+ fullscreen: root.hasFullscreen -+ -+ Panels { -+ id: panels -+ -+ screen: root.screen -+ visibilities: visibilities -+ bar: bar -+ borderThickness: root.borderThickness -+ -+ utilities.horizontalStretch: (sidebarBg.rawDeformMatrix.m11 - 1) / 2 + 1 -+ utilities.deformMatrix: utilsBg.rawDeformMatrix -+ -+ dashboard.transform: Matrix4x4 { -+ matrix: dashBg.deformMatrix -+ } -+ launcher.transform: Matrix4x4 { -+ matrix: launcherBg.deformMatrix -+ } -+ session.transform: Matrix4x4 { -+ matrix: sessionBg.deformMatrix -+ } -+ sidebar.transform: Matrix4x4 { -+ matrix: sidebarBg.deformMatrix -+ } -+ osd.transform: Matrix4x4 { -+ matrix: osdBg.deformMatrix -+ } -+ notifications.transform: Matrix4x4 { -+ matrix: notifsBg.deformMatrix -+ } -+ utilities.transform: Matrix4x4 { -+ matrix: utilsBg.deformMatrix -+ } -+ popouts.transform: Matrix4x4 { -+ matrix: popoutBg.deformMatrix -+ } -+ } -+ -+ BarWrapper { -+ id: bar -+ -+ anchors.top: parent.top -+ anchors.bottom: parent.bottom -+ -+ screen: root.screen -+ visibilities: visibilities -+ popouts: panels.popouts -+ -+ fullscreen: root.hasFullscreen -+ -+ Component.onCompleted: Visibilities.bars.set(root.screen, this) -+ } -+ } -+ -+ component PanelBg: BlobRect { -+ required property Item panel -+ property real deformAmount: 0.15 -+ -+ group: blobGroup -+ x: panel.x + bar.implicitWidth -+ y: panel.y + root.borderThickness -+ implicitWidth: panel.width -+ implicitHeight: panel.height -+ radius: Tokens.rounding.large -+ deformScale: (deformAmount * Config.appearance.deformScale) / 10000 -+ } -+} -diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml -index 00f9596a..c642692c 100644 ---- a/modules/drawers/Drawers.qml -+++ b/modules/drawers/Drawers.qml -@@ -1,193 +1,24 @@ --pragma ComponentBehavior: Bound -- --import qs.components --import qs.components.containers --import qs.services --import qs.config --import qs.modules.bar - import Quickshell --import Quickshell.Wayland --import Quickshell.Hyprland --import QtQuick --import QtQuick.Effects -+import qs.services - - Variants { -- model: Quickshell.screens -+ model: Screens.screens - - Scope { - id: scope - - required property ShellScreen modelData -- readonly property bool barDisabled: { -- const regexChecker = /^\^.*\$$/; -- for (const filter of Config.bar.excludedScreens) { -- // If filter is a regex -- if (regexChecker.test(filter)) { -- if ((new RegExp(filter)).test(modelData.name)) -- return true; -- } else { -- if (filter === modelData.name) -- return true; -- } -- } -- return false; -- } - - Exclusions { - screen: scope.modelData -- bar: bar -+ bar: drawerWindow.bar - } - -- StyledWindow { -- id: win -- -- readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false -- readonly property int dragMaskPadding: { -- if (focusGrab.active || panels.popouts.isDetached) -- return 0; -- -- const mon = Hypr.monitorFor(screen); -- if (mon?.lastIpcObject?.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject?.windows > 0) -- return 0; -- -- const thresholds = []; -- for (const panel of ["dashboard", "launcher", "session", "sidebar"]) -- if (Config[panel].enabled) -- thresholds.push(Config[panel].dragThreshold); -- return Math.max(...thresholds); -- } -- -- onHasFullscreenChanged: { -- visibilities.launcher = false; -- visibilities.session = false; -- visibilities.dashboard = false; -- } -+ ContentWindow { -+ id: drawerWindow - - screen: scope.modelData - name: "drawers" -- WlrLayershell.exclusionMode: ExclusionMode.Ignore -- WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None -- -- mask: Region { -- x: bar.implicitWidth + win.dragMaskPadding -- y: Config.border.thickness + win.dragMaskPadding -- width: win.width - bar.implicitWidth - Config.border.thickness - win.dragMaskPadding * 2 -- height: win.height - Config.border.thickness * 2 - win.dragMaskPadding * 2 -- intersection: Intersection.Xor -- -- regions: regions.instances -- } -- -- anchors.top: true -- anchors.bottom: true -- anchors.left: true -- anchors.right: true -- -- Variants { -- id: regions -- -- model: panels.children -- -- Region { -- required property Item modelData -- -- x: modelData.x + bar.implicitWidth -- y: modelData.y + Config.border.thickness -- width: modelData.width -- height: modelData.height -- intersection: Intersection.Subtract -- } -- } -- -- HyprlandFocusGrab { -- id: focusGrab -- -- active: (visibilities.launcher && Config.launcher.enabled) || (visibilities.session && Config.session.enabled) || (visibilities.sidebar && Config.sidebar.enabled) || (!Config.dashboard.showOnHover && visibilities.dashboard && Config.dashboard.enabled) || (panels.popouts.currentName.startsWith("traymenu") && panels.popouts.current?.depth > 1) -- windows: [win] -- onCleared: { -- visibilities.launcher = false; -- visibilities.session = false; -- visibilities.sidebar = false; -- visibilities.dashboard = false; -- panels.popouts.hasCurrent = false; -- bar.closeTray(); -- } -- } -- -- StyledRect { -- anchors.fill: parent -- opacity: visibilities.session && Config.session.enabled ? 0.5 : 0 -- color: Colours.palette.m3scrim -- -- Behavior on opacity { -- Anim {} -- } -- } -- -- Item { -- anchors.fill: parent -- opacity: Colours.transparency.enabled ? Colours.transparency.base : 1 -- layer.enabled: true -- layer.effect: MultiEffect { -- shadowEnabled: true -- blurMax: 15 -- shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) -- } -- -- Border { -- bar: bar -- } -- -- Backgrounds { -- panels: panels -- bar: bar -- } -- } -- -- PersistentProperties { -- id: visibilities -- -- property bool bar -- property bool osd -- property bool session -- property bool launcher -- property bool dashboard -- property bool utilities -- property bool sidebar -- -- Component.onCompleted: Visibilities.load(scope.modelData, this) -- } -- -- Interactions { -- screen: scope.modelData -- popouts: panels.popouts -- visibilities: visibilities -- panels: panels -- bar: bar -- -- Panels { -- id: panels -- -- screen: scope.modelData -- visibilities: visibilities -- bar: bar -- } -- -- BarWrapper { -- id: bar -- -- anchors.top: parent.top -- anchors.bottom: parent.bottom -- -- screen: scope.modelData -- visibilities: visibilities -- popouts: panels.popouts -- -- disabled: scope.barDisabled -- -- Component.onCompleted: Visibilities.bars.set(scope.modelData, this) -- } -- } - } - } - } -diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml -index e4015c89..fe72730c 100644 ---- a/modules/drawers/Exclusions.qml -+++ b/modules/drawers/Exclusions.qml -@@ -1,15 +1,16 @@ - pragma ComponentBehavior: Bound - --import qs.components.containers --import qs.config --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia.Config -+import qs.components.containers -+import qs.modules.bar as Bar - - Scope { - id: root - - required property ShellScreen screen -- required property Item bar -+ required property Bar.BarWrapper bar - - ExclusionZone { - anchors.left: true -@@ -31,7 +32,7 @@ Scope { - component ExclusionZone: StyledWindow { - screen: root.screen - name: "border-exclusion" -- exclusiveZone: Config.border.thickness -+ exclusiveZone: contentItem.Config.border.thickness - mask: Region {} - implicitWidth: 1 - implicitHeight: 1 -diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml -index 9579b15a..b89cb007 100644 ---- a/modules/drawers/Interactions.qml -+++ b/modules/drawers/Interactions.qml -@@ -1,17 +1,22 @@ -+import QtQuick -+import QtQuick.Controls -+import Quickshell -+import Caelestia.Config -+import qs.components - import qs.components.controls --import qs.config -+import qs.modules.bar as Bar - import qs.modules.bar.popouts as BarPopouts --import Quickshell --import QtQuick - - CustomMouseArea { - id: root - - required property ShellScreen screen - required property BarPopouts.Wrapper popouts -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - required property Panels panels -- required property Item bar -+ required property Bar.BarWrapper bar -+ required property real borderThickness -+ required property bool fullscreen - - property point dragStart - property bool dashboardShortcutActive -@@ -19,7 +24,7 @@ CustomMouseArea { - property bool utilitiesShortcutActive - - function withinPanelHeight(panel: Item, x: real, y: real): bool { -- const panelY = Config.border.thickness + panel.y; -+ const panelY = root.borderThickness + panel.y; - return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; - } - -@@ -33,24 +38,29 @@ CustomMouseArea { - } - - function inRightPanel(panel: Item, x: real, y: real): bool { -- return x > bar.implicitWidth + panel.x && withinPanelHeight(panel, x, y); -+ return x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panel.x) && withinPanelHeight(panel, x, y); - } - - function inTopPanel(panel: Item, x: real, y: real): bool { -- return y < Config.border.thickness + panel.y + panel.height && withinPanelWidth(panel, x, y); -+ const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property -+ return y < Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) && withinPanelWidth(panel, x, y); - } - -- function inBottomPanel(panel: Item, x: real, y: real): bool { -- return y > root.height - Config.border.thickness - panel.height - Config.border.rounding && withinPanelWidth(panel, x, y); -+ function inBottomPanel(panel: Item, x: real, y: real, isCorner = false): bool { -+ const panelHeight = panel.height * (1 - (panel.offsetScale ?? 0)); // qmllint disable missing-property -+ return y > height - Math.max(Config.border.minThickness, Config.border.thickness + panelHeight) - (isCorner ? Config.border.rounding : 0) && withinPanelWidth(panel, x, y); - } - - function onWheel(event: WheelEvent): void { -+ if (fullscreen) -+ return; - if (event.x < bar.implicitWidth) { - bar.handleWheel(event.y, event.angleDelta); - } - } - - anchors.fill: parent -+ acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons - hoverEnabled: true - - onPressed: event => dragStart = Qt.point(event.x, event.y) -@@ -68,7 +78,7 @@ CustomMouseArea { - if (!utilitiesShortcutActive) - visibilities.utilities = false; - -- if (!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) { -+ if (!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) { - popouts.hasCurrent = false; - bar.closeTray(); - } -@@ -87,21 +97,26 @@ CustomMouseArea { - const dragX = x - dragStart.x; - const dragY = y - dragStart.y; - -+ if (fullscreen) { -+ root.panels.osd.hovered = inRightPanel(panels.osdWrapper, x, y); -+ return; -+ } -+ - // Show bar in non-exclusive mode on hover -- if (!visibilities.bar && Config.bar.showOnHover && x < bar.implicitWidth) -+ if (!visibilities.bar && Config.bar.showOnHover && x < bar.clampedWidth) - bar.isHovered = true; - - // Show/hide bar on drag -- if (pressed && dragStart.x < bar.implicitWidth) { -+ if (pressed && dragStart.x < bar.clampedWidth) { - if (dragX > Config.bar.dragThreshold) - visibilities.bar = true; - else if (dragX < -Config.bar.dragThreshold) - visibilities.bar = false; - } - -- if (panels.sidebar.width === 0) { -+ if (panels.sidebar.offsetScale === 1) { - // Show osd on hover -- const showOsd = inRightPanel(panels.osd, x, y); -+ const showOsd = inRightPanel(panels.osdWrapper, x, y); - - // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { -@@ -113,26 +128,26 @@ CustomMouseArea { - root.panels.osd.hovered = true; - } - -- const showSidebar = pressed && dragStart.x > bar.implicitWidth + panels.sidebar.x; -+ const showSidebar = pressed && dragStart.x > Math.min(width - Config.border.minThickness, bar.implicitWidth + panels.sidebar.x); - - // Show/hide session on drag -- if (pressed && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { -+ if (pressed && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { - if (dragX < -Config.session.dragThreshold) - visibilities.session = true; - else if (dragX > Config.session.dragThreshold) - visibilities.session = false; - - // Show sidebar on drag if in session area and session is nearly fully visible -- if (showSidebar && panels.session.width >= panels.session.nonAnimWidth && dragX < -Config.sidebar.dragThreshold) -+ if (showSidebar && panels.session.offsetScale <= 0 && dragX < -Config.sidebar.dragThreshold) - visibilities.sidebar = true; - } else if (showSidebar && dragX < -Config.sidebar.dragThreshold) { - // Show sidebar on drag if not in session area - visibilities.sidebar = true; - } - } else { -- const outOfSidebar = x < width - panels.sidebar.width; -+ const outOfSidebar = x < width - panels.sidebar.width * (1 - panels.sidebar.offsetScale); - // Show osd on hover -- const showOsd = outOfSidebar && inRightPanel(panels.osd, x, y); -+ const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y); - - // Always update visibility based on hover if not in shortcut mode - if (!osdShortcutActive) { -@@ -145,7 +160,7 @@ CustomMouseArea { - } - - // Show/hide session on drag -- if (pressed && outOfSidebar && inRightPanel(panels.session, dragStart.x, dragStart.y) && withinPanelHeight(panels.session, x, y)) { -+ if (pressed && outOfSidebar && inRightPanel(panels.sessionWrapper, dragStart.x, dragStart.y) && withinPanelHeight(panels.sessionWrapper, x, y)) { - if (dragX < -Config.session.dragThreshold) - visibilities.session = true; - else if (dragX > Config.session.dragThreshold) -@@ -188,7 +203,7 @@ CustomMouseArea { - } - - // Show utilities on hover -- const showUtilities = inBottomPanel(panels.utilities, x, y); -+ const showUtilities = inBottomPanel(panels.utilities, x, y, true); - - // Always update visibility based on hover if not in shortcut mode - if (!utilitiesShortcutActive) { -@@ -201,7 +216,7 @@ CustomMouseArea { - // Show popouts on hover - if (x < bar.implicitWidth) { - bar.checkPopout(y); -- } else if ((!popouts.currentName.startsWith("traymenu") || (popouts.current?.depth ?? 0) <= 1) && !inLeftPanel(panels.popouts, x, y)) { -+ } else if ((!popouts.currentName.startsWith("traymenu") || ((popouts.current as StackView)?.depth ?? 0) <= 1) && !inLeftPanel(panels.popoutsWrapper, x, y)) { - popouts.hasCurrent = false; - bar.closeTray(); - } -@@ -209,8 +224,6 @@ CustomMouseArea { - - // Monitor individual visibility changes - Connections { -- target: root.visibilities -- - function onLauncherChanged() { - // If launcher is hidden, clear shortcut flags for dashboard and OSD - if (!root.visibilities.launcher) { -@@ -220,7 +233,7 @@ CustomMouseArea { - - // Also hide dashboard and OSD if they're not being hovered - const inDashboardArea = root.inTopPanel(root.panels.dashboard, root.mouseX, root.mouseY); -- const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); -+ const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); - - if (!inDashboardArea) { - root.visibilities.dashboard = false; -@@ -248,7 +261,7 @@ CustomMouseArea { - function onOsdChanged() { - if (root.visibilities.osd) { - // OSD became visible, immediately check if this should be shortcut mode -- const inOsdArea = root.inRightPanel(root.panels.osd, root.mouseX, root.mouseY); -+ const inOsdArea = root.inRightPanel(root.panels.osdWrapper, root.mouseX, root.mouseY); - if (!inOsdArea) { - root.osdShortcutActive = true; - } -@@ -270,5 +283,7 @@ CustomMouseArea { - root.utilitiesShortcutActive = false; - } - } -+ -+ target: root.visibilities - } - } -diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml -index 7705732a..4ca420e3 100644 ---- a/modules/drawers/Panels.qml -+++ b/modules/drawers/Panels.qml -@@ -1,69 +1,106 @@ --import qs.config --import qs.modules.osd as Osd -+import QtQuick -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.modules.bar as Bar -+import qs.modules.dashboard as Dashboard -+import qs.modules.launcher as Launcher - import qs.modules.notifications as Notifications -+import qs.modules.osd as Osd - import qs.modules.session as Session --import qs.modules.launcher as Launcher --import qs.modules.dashboard as Dashboard --import qs.modules.bar.popouts as BarPopouts -+import qs.modules.sidebar as Sidebar - import qs.modules.utilities as Utilities -+import qs.modules.bar.popouts as BarPopouts - import qs.modules.utilities.toasts as Toasts --import qs.modules.sidebar as Sidebar --import Quickshell --import QtQuick - - Item { - id: root - - required property ShellScreen screen -- required property PersistentProperties visibilities -- required property Item bar -+ required property DrawerVisibilities visibilities -+ required property Bar.BarWrapper bar -+ required property real borderThickness - - readonly property alias osd: osd -+ readonly property alias osdWrapper: osdWrapper - readonly property alias notifications: notifications - readonly property alias session: session -+ readonly property alias sessionWrapper: sessionWrapper - readonly property alias launcher: launcher - readonly property alias dashboard: dashboard -- readonly property alias popouts: popouts -+ readonly property alias popouts: popoutsWrapper.content -+ readonly property alias popoutsWrapper: popoutsWrapper - readonly property alias utilities: utilities - readonly property alias toasts: toasts - readonly property alias sidebar: sidebar - - anchors.fill: parent -- anchors.margins: Config.border.thickness -+ anchors.margins: borderThickness - anchors.leftMargin: bar.implicitWidth - -- Osd.Wrapper { -- id: osd -+ Behavior on anchors.margins { -+ Anim {} -+ } - -- clip: session.width > 0 || sidebar.width > 0 -- screen: root.screen -- visibilities: root.visibilities -+ Behavior on anchors.leftMargin { -+ Anim {} -+ } -+ -+ Item { -+ id: osdWrapper - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right -- anchors.rightMargin: session.width + sidebar.width -+ anchors.rightMargin: sessionWrapper.anchors.rightMargin + session.width * (1 - session.offsetScale) -+ clip: sidebar.visible || session.visible -+ -+ implicitWidth: osd.implicitWidth * (1 - osd.offsetScale) -+ implicitHeight: osd.implicitHeight -+ -+ Osd.Wrapper { -+ id: osd -+ -+ screen: root.screen -+ visibilities: root.visibilities -+ sidebarOrSessionVisible: sidebar.visible || session.visible -+ -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.right: parent.right -+ } - } - - Notifications.Wrapper { - id: notifications - - visibilities: root.visibilities -- panels: root -+ sidebarPanel: sidebar -+ osdPanel: osdWrapper -+ sessionPanel: sessionWrapper - - anchors.top: parent.top - anchors.right: parent.right - } - -- Session.Wrapper { -- id: session -- -- clip: sidebar.width > 0 -- visibilities: root.visibilities -- panels: root -+ Item { -+ id: sessionWrapper - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right -- anchors.rightMargin: sidebar.width -+ anchors.rightMargin: sidebar.width * (1 - sidebar.offsetScale) -+ clip: sidebar.visible -+ -+ implicitWidth: session.implicitWidth * (1 - session.offsetScale) -+ implicitHeight: session.implicitHeight -+ -+ Session.Wrapper { -+ id: session -+ -+ visibilities: root.visibilities -+ sidebarVisible: sidebar.visible -+ -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.right: parent.right -+ } - } - - Launcher.Wrapper { -@@ -86,22 +123,11 @@ Item { - anchors.top: parent.top - } - -- BarPopouts.Wrapper { -- id: popouts -+ BarPopouts.ClipWrapper { -+ id: popoutsWrapper - - screen: root.screen -- -- x: isDetached ? (root.width - nonAnimWidth) / 2 : 0 -- y: { -- if (isDetached) -- return (root.height - nonAnimHeight) / 2; -- -- const off = currentCenter - Config.border.thickness - nonAnimHeight / 2; -- const diff = root.height - Math.floor(off + nonAnimHeight); -- if (diff < 0) -- return off + diff; -- return Math.max(off, 0); -- } -+ borderThickness: root.borderThickness - } - - Utilities.Wrapper { -@@ -109,7 +135,7 @@ Item { - - visibilities: root.visibilities - sidebar: sidebar -- popouts: popouts -+ popouts: popoutsWrapper.content - - anchors.bottom: parent.bottom - anchors.right: parent.right -@@ -120,14 +146,13 @@ Item { - - anchors.bottom: sidebar.visible ? parent.bottom : utilities.top - anchors.right: sidebar.left -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - } - - Sidebar.Wrapper { - id: sidebar - - visibilities: root.visibilities -- panels: root - - anchors.top: notifications.bottom - anchors.bottom: utilities.top -diff --git a/modules/drawers/Regions.qml b/modules/drawers/Regions.qml -new file mode 100644 -index 00000000..47ad0680 ---- /dev/null -+++ b/modules/drawers/Regions.qml -@@ -0,0 +1,84 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import Quickshell -+import Caelestia.Config -+import qs.modules.bar as Bar -+ -+Region { -+ id: root -+ -+ required property Bar.BarWrapper bar -+ required property Panels panels -+ required property var win -+ -+ readonly property real borderThickness: win.contentItem.Config.border.thickness -+ readonly property real clampedThickness: win.contentItem.Config.border.clampedThickness -+ -+ x: bar.clampedWidth + win.dragMaskPadding -+ y: clampedThickness + win.dragMaskPadding -+ width: win.width - bar.clampedWidth - clampedThickness - win.dragMaskPadding * 2 -+ height: win.height - clampedThickness * 2 - win.dragMaskPadding * 2 -+ intersection: Intersection.Xor -+ -+ R { -+ panel: root.panels.dashboard -+ y: 0 -+ height: panel.height * (1 - root.panels.dashboard.offsetScale) + root.borderThickness -+ } -+ -+ R { -+ panel: root.panels.launcher -+ y: root.win.height - height -+ height: panel.height * (1 - root.panels.launcher.offsetScale) + root.borderThickness -+ } -+ -+ R { -+ id: sessionRegion -+ -+ panel: root.panels.sessionWrapper -+ x: root.win.width - width -+ width: panel.width * (1 - root.panels.session.offsetScale) + root.borderThickness + sidebarRegion.width -+ } -+ -+ R { -+ id: sidebarRegion -+ -+ panel: root.panels.sidebar -+ x: root.win.width - width -+ width: panel.width * (1 - root.panels.sidebar.offsetScale) + root.borderThickness -+ } -+ -+ R { -+ panel: root.panels.osdWrapper -+ x: root.win.width - width -+ width: panel.width * (1 - root.panels.osd.offsetScale) + root.borderThickness + sessionRegion.width -+ } -+ -+ R { -+ panel: root.panels.notifications -+ y: 0 -+ height: panel.height + root.borderThickness -+ } -+ -+ R { -+ panel: root.panels.utilities -+ y: root.win.height - height -+ height: panel.height * (1 - root.panels.utilities.offsetScale) + root.borderThickness -+ } -+ -+ R { -+ panel: root.panels.popoutsWrapper -+ width: panel.width * (1 - root.panels.popoutsWrapper.offsetScale) -+ } -+ -+ component R: Region { -+ required property Item panel -+ -+ x: panel.x + root.bar.implicitWidth -+ y: panel.y + root.borderThickness -+ width: panel.width -+ height: panel.height -+ intersection: Intersection.Subtract -+ } -+} -diff --git a/modules/launcher/AppList.qml b/modules/launcher/AppList.qml -index 7f7b843a..a2109c04 100644 ---- a/modules/launcher/AppList.qml -+++ b/modules/launcher/AppList.qml -@@ -1,20 +1,20 @@ - pragma ComponentBehavior: Bound - --import "items" --import "services" -+import QtQuick -+import Quickshell -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config --import Quickshell --import QtQuick -+import qs.modules.launcher.items -+import qs.modules.launcher.services - - StyledListView { - id: root - - required property StyledTextField search -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - - model: ScriptModel { - id: model -@@ -22,9 +22,9 @@ StyledListView { - onValuesChanged: root.currentIndex = 0 - } - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - orientation: Qt.Vertical -- implicitHeight: (Config.launcher.sizes.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing -+ implicitHeight: (Tokens.sizes.launcher.itemHeight + spacing) * Math.min(Config.launcher.maxShown, count) - spacing - - preferredHighlightBegin: 0 - preferredHighlightEnd: height -@@ -32,7 +32,7 @@ StyledListView { - - highlightFollowsCurrentItem: false - highlight: StyledRect { -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.palette.m3onSurface - opacity: 0.08 - -@@ -42,15 +42,14 @@ StyledListView { - - Behavior on y { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } - - state: { - const text = search.text; -- const prefix = Config.launcher.actionPrefix; -+ const prefix = GlobalConfig.launcher.actionPrefix; - if (text.startsWith(prefix)) { - for (const action of ["calc", "scheme", "variant"]) - if (text.startsWith(`${prefix}${action} `)) -@@ -118,16 +117,16 @@ StyledListView { - property: "opacity" - from: 1 - to: 0 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ duration: Tokens.anim.durations.small -+ easing: Tokens.anim.standardAccel - } - Anim { - target: root - property: "scale" - from: 1 - to: 0.9 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ duration: Tokens.anim.durations.small -+ easing: Tokens.anim.standardAccel - } - } - PropertyAction { -@@ -140,16 +139,16 @@ StyledListView { - property: "opacity" - from: 0 - to: 1 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ duration: Tokens.anim.durations.small -+ easing: Tokens.anim.standardDecel - } - Anim { - target: root - property: "scale" - from: 0.9 - to: 1 -- duration: Appearance.anim.durations.small -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ duration: Tokens.anim.durations.small -+ easing: Tokens.anim.standardDecel - } - } - PropertyAction { -@@ -197,7 +196,7 @@ StyledListView { - addDisplaced: Transition { - Anim { - property: "y" -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - Anim { - properties: "opacity,scale" -diff --git a/modules/launcher/Background.qml b/modules/launcher/Background.qml -deleted file mode 100644 -index 709c7d03..00000000 ---- a/modules/launcher/Background.qml -+++ /dev/null -@@ -1,60 +0,0 @@ --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Shapes -- --ShapePath { -- id: root -- -- required property Wrapper wrapper -- readonly property real rounding: Config.border.rounding -- readonly property bool flatten: wrapper.height < rounding * 2 -- readonly property real roundingY: flatten ? wrapper.height / 2 : rounding -- -- strokeWidth: -1 -- fillColor: Colours.palette.m3surface -- -- PathArc { -- relativeX: root.rounding -- relativeY: -root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- direction: PathArc.Counterclockwise -- } -- PathLine { -- relativeX: 0 -- relativeY: -(root.wrapper.height - root.roundingY * 2) -- } -- PathArc { -- relativeX: root.rounding -- relativeY: -root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- } -- PathLine { -- relativeX: root.wrapper.width - root.rounding * 2 -- relativeY: 0 -- } -- PathArc { -- relativeX: root.rounding -- relativeY: root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- } -- PathLine { -- relativeX: 0 -- relativeY: root.wrapper.height - root.roundingY * 2 -- } -- PathArc { -- relativeX: root.rounding -- relativeY: root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- direction: PathArc.Counterclockwise -- } -- -- Behavior on fillColor { -- CAnim {} -- } --} -diff --git a/modules/launcher/Content.qml b/modules/launcher/Content.qml -index c0859769..69be0c3a 100644 ---- a/modules/launcher/Content.qml -+++ b/modules/launcher/Content.qml -@@ -1,22 +1,21 @@ - pragma ComponentBehavior: Bound - --import "services" -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config --import Quickshell --import QtQuick -+import qs.modules.launcher.services - - Item { - id: root - -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - required property var panels - required property real maxHeight - -- readonly property int padding: Appearance.padding.large -- readonly property int rounding: Appearance.rounding.large -+ readonly property int padding: Tokens.padding.large -+ readonly property int rounding: Tokens.rounding.large - - implicitWidth: listWrapper.width + padding * 2 - implicitHeight: searchWrapper.height + listWrapper.height + padding * 2 -@@ -48,7 +47,7 @@ Item { - id: searchWrapper - - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - anchors.left: parent.left - anchors.right: parent.right -@@ -73,13 +72,13 @@ Item { - - anchors.left: searchIcon.right - anchors.right: clearIcon.left -- anchors.leftMargin: Appearance.spacing.small -- anchors.rightMargin: Appearance.spacing.small -+ anchors.leftMargin: Tokens.spacing.small -+ anchors.rightMargin: Tokens.spacing.small - -- topPadding: Appearance.padding.larger -- bottomPadding: Appearance.padding.larger -+ topPadding: Tokens.padding.larger -+ bottomPadding: Tokens.padding.larger - -- placeholderText: qsTr("Type \"%1\" for commands").arg(Config.launcher.actionPrefix) -+ placeholderText: qsTr("Type \"%1\" for commands").arg(GlobalConfig.launcher.actionPrefix) - - onAccepted: { - const currentItem = list.currentList?.currentItem; -@@ -89,8 +88,8 @@ Item { - Wallpapers.previewColourLock = true; - Wallpapers.setWallpaper(currentItem.modelData.path); - root.visibilities.launcher = false; -- } else if (text.startsWith(Config.launcher.actionPrefix)) { -- if (text.startsWith(`${Config.launcher.actionPrefix}calc `)) -+ } else if (text.startsWith(GlobalConfig.launcher.actionPrefix)) { -+ if (text.startsWith(`${GlobalConfig.launcher.actionPrefix}calc `)) - currentItem.onClicked(); - else - currentItem.modelData.onClicked(list.currentList); -@@ -107,14 +106,14 @@ Item { - Keys.onEscapePressed: root.visibilities.launcher = false - - Keys.onPressed: event => { -- if (!Config.launcher.vimKeybinds) -+ if (!GlobalConfig.launcher.vimKeybinds) - return; - - if (event.modifiers & Qt.ControlModifier) { -- if (event.key === Qt.Key_J) { -+ if (event.key === Qt.Key_J || event.key === Qt.Key_N) { - list.currentList?.incrementCurrentIndex(); - event.accepted = true; -- } else if (event.key === Qt.Key_K) { -+ } else if (event.key === Qt.Key_K || event.key === Qt.Key_P) { - list.currentList?.decrementCurrentIndex(); - event.accepted = true; - } -@@ -130,8 +129,6 @@ Item { - Component.onCompleted: forceActiveFocus() - - Connections { -- target: root.visibilities -- - function onLauncherChanged(): void { - if (!root.visibilities.launcher) - search.text = ""; -@@ -141,6 +138,8 @@ Item { - if (!root.visibilities.session) - search.forceActiveFocus(); - } -+ -+ target: root.visibilities - } - } - -@@ -177,13 +176,13 @@ Item { - - Behavior on width { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -diff --git a/modules/launcher/ContentList.qml b/modules/launcher/ContentList.qml -index b2a9c770..db7abdf9 100644 ---- a/modules/launcher/ContentList.qml -+++ b/modules/launcher/ContentList.qml -@@ -1,26 +1,25 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick - - Item { - id: root - - required property var content -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - required property var panels - required property real maxHeight - required property StyledTextField search - required property int padding - required property int rounding - -- readonly property bool showWallpapers: search.text.startsWith(`${Config.launcher.actionPrefix}wallpaper `) -- readonly property Item currentList: showWallpapers ? wallpaperList.item : appList.item -+ readonly property bool showWallpapers: search.text.startsWith(`${GlobalConfig.launcher.actionPrefix}wallpaper `) -+ readonly property var currentList: showWallpapers ? wallpaperList.item : appList.item // Can be either ListView or PathView, so can't type properly - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom -@@ -33,7 +32,7 @@ Item { - name: "apps" - - PropertyChanges { -- root.implicitWidth: Config.launcher.sizes.itemWidth -+ root.implicitWidth: root.Tokens.sizes.launcher.itemWidth - root.implicitHeight: Math.min(root.maxHeight, appList.implicitHeight > 0 ? appList.implicitHeight : empty.implicitHeight) - appList.active: true - } -@@ -47,8 +46,8 @@ Item { - name: "wallpapers" - - PropertyChanges { -- root.implicitWidth: Math.max(Config.launcher.sizes.itemWidth * 1.2, wallpaperList.implicitWidth) -- root.implicitHeight: Config.launcher.sizes.wallpaperHeight -+ root.implicitWidth: Math.max(root.Tokens.sizes.launcher.itemWidth * 1.2, wallpaperList.implicitWidth) -+ root.implicitHeight: root.Tokens.sizes.launcher.wallpaperHeight - wallpaperList.active: true - } - } -@@ -61,7 +60,7 @@ Item { - property: "opacity" - from: 1 - to: 0 -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - PropertyAction {} - Anim { -@@ -69,7 +68,7 @@ Item { - property: "opacity" - from: 0 - to: 1 -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - } -@@ -90,6 +89,7 @@ Item { - Loader { - id: wallpaperList - -+ asynchronous: true - active: false - - anchors.top: parent.top -@@ -110,8 +110,8 @@ Item { - opacity: root.currentList?.count === 0 ? 1 : 0 - scale: root.currentList?.count === 0 ? 1 : 0.5 - -- spacing: Appearance.spacing.normal -- padding: Appearance.padding.large -+ spacing: Tokens.spacing.normal -+ padding: Tokens.padding.large - - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter -@@ -119,7 +119,7 @@ Item { - MaterialIcon { - text: root.state === "wallpapers" ? "wallpaper_slideshow" : "manage_search" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - - anchors.verticalCenter: parent.verticalCenter - } -@@ -130,14 +130,14 @@ Item { - StyledText { - text: root.state === "wallpapers" ? qsTr("No wallpapers found") : qsTr("No results") - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - font.weight: 500 - } - - StyledText { - text: root.state === "wallpapers" && Wallpapers.list.length === 0 ? qsTr("Try putting some wallpapers in %1").arg(Paths.shortenHome(Paths.wallsdir)) : qsTr("Try searching for something else") - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - } - -@@ -154,8 +154,8 @@ Item { - enabled: root.visibilities.launcher - - Anim { -- duration: Appearance.anim.durations.large -- easing.bezierCurve: Appearance.anim.curves.emphasizedDecel -+ duration: Tokens.anim.durations.large -+ easing: Tokens.anim.emphasizedDecel - } - } - -@@ -163,8 +163,8 @@ Item { - enabled: root.visibilities.launcher - - Anim { -- duration: Appearance.anim.durations.large -- easing.bezierCurve: Appearance.anim.curves.emphasizedDecel -+ duration: Tokens.anim.durations.large -+ easing: Tokens.anim.emphasizedDecel - } - } - } -diff --git a/modules/launcher/WallpaperList.qml b/modules/launcher/WallpaperList.qml -index 4aba4365..3663f4bc 100644 ---- a/modules/launcher/WallpaperList.qml -+++ b/modules/launcher/WallpaperList.qml -@@ -1,11 +1,11 @@ - pragma ComponentBehavior: Bound - - import "items" -+import QtQuick -+import Quickshell -+import Caelestia.Config - import qs.components.controls - import qs.services --import qs.config --import Quickshell --import QtQuick - - PathView { - id: root -@@ -15,10 +15,10 @@ PathView { - required property var panels - required property var content - -- readonly property int itemWidth: Config.launcher.sizes.wallpaperWidth * 0.8 + Appearance.padding.larger * 2 -+ readonly property int itemWidth: Tokens.sizes.launcher.wallpaperWidth * 0.8 + Tokens.padding.larger * 2 - - readonly property int numItems: { -- const screen = QsWindow.window?.screen; -+ const screen = (QsWindow.window as QsWindow)?.screen; - if (!screen) - return 0; - -@@ -58,7 +58,7 @@ PathView { - - onCurrentItemChanged: { - if (currentItem) -- Wallpapers.preview(currentItem.modelData.path); -+ Wallpapers.preview((currentItem as WallpaperItem).modelData.path); - } - - implicitWidth: Math.min(numItems, count) * itemWidth -diff --git a/modules/launcher/Wrapper.qml b/modules/launcher/Wrapper.qml -index d62d726a..d5630b23 100644 ---- a/modules/launcher/Wrapper.qml -+++ b/modules/launcher/Wrapper.qml -@@ -1,111 +1,47 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.modules.launcher.services - - Item { - id: root - - required property ShellScreen screen -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - required property var panels - - readonly property bool shouldBeActive: visibilities.launcher && Config.launcher.enabled -- property int contentHeight - - readonly property real maxHeight: { -- let max = screen.height - Config.border.thickness * 2 - Appearance.spacing.large; -+ let max = screen.height - Config.border.thickness * 2 - Tokens.spacing.large; - if (visibilities.dashboard) - max -= panels.dashboard.nonAnimHeight; - return max; - } - -- onMaxHeightChanged: timer.start() -- -- visible: height > 0 -- implicitHeight: 0 -- implicitWidth: content.implicitWidth -+ property real offsetScale: shouldBeActive ? 0 : 1 - - onShouldBeActiveChanged: { -- if (shouldBeActive) { -- timer.stop(); -- hideAnim.stop(); -- showAnim.start(); -- } else { -- showAnim.stop(); -- hideAnim.start(); -- } -+ if (shouldBeActive) -+ implicitHeight = Qt.binding(() => content.implicitHeight); -+ else -+ implicitHeight = implicitHeight; // Break binding during close anim - } - -- SequentialAnimation { -- id: showAnim -- -- Anim { -- target: root -- property: "implicitHeight" -- to: root.contentHeight -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- ScriptAction { -- script: root.implicitHeight = Qt.binding(() => content.implicitHeight) -- } -- } -+ visible: offsetScale < 1 -+ anchors.bottomMargin: (-implicitHeight - 5) * offsetScale -+ implicitHeight: content.implicitHeight -+ implicitWidth: content.implicitWidth || 630 // Hard coded fallback for first open -+ opacity: 1 - offsetScale - -- SequentialAnimation { -- id: hideAnim -+ Component.onCompleted: Qt.callLater(() => Apps) // Load apps on init - -- ScriptAction { -- script: root.implicitHeight = root.implicitHeight -- } -+ Behavior on offsetScale { - Anim { -- target: root -- property: "implicitHeight" -- to: 0 -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- } -- -- Connections { -- target: Config.launcher -- -- function onEnabledChanged(): void { -- timer.start(); -- } -- -- function onMaxShownChanged(): void { -- timer.start(); -- } -- } -- -- Connections { -- target: DesktopEntries.applications -- -- function onValuesChanged(): void { -- if (DesktopEntries.applications.values.length < Config.launcher.maxShown) -- timer.start(); -- } -- } -- -- Timer { -- id: timer -- -- interval: Appearance.anim.durations.extraLarge -- onRunningChanged: { -- if (running && !root.shouldBeActive) { -- content.visible = false; -- content.active = true; -- } else { -- root.contentHeight = Math.min(root.maxHeight, content.implicitHeight); -- content.active = Qt.binding(() => root.shouldBeActive || root.visible); -- content.visible = true; -- if (showAnim.running) { -- showAnim.stop(); -- showAnim.start(); -- } -- } -+ type: Anim.DefaultSpatial - } - } - -@@ -115,16 +51,12 @@ Item { - anchors.top: parent.top - anchors.horizontalCenter: parent.horizontalCenter - -- visible: false -- active: false -- Component.onCompleted: timer.start() -+ active: root.shouldBeActive || root.visible - - sourceComponent: Content { - visibilities: root.visibilities - panels: root.panels - maxHeight: root.maxHeight -- -- Component.onCompleted: root.contentHeight = implicitHeight - } - } - } -diff --git a/modules/launcher/items/ActionItem.qml b/modules/launcher/items/ActionItem.qml -index e1580290..0f9fb5dd 100644 ---- a/modules/launcher/items/ActionItem.qml -+++ b/modules/launcher/items/ActionItem.qml -@@ -1,8 +1,7 @@ --import "../services" -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import QtQuick - - Item { - id: root -@@ -10,37 +9,34 @@ Item { - required property var modelData - required property var list - -- implicitHeight: Config.launcher.sizes.itemHeight -+ implicitHeight: Tokens.sizes.launcher.itemHeight - - anchors.left: parent?.left - anchors.right: parent?.right - - StateLayer { -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -- root.modelData?.onClicked(root.list); -- } -+ radius: Tokens.rounding.normal -+ onClicked: root.modelData?.onClicked(root.list) - } - - Item { - anchors.fill: parent -- anchors.leftMargin: Appearance.padding.larger -- anchors.rightMargin: Appearance.padding.larger -- anchors.margins: Appearance.padding.smaller -+ anchors.leftMargin: Tokens.padding.larger -+ anchors.rightMargin: Tokens.padding.larger -+ anchors.margins: Tokens.padding.smaller - - MaterialIcon { - id: icon - - text: root.modelData?.icon ?? "" -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - - anchors.verticalCenter: parent.verticalCenter - } - - Item { - anchors.left: icon.right -- anchors.leftMargin: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.spacing.normal - anchors.verticalCenter: icon.verticalCenter - - implicitWidth: parent.width - icon.width -@@ -50,18 +46,18 @@ Item { - id: name - - text: root.modelData?.name ?? "" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledText { - id: desc - - text: root.modelData?.desc ?? "" -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - - elide: Text.ElideRight -- width: root.width - icon.width - Appearance.rounding.normal * 2 -+ width: root.width - icon.width - Tokens.rounding.normal * 2 - - anchors.top: name.bottom - } -diff --git a/modules/launcher/items/AppItem.qml b/modules/launcher/items/AppItem.qml -index 48aace76..80739c8f 100644 ---- a/modules/launcher/items/AppItem.qml -+++ b/modules/launcher/items/AppItem.qml -@@ -1,26 +1,26 @@ --import "../services" --import qs.components --import qs.services --import qs.config -+import QtQuick - import Quickshell - import Quickshell.Widgets --import QtQuick -+import Caelestia.Config -+import qs.components -+import qs.services -+import qs.utils -+import qs.modules.launcher.services - - Item { - id: root - - required property DesktopEntry modelData -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - -- implicitHeight: Config.launcher.sizes.itemHeight -+ implicitHeight: Tokens.sizes.launcher.itemHeight - - anchors.left: parent?.left - anchors.right: parent?.right - - StateLayer { -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -+ radius: Tokens.rounding.normal -+ onClicked: { - Apps.launch(root.modelData); - root.visibilities.launcher = false; - } -@@ -28,13 +28,14 @@ Item { - - Item { - anchors.fill: parent -- anchors.leftMargin: Appearance.padding.larger -- anchors.rightMargin: Appearance.padding.larger -- anchors.margins: Appearance.padding.smaller -+ anchors.leftMargin: Tokens.padding.larger -+ anchors.rightMargin: Tokens.padding.larger -+ anchors.margins: Tokens.padding.smaller - - IconImage { - id: icon - -+ asynchronous: true - source: Quickshell.iconPath(root.modelData?.icon, "image-missing") - implicitSize: parent.height * 0.8 - -@@ -43,31 +44,46 @@ Item { - - Item { - anchors.left: icon.right -- anchors.leftMargin: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.spacing.normal - anchors.verticalCenter: icon.verticalCenter - -- implicitWidth: parent.width - icon.width -+ implicitWidth: parent.width - icon.width - favouriteIcon.width - implicitHeight: name.implicitHeight + comment.implicitHeight - - StyledText { - id: name - - text: root.modelData?.name ?? "" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledText { - id: comment - - text: (root.modelData?.comment || root.modelData?.genericName || root.modelData?.name) ?? "" -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - - elide: Text.ElideRight -- width: root.width - icon.width - Appearance.rounding.normal * 2 -+ width: root.width - icon.width - favouriteIcon.width - Tokens.rounding.normal * 2 - - anchors.top: name.bottom - } - } -+ -+ Loader { -+ id: favouriteIcon -+ -+ asynchronous: true -+ anchors.verticalCenter: parent.verticalCenter -+ anchors.right: parent.right -+ active: root.modelData && Strings.testRegexList(GlobalConfig.launcher.favouriteApps, root.modelData.id) -+ -+ sourceComponent: MaterialIcon { -+ text: "favorite" -+ fill: 1 -+ color: Colours.palette.m3primary -+ } -+ } - } - } -diff --git a/modules/launcher/items/CalcItem.qml b/modules/launcher/items/CalcItem.qml -index 65489d9b..f746f96c 100644 ---- a/modules/launcher/items/CalcItem.qml -+++ b/modules/launcher/items/CalcItem.qml -@@ -1,46 +1,48 @@ --import qs.components --import qs.services --import qs.config --import Caelestia --import Quickshell - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Caelestia -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root - - required property var list -- readonly property string math: list.search.text.slice(`${Config.launcher.actionPrefix}calc `.length) -+ readonly property string math: list.search.text.slice(`${GlobalConfig.launcher.actionPrefix}calc `.length) - - function onClicked(): void { -- Quickshell.execDetached(["wl-copy", Qalculator.eval(math, false)]); -+ Quickshell.execDetached(["wl-copy", Qalculator.rawResult]); - root.list.visibilities.launcher = false; - } - -- implicitHeight: Config.launcher.sizes.itemHeight -+ onMathChanged: { -+ if (math.length > 0) -+ Qalculator.evalAsync(math); -+ } -+ -+ implicitHeight: Tokens.sizes.launcher.itemHeight - - anchors.left: parent?.left - anchors.right: parent?.right - - StateLayer { -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -- root.onClicked(); -- } -+ radius: Tokens.rounding.normal -+ onClicked: root.onClicked() - } - - RowLayout { - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter -- anchors.margins: Appearance.padding.larger -+ anchors.margins: Tokens.padding.larger - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - MaterialIcon { - text: "function" -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - Layout.alignment: Qt.AlignVCenter - } - -@@ -55,7 +57,7 @@ Item { - return Colours.palette.m3onSurface; - } - -- text: root.math.length > 0 ? Qalculator.eval(root.math) : qsTr("Type an expression to calculate") -+ text: root.math.length > 0 ? (Qalculator.result || qsTr("Calculating...")) : qsTr("Type an expression to calculate") - elide: Text.ElideLeft - - Layout.fillWidth: true -@@ -64,23 +66,23 @@ Item { - - StyledRect { - color: Colours.palette.m3tertiary -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - clip: true - -- implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Appearance.padding.small * 2 -+ implicitWidth: (stateLayer.containsMouse ? label.implicitWidth + label.anchors.rightMargin : 0) + icon.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: Math.max(label.implicitHeight, icon.implicitHeight) + Tokens.padding.small * 2 - - Layout.alignment: Qt.AlignVCenter - - StateLayer { - id: stateLayer - -- color: Colours.palette.m3onTertiary -- -- function onClicked(): void { -- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); -+ onClicked: { -+ Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.terminal, "fish", "-C", `exec qalc -i '${root.math}'`]); - root.list.visibilities.launcher = false; - } -+ -+ color: Colours.palette.m3onTertiary - } - - StyledText { -@@ -88,11 +90,11 @@ Item { - - anchors.verticalCenter: parent.verticalCenter - anchors.right: icon.left -- anchors.rightMargin: Appearance.spacing.small -+ anchors.rightMargin: Tokens.spacing.small - - text: qsTr("Open in calculator") - color: Colours.palette.m3onTertiary -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - - opacity: stateLayer.containsMouse ? 1 : 0 - -@@ -106,16 +108,16 @@ Item { - - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right -- anchors.rightMargin: Appearance.padding.normal -+ anchors.rightMargin: Tokens.padding.normal - - text: "open_in_new" - color: Colours.palette.m3onTertiary -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - - Behavior on implicitWidth { - Anim { -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - } -diff --git a/modules/launcher/items/SchemeItem.qml b/modules/launcher/items/SchemeItem.qml -index 3ff18468..ab0784bd 100644 ---- a/modules/launcher/items/SchemeItem.qml -+++ b/modules/launcher/items/SchemeItem.qml -@@ -1,8 +1,8 @@ --import "../services" -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import QtQuick -+import qs.modules.launcher.services - - Item { - id: root -@@ -10,24 +10,21 @@ Item { - required property Schemes.Scheme modelData - required property var list - -- implicitHeight: Config.launcher.sizes.itemHeight -+ implicitHeight: Tokens.sizes.launcher.itemHeight - - anchors.left: parent?.left - anchors.right: parent?.right - - StateLayer { -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -- root.modelData?.onClicked(root.list); -- } -+ radius: Tokens.rounding.normal -+ onClicked: root.modelData?.onClicked(root.list) - } - - Item { - anchors.fill: parent -- anchors.leftMargin: Appearance.padding.larger -- anchors.rightMargin: Appearance.padding.larger -- anchors.margins: Appearance.padding.smaller -+ anchors.leftMargin: Tokens.padding.larger -+ anchors.rightMargin: Tokens.padding.larger -+ anchors.margins: Tokens.padding.smaller - - StyledRect { - id: preview -@@ -38,7 +35,7 @@ Item { - border.color: Qt.alpha(`#${root.modelData?.colours?.outline}`, 0.5) - - color: `#${root.modelData?.colours?.surface}` -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - implicitWidth: parent.height * 0.8 - implicitHeight: parent.height * 0.8 - -@@ -57,27 +54,27 @@ Item { - - implicitWidth: preview.implicitWidth - color: `#${root.modelData?.colours?.primary}` -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - } - } - } - - Column { - anchors.left: preview.right -- anchors.leftMargin: Appearance.spacing.normal -+ anchors.leftMargin: Tokens.spacing.normal - anchors.verticalCenter: parent.verticalCenter - -- width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) -+ width: parent.width - preview.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) - spacing: 0 - - StyledText { - text: root.modelData?.flavour ?? "" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: root.modelData?.name ?? "" -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - - elide: Text.ElideRight -@@ -89,6 +86,7 @@ Item { - Loader { - id: current - -+ asynchronous: true - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - -@@ -97,7 +95,7 @@ Item { - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -diff --git a/modules/launcher/items/VariantItem.qml b/modules/launcher/items/VariantItem.qml -index 5c34fa89..b13d5d8e 100644 ---- a/modules/launcher/items/VariantItem.qml -+++ b/modules/launcher/items/VariantItem.qml -@@ -1,8 +1,8 @@ --import "../services" -+import QtQuick -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import QtQuick -+import qs.modules.launcher.services - - Item { - id: root -@@ -10,50 +10,47 @@ Item { - required property M3Variants.Variant modelData - required property var list - -- implicitHeight: Config.launcher.sizes.itemHeight -+ implicitHeight: Tokens.sizes.launcher.itemHeight - - anchors.left: parent?.left - anchors.right: parent?.right - - StateLayer { -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -- root.modelData?.onClicked(root.list); -- } -+ radius: Tokens.rounding.normal -+ onClicked: root.modelData?.onClicked(root.list) - } - - Item { - anchors.fill: parent -- anchors.leftMargin: Appearance.padding.larger -- anchors.rightMargin: Appearance.padding.larger -- anchors.margins: Appearance.padding.smaller -+ anchors.leftMargin: Tokens.padding.larger -+ anchors.rightMargin: Tokens.padding.larger -+ anchors.margins: Tokens.padding.smaller - - MaterialIcon { - id: icon - - text: root.modelData?.icon ?? "" -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - - anchors.verticalCenter: parent.verticalCenter - } - - Column { - anchors.left: icon.right -- anchors.leftMargin: Appearance.spacing.larger -+ anchors.leftMargin: Tokens.spacing.larger - anchors.verticalCenter: icon.verticalCenter - -- width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Appearance.spacing.normal : 0) -+ width: parent.width - icon.width - anchors.leftMargin - (current.active ? current.width + Tokens.spacing.normal : 0) - spacing: 0 - - StyledText { - text: root.modelData?.name ?? "" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - StyledText { - text: root.modelData?.description ?? "" -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - color: Colours.palette.m3outline - - elide: Text.ElideRight -@@ -65,6 +62,7 @@ Item { - Loader { - id: current - -+ asynchronous: true - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - -@@ -73,7 +71,7 @@ Item { - sourceComponent: MaterialIcon { - text: "check" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -diff --git a/modules/launcher/items/WallpaperItem.qml b/modules/launcher/items/WallpaperItem.qml -index 9fdac3f3..d09a894c 100644 ---- a/modules/launcher/items/WallpaperItem.qml -+++ b/modules/launcher/items/WallpaperItem.qml -@@ -1,34 +1,33 @@ -+import QtQuick -+import Quickshell -+import Caelestia.Config -+import Caelestia.Models - import qs.components - import qs.components.effects - import qs.components.images - import qs.services --import qs.config --import Caelestia.Models --import Quickshell --import QtQuick - - Item { - id: root - - required property FileSystemEntry modelData -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - - scale: 0.5 - opacity: 0 -- z: PathView.z ?? 0 -+ z: PathView.z ?? 0 // qmllint disable missing-property - - Component.onCompleted: { - scale = Qt.binding(() => PathView.isCurrentItem ? 1 : PathView.onPath ? 0.8 : 0); - opacity = Qt.binding(() => PathView.onPath ? 1 : 0); - } - -- implicitWidth: image.width + Appearance.padding.larger * 2 -- implicitHeight: image.height + label.height + Appearance.spacing.small / 2 + Appearance.padding.large + Appearance.padding.normal -+ implicitWidth: image.width + Tokens.padding.larger * 2 -+ implicitHeight: image.height + label.height + Tokens.spacing.small / 2 + Tokens.padding.large + Tokens.padding.normal - - StateLayer { -- radius: Appearance.rounding.normal -- -- function onClicked(): void { -+ radius: Tokens.rounding.normal -+ onClicked: { - Wallpapers.setWallpaper(root.modelData.path); - root.visibilities.launcher = false; - } -@@ -49,27 +48,29 @@ Item { - id: image - - anchors.horizontalCenter: parent.horizontalCenter -- y: Appearance.padding.large -+ y: Tokens.padding.large - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - -- implicitWidth: Config.launcher.sizes.wallpaperWidth -+ implicitWidth: Tokens.sizes.launcher.wallpaperWidth - implicitHeight: implicitWidth / 16 * 9 - - MaterialIcon { - anchors.centerIn: parent - text: "image" - color: Colours.tPalette.m3outline -- font.pointSize: Appearance.font.size.extraLarge * 2 -+ font.pointSize: Tokens.font.size.extraLarge * 2 - font.weight: 600 - } - - CachingImage { -+ anchors.fill: parent - path: root.modelData.path - smooth: !root.PathView.view.moving -- cache: true -- -- anchors.fill: parent -+ sourceSize: { -+ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; -+ return Qt.size(image.implicitWidth * dpr, image.implicitHeight * dpr); -+ } - } - } - -@@ -77,15 +78,15 @@ Item { - id: label - - anchors.top: image.bottom -- anchors.topMargin: Appearance.spacing.small / 2 -+ anchors.topMargin: Tokens.spacing.small / 2 - anchors.horizontalCenter: parent.horizontalCenter - -- width: image.width - Appearance.padding.normal * 2 -+ width: image.width - Tokens.padding.normal * 2 - horizontalAlignment: Text.AlignHCenter - elide: Text.ElideRight - renderType: Text.QtRendering - text: root.modelData.relativePath -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - Behavior on scale { -diff --git a/modules/launcher/services/Actions.qml b/modules/launcher/services/Actions.qml -index 5c1cb6bb..634b3e6a 100644 ---- a/modules/launcher/services/Actions.qml -+++ b/modules/launcher/services/Actions.qml -@@ -1,26 +1,26 @@ - pragma Singleton - - import ".." -+import QtQuick -+import Quickshell -+import Caelestia.Config - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick - - Searcher { - id: root - - function transformSearch(search: string): string { -- return search.slice(Config.launcher.actionPrefix.length); -+ return search.slice(GlobalConfig.launcher.actionPrefix.length); - } - - list: variants.instances -- useFuzzy: Config.launcher.useFuzzy.actions -+ useFuzzy: GlobalConfig.launcher.useFuzzy.actions - - Variants { - id: variants - -- model: Config.launcher.actions.filter(a => (a.enabled ?? true) && (Config.launcher.enableDangerousActions || !(a.dangerous ?? false))) -+ model: GlobalConfig.launcher.actions.filter(a => (a.enabled ?? true) && (GlobalConfig.launcher.enableDangerousActions || !(a.dangerous ?? false))) - - Action {} - } -@@ -39,7 +39,7 @@ Searcher { - return; - - if (command[0] === "autocomplete" && command.length > 1) { -- list.search.text = `${Config.launcher.actionPrefix}${command[1]} `; -+ list.search.text = `${GlobalConfig.launcher.actionPrefix}${command[1]} `; - } else if (command[0] === "setMode" && command.length > 1) { - list.visibilities.launcher = false; - Colours.setMode(command[1]); -diff --git a/modules/launcher/services/Apps.qml b/modules/launcher/services/Apps.qml -index c409a7bb..168d9209 100644 ---- a/modules/launcher/services/Apps.qml -+++ b/modules/launcher/services/Apps.qml -@@ -1,9 +1,9 @@ - pragma Singleton - --import qs.config --import qs.utils --import Caelestia - import Quickshell -+import Caelestia -+import Caelestia.Config -+import qs.utils - - Searcher { - id: root -@@ -13,7 +13,7 @@ Searcher { - - if (entry.runInTerminal) - Quickshell.execDetached({ -- command: ["app2unit", "--", ...Config.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], -+ command: ["app2unit", "--", ...GlobalConfig.general.apps.terminal, `${Quickshell.shellDir}/assets/wrap_term_launch.sh`, ...entry.command], - workingDirectory: entry.workingDirectory - }); - else -@@ -24,7 +24,7 @@ Searcher { - } - - function search(search: string): list { -- const prefix = Config.launcher.specialPrefix; -+ const prefix = GlobalConfig.launcher.specialPrefix; - - if (search.startsWith(`${prefix}i `)) { - keys = ["id", "name"]; -@@ -66,12 +66,13 @@ Searcher { - } - - list: appDb.apps -- useFuzzy: Config.launcher.useFuzzy.apps -+ useFuzzy: GlobalConfig.launcher.useFuzzy.apps - - AppDb { - id: appDb - - path: `${Paths.state}/apps.sqlite` -- entries: DesktopEntries.applications.values.filter(a => !Config.launcher.hiddenApps.includes(a.id)) -+ favouriteApps: GlobalConfig.launcher.favouriteApps -+ entries: DesktopEntries.applications.values.filter(a => !Strings.testRegexList(GlobalConfig.launcher.hiddenApps, a.id)) - } - } -diff --git a/modules/launcher/services/M3Variants.qml b/modules/launcher/services/M3Variants.qml -index 963a4d43..4517d02c 100644 ---- a/modules/launcher/services/M3Variants.qml -+++ b/modules/launcher/services/M3Variants.qml -@@ -1,16 +1,16 @@ - pragma Singleton - - import ".." --import qs.config --import qs.utils --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia.Config -+import qs.utils - - Searcher { - id: root - - function transformSearch(search: string): string { -- return search.slice(`${Config.launcher.actionPrefix}variant `.length); -+ return search.slice(`${GlobalConfig.launcher.actionPrefix}variant `.length); - } - - list: [ -@@ -69,7 +69,7 @@ Searcher { - description: qsTr("All colours are grayscale, no chroma.") - } - ] -- useFuzzy: Config.launcher.useFuzzy.variants -+ useFuzzy: GlobalConfig.launcher.useFuzzy.variants - - component Variant: QtObject { - required property string variant -diff --git a/modules/launcher/services/Schemes.qml b/modules/launcher/services/Schemes.qml -index dbb2dac0..cc555f29 100644 ---- a/modules/launcher/services/Schemes.qml -+++ b/modules/launcher/services/Schemes.qml -@@ -1,11 +1,11 @@ - pragma Singleton - - import ".." --import qs.config --import qs.utils -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick -+import Caelestia.Config -+import qs.utils - - Searcher { - id: root -@@ -14,7 +14,7 @@ Searcher { - property string currentVariant - - function transformSearch(search: string): string { -- return search.slice(`${Config.launcher.actionPrefix}scheme `.length); -+ return search.slice(`${GlobalConfig.launcher.actionPrefix}scheme `.length); - } - - function selector(item: var): string { -@@ -26,7 +26,7 @@ Searcher { - } - - list: schemes.instances -- useFuzzy: Config.launcher.useFuzzy.schemes -+ useFuzzy: GlobalConfig.launcher.useFuzzy.schemes - keys: ["name", "flavour"] - weights: [0.9, 0.1] - -@@ -55,7 +55,7 @@ Searcher { - for (const f of s) - flat.push(f); - -- schemes.model = flat.sort((a, b) => (a.name + a.flavour).localeCompare((b.name + b.flavour))); -+ schemes.model = flat.sort((a, b) => String(a.name + a.flavour).localeCompare((b.name + b.flavour))); - } - } - } -diff --git a/modules/lock/Center.qml b/modules/lock/Center.qml -index 19cf9d29..a987d4eb 100644 ---- a/modules/lock/Center.qml -+++ b/modules/lock/Center.qml -@@ -1,37 +1,37 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.images - import qs.services --import qs.config - import qs.utils --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property var lock - readonly property real centerScale: Math.min(1, (lock.screen?.height ?? 1440) / 1440) -- readonly property int centerWidth: Config.lock.sizes.centerWidth * centerScale -+ readonly property int centerWidth: Tokens.sizes.lock.centerWidth * centerScale - - Layout.preferredWidth: centerWidth - Layout.fillWidth: false - Layout.fillHeight: true - -- spacing: Appearance.spacing.large * 2 -+ spacing: Tokens.spacing.large * 2 - - RowLayout { - Layout.alignment: Qt.AlignHCenter -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - Layout.alignment: Qt.AlignVCenter - text: Time.hourStr - color: Colours.palette.m3secondary -- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) -- font.family: Appearance.font.family.clock -+ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) -+ font.family: Tokens.font.family.clock - font.bold: true - } - -@@ -39,8 +39,8 @@ ColumnLayout { - Layout.alignment: Qt.AlignVCenter - text: ":" - color: Colours.palette.m3primary -- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) -- font.family: Appearance.font.family.clock -+ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) -+ font.family: Tokens.font.family.clock - font.bold: true - } - -@@ -48,23 +48,24 @@ ColumnLayout { - Layout.alignment: Qt.AlignVCenter - text: Time.minuteStr - color: Colours.palette.m3secondary -- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 3 * root.centerScale) -- font.family: Appearance.font.family.clock -+ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 3 * root.centerScale) -+ font.family: Tokens.font.family.clock - font.bold: true - } - - Loader { -- Layout.leftMargin: Appearance.spacing.small -+ asynchronous: true -+ Layout.leftMargin: Tokens.spacing.small - Layout.alignment: Qt.AlignVCenter - -- active: Config.services.useTwelveHourClock -+ active: GlobalConfig.services.useTwelveHourClock - visible: active - - sourceComponent: StyledText { - text: Time.amPmStr - color: Colours.palette.m3primary -- font.pointSize: Math.floor(Appearance.font.size.extraLarge * 2 * root.centerScale) -- font.family: Appearance.font.family.clock -+ font.pointSize: Math.floor(Tokens.font.size.extraLarge * 2 * root.centerScale) -+ font.family: Tokens.font.family.clock - font.bold: true - } - } -@@ -72,24 +73,24 @@ ColumnLayout { - - StyledText { - Layout.alignment: Qt.AlignHCenter -- Layout.topMargin: -Appearance.padding.large * 2 -+ Layout.topMargin: -Tokens.padding.large * 2 - - text: Time.format("dddd, d MMMM yyyy") - color: Colours.palette.m3tertiary -- font.pointSize: Math.floor(Appearance.font.size.extraLarge * root.centerScale) -- font.family: Appearance.font.family.mono -+ font.pointSize: Math.floor(Tokens.font.size.extraLarge * root.centerScale) -+ font.family: Tokens.font.family.mono - font.bold: true - } - - StyledClippingRect { -- Layout.topMargin: Appearance.spacing.large * 2 -+ Layout.topMargin: Tokens.spacing.large * 2 - Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth / 2 - implicitHeight: root.centerWidth / 2 - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - MaterialIcon { - anchors.centerIn: parent -@@ -97,6 +98,7 @@ ColumnLayout { - text: "person" - color: Colours.palette.m3onSurfaceVariant - font.pointSize: Math.floor(root.centerWidth / 4) -+ visible: pfp.status !== Image.Ready - } - - CachingImage { -@@ -111,10 +113,10 @@ ColumnLayout { - Layout.alignment: Qt.AlignHCenter - - implicitWidth: root.centerWidth * 0.8 -- implicitHeight: input.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: input.implicitHeight + Tokens.padding.small * 2 - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - focus: true - onActiveFocusChanged: { -@@ -133,24 +135,24 @@ ColumnLayout { - } - - StateLayer { -- hoverEnabled: false -- cursorShape: Qt.IBeamCursor -- -- function onClicked(): void { -+ onClicked: { - parent.forceActiveFocus(); - } -+ -+ hoverEnabled: false -+ cursorShape: Qt.IBeamCursor - } - - RowLayout { - id: input - - anchors.fill: parent -- anchors.margins: Appearance.padding.small -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.small -+ spacing: Tokens.spacing.normal - - Item { - implicitWidth: implicitHeight -- implicitHeight: fprintIcon.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: fprintIcon.implicitHeight + Tokens.padding.small * 2 - - MaterialIcon { - id: fprintIcon -@@ -158,13 +160,13 @@ ColumnLayout { - anchors.centerIn: parent - animate: true - text: { -- if (root.lock.pam.fprint.tries >= Config.lock.maxFprintTries) -+ if (root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries) - return "fingerprint_off"; - if (root.lock.pam.fprint.active) - return "fingerprint"; - return "lock"; - } -- color: root.lock.pam.fprint.tries >= Config.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface -+ color: root.lock.pam.fprint.tries >= GlobalConfig.lock.maxFprintTries ? Colours.palette.m3error : Colours.palette.m3onSurface - opacity: root.lock.pam.passwd.active ? 0 : 1 - - Behavior on opacity { -@@ -186,17 +188,14 @@ ColumnLayout { - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: enterIcon.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: enterIcon.implicitHeight + Tokens.padding.small * 2 - - color: root.lock.pam.buffer ? Colours.palette.m3primary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - StateLayer { - color: root.lock.pam.buffer ? Colours.palette.m3onPrimary : Colours.palette.m3onSurface -- -- function onClicked(): void { -- root.lock.pam.passwd.start(); -- } -+ onClicked: root.lock.pam.passwd.start() - } - - MaterialIcon { -@@ -213,7 +212,7 @@ ColumnLayout { - - Item { - Layout.fillWidth: true -- Layout.topMargin: -Appearance.spacing.large -+ Layout.topMargin: -Tokens.spacing.large - - implicitHeight: Math.max(message.implicitHeight, stateMessage.implicitHeight) - -@@ -270,7 +269,7 @@ ColumnLayout { - color: Colours.palette.m3onSurfaceVariant - animateProp: "opacity" - -- font.family: Appearance.font.family.mono -+ font.family: Tokens.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - lineHeight: 1.2 -@@ -325,8 +324,8 @@ ColumnLayout { - opacity: 0 - color: Colours.palette.m3error - -- font.pointSize: Appearance.font.size.small -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.small -+ font.family: Tokens.font.family.mono - horizontalAlignment: Qt.AlignHCenter - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - -@@ -355,8 +354,6 @@ ColumnLayout { - } - - Connections { -- target: root.lock.pam -- - function onFlashMsg(): void { - exitAnim.stop(); - if (message.scale < 1) -@@ -364,6 +361,8 @@ ColumnLayout { - else - flashAnim.restart(); - } -+ -+ target: root.lock.pam - } - - Anim { -@@ -395,13 +394,13 @@ ColumnLayout { - target: message - property: "scale" - to: 0.7 -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - Anim { - target: message - property: "opacity" - to: 0 -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - } - } -@@ -410,7 +409,7 @@ ColumnLayout { - component FlashAnim: NumberAnimation { - target: message - property: "opacity" -- duration: Appearance.anim.durations.small -+ duration: Tokens.anim.durations.small - easing.type: Easing.Linear - } - } -diff --git a/modules/lock/Content.qml b/modules/lock/Content.qml -index a024ddc2..0ff7daad 100644 ---- a/modules/lock/Content.qml -+++ b/modules/lock/Content.qml -@@ -1,26 +1,26 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - RowLayout { - id: root - - required property var lock - -- spacing: Appearance.spacing.large * 2 -+ spacing: Tokens.spacing.large * 2 - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - Layout.fillWidth: true - implicitHeight: weather.implicitHeight - -- topLeftRadius: Appearance.rounding.large -- radius: Appearance.rounding.small -+ topLeftRadius: Tokens.rounding.large -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - WeatherInfo { -@@ -34,7 +34,7 @@ RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Fetch {} -@@ -44,8 +44,8 @@ RowLayout { - Layout.fillWidth: true - implicitHeight: media.implicitHeight - -- bottomLeftRadius: Appearance.rounding.large -- radius: Appearance.rounding.small -+ bottomLeftRadius: Tokens.rounding.large -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Media { -@@ -62,14 +62,14 @@ RowLayout { - - ColumnLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - Layout.fillWidth: true - implicitHeight: resources.implicitHeight - -- topRightRadius: Appearance.rounding.large -- radius: Appearance.rounding.small -+ topRightRadius: Tokens.rounding.large -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - Resources { -@@ -81,8 +81,8 @@ RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - -- bottomRightRadius: Appearance.rounding.large -- radius: Appearance.rounding.small -+ bottomRightRadius: Tokens.rounding.large -+ radius: Tokens.rounding.small - color: Colours.tPalette.m3surfaceContainer - - NotifDock { -diff --git a/modules/lock/Fetch.qml b/modules/lock/Fetch.qml -index ded56084..0afbe455 100644 ---- a/modules/lock/Fetch.qml -+++ b/modules/lock/Fetch.qml -@@ -1,41 +1,41 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Services.UPower -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services --import qs.config - import qs.utils --import Quickshell.Services.UPower --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - - anchors.fill: parent -- anchors.margins: Appearance.padding.large * 2 -- anchors.topMargin: Appearance.padding.large -+ anchors.margins: Tokens.padding.large * 2 -+ anchors.topMargin: Tokens.padding.large - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: false -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { -- implicitWidth: prompt.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: prompt.implicitHeight + Appearance.padding.normal * 2 -+ implicitWidth: prompt.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: prompt.implicitHeight + Tokens.padding.normal * 2 - - color: Colours.palette.m3primary -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - - MonoText { - id: prompt - - anchors.centerIn: parent - text: ">" -- font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal -+ font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - color: Colours.palette.m3onPrimary - } - } -@@ -43,7 +43,7 @@ ColumnLayout { - MonoText { - Layout.fillWidth: true - text: "caelestiafetch.sh" -- font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal -+ font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - elide: Text.ElideRight - } - -@@ -51,7 +51,7 @@ ColumnLayout { - Layout.fillHeight: true - active: !iconLoader.active - -- sourceComponent: OsLogo {} -+ sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon - } - } - -@@ -66,15 +66,15 @@ ColumnLayout { - Layout.fillHeight: true - active: root.width > 320 - -- sourceComponent: OsLogo {} -+ sourceComponent: SysInfo.isDefaultLogo ? caelestiaLogo : distroIcon - } - - ColumnLayout { - Layout.fillWidth: true -- Layout.topMargin: Appearance.padding.normal -- Layout.bottomMargin: Appearance.padding.normal -+ Layout.topMargin: Tokens.padding.normal -+ Layout.bottomMargin: Tokens.padding.normal - Layout.leftMargin: iconLoader.active ? 0 : width * 0.1 -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - WrappedLoader { - Layout.fillWidth: true -@@ -125,41 +125,54 @@ ColumnLayout { - active: root.height > 180 - - sourceComponent: RowLayout { -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - Repeater { -- model: Math.max(0, Math.min(8, root.width / (Appearance.font.size.larger * 2 + Appearance.spacing.large))) -+ model: Math.max(0, Math.min(8, root.width / (Tokens.font.size.larger * 2 + Tokens.spacing.large))) - - StyledRect { - required property int index - - implicitWidth: implicitHeight -- implicitHeight: Appearance.font.size.larger * 2 -+ implicitHeight: Tokens.font.size.larger * 2 - color: Colours.palette[`term${index}`] -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - } - } - } - } - -- component WrappedLoader: Loader { -- visible: active -+ Component { -+ id: caelestiaLogo -+ -+ Logo { -+ width: height -+ } -+ } -+ -+ Component { -+ id: distroIcon -+ -+ ColouredIcon { -+ source: SysInfo.osLogo -+ implicitSize: height -+ colour: Colours.palette.m3primary -+ layer.enabled: Config.lock.recolourLogo -+ } - } - -- component OsLogo: ColouredIcon { -- source: SysInfo.osLogo -- implicitSize: height -- colour: Colours.palette.m3primary -- layer.enabled: Config.lock.recolourLogo || SysInfo.isDefaultLogo -+ component WrappedLoader: Loader { -+ asynchronous: true -+ visible: active - } - - component FetchText: MonoText { - Layout.fillWidth: true -- font.pointSize: root.width > 400 ? Appearance.font.size.larger : Appearance.font.size.normal -+ font.pointSize: root.width > 400 ? Tokens.font.size.larger : Tokens.font.size.normal - elide: Text.ElideRight - } - - component MonoText: StyledText { -- font.family: Appearance.font.family.mono -+ font.family: Tokens.font.family.mono - } - } -diff --git a/modules/lock/InputField.qml b/modules/lock/InputField.qml -index 358093f3..1b6c34a7 100644 ---- a/modules/lock/InputField.qml -+++ b/modules/lock/InputField.qml -@@ -1,11 +1,11 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import Quickshell - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root -@@ -20,8 +20,6 @@ Item { - clip: true - - Connections { -- target: root.pam -- - function onBufferChanged(): void { - if (root.pam.buffer.length > root.buffer.length) { - charList.bindImWidth(); -@@ -32,6 +30,8 @@ Item { - - root.buffer = root.pam.buffer; - } -+ -+ target: root.pam - } - - StyledText { -@@ -49,8 +49,8 @@ Item { - - animate: true - color: root.pam.passwd.active ? Colours.palette.m3secondary : Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.normal -+ font.family: Tokens.font.family.mono - - opacity: root.buffer ? 0 : 1 - -@@ -74,10 +74,10 @@ Item { - anchors.horizontalCenterOffset: implicitWidth > root.width ? -(implicitWidth - root.width) / 2 : 0 - - implicitWidth: fullWidth -- implicitHeight: Appearance.font.size.normal -+ implicitHeight: Tokens.font.size.normal - - orientation: Qt.Horizontal -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - interactive: false - - model: ScriptModel { -@@ -91,7 +91,7 @@ Item { - implicitHeight: charList.implicitHeight - - color: Colours.palette.m3onSurface -- radius: Appearance.rounding.small / 2 -+ radius: Tokens.rounding.small / 2 - - opacity: 0 - scale: 0 -@@ -134,8 +134,7 @@ Item { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -diff --git a/modules/lock/Lock.qml b/modules/lock/Lock.qml -index 6fd5277f..f852cb7f 100644 ---- a/modules/lock/Lock.qml -+++ b/modules/lock/Lock.qml -@@ -1,9 +1,10 @@ - pragma ComponentBehavior: Bound - --import qs.components.misc -+import QtQuick - import Quickshell - import Quickshell.Io - import Quickshell.Wayland -+import qs.components.misc - - Scope { - property alias lock: lock -@@ -25,21 +26,37 @@ Scope { - lock: lock - } - -+ Loader { -+ asynchronous: true -+ active: true -+ onLoaded: active = false -+ -+ // Force a load of a screencopy so the one in the lock works -+ // My guess is the ICC backend loads async on first request, which if the lock is -+ // the first request it fails to capture (because it's async and the compositor -+ // refuses capture when locked) -+ sourceComponent: ScreencopyView { -+ captureSource: Quickshell.screens[0] -+ } -+ } -+ -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "lock" - description: "Lock the current session" - onPressed: lock.locked = true - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "unlock" - description: "Unlock the current session" - onPressed: lock.unlock() - } - - IpcHandler { -- target: "lock" -- - function lock(): void { - lock.locked = true; - } -@@ -51,5 +68,7 @@ Scope { - function isLocked(): bool { - return lock.locked; - } -+ -+ target: "lock" - } - } -diff --git a/modules/lock/LockSurface.qml b/modules/lock/LockSurface.qml -index 279c5513..322773fd 100644 ---- a/modules/lock/LockSurface.qml -+++ b/modules/lock/LockSurface.qml -@@ -1,11 +1,11 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import Quickshell.Wayland - import QtQuick - import QtQuick.Effects -+import Quickshell.Wayland -+import Caelestia.Config -+import qs.components -+import qs.services - - WlSessionLockSurface { - id: root -@@ -15,14 +15,17 @@ WlSessionLockSurface { - - readonly property alias unlocking: unlockAnim.running - -+ contentItem.Config.screen: screen.name -+ contentItem.Tokens.screen: screen.name -+ - color: "transparent" - - Connections { -- target: root.lock -- - function onUnlock(): void { - unlockAnim.start(); - } -+ -+ target: root.lock - } - - SequentialAnimation { -@@ -33,8 +36,7 @@ WlSessionLockSurface { - target: lockContent - properties: "implicitWidth,implicitHeight" - to: lockContent.size -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - Anim { - target: lockBg -@@ -45,30 +47,29 @@ WlSessionLockSurface { - target: content - property: "scale" - to: 0 -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - Anim { - target: content - property: "opacity" - to: 0 -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - Anim { - target: lockIcon - property: "opacity" - to: 1 -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - Anim { - target: background - property: "opacity" - to: 0 -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - SequentialAnimation { - PauseAnimation { -- duration: Appearance.anim.durations.small -+ duration: Tokens.anim.durations.small - } - Anim { - target: lockContent -@@ -93,7 +94,7 @@ WlSessionLockSurface { - target: background - property: "opacity" - to: 1 -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - SequentialAnimation { - ParallelAnimation { -@@ -101,15 +102,14 @@ WlSessionLockSurface { - target: lockContent - property: "scale" - to: 1 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - Anim { - target: lockContent - property: "rotation" - to: 360 -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.standardAccel -+ duration: Tokens.anim.durations.expressiveFastSpatial -+ easing: Tokens.anim.standardAccel - } - } - ParallelAnimation { -@@ -117,7 +117,7 @@ WlSessionLockSurface { - target: lockIcon - property: "rotation" - to: 360 -- easing.bezierCurve: Appearance.anim.curves.standardDecel -+ easing: Tokens.anim.standardDecel - } - Anim { - target: lockIcon -@@ -133,27 +133,24 @@ WlSessionLockSurface { - target: content - property: "scale" - to: 1 -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - Anim { - target: lockBg - property: "radius" -- to: Appearance.rounding.large * 1.5 -+ to: lockContent.Tokens.rounding.large * 1.5 - } - Anim { - target: lockContent - property: "implicitWidth" -- to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult * lockContent.Tokens.sizes.lock.ratio -+ type: Anim.DefaultSpatial - } - Anim { - target: lockContent - property: "implicitHeight" -- to: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ to: (root.screen?.height ?? 0) * lockContent.Tokens.sizes.lock.heightMult -+ type: Anim.DefaultSpatial - } - } - } -@@ -179,8 +176,8 @@ WlSessionLockSurface { - Item { - id: lockContent - -- readonly property int size: lockIcon.implicitHeight + Appearance.padding.large * 4 -- readonly property int radius: size / 4 * Appearance.rounding.scale -+ readonly property int size: lockIcon.implicitHeight + Tokens.padding.large * 4 -+ readonly property int radius: size / 4 * Tokens.rounding.scale - - anchors.centerIn: parent - implicitWidth: size -@@ -210,7 +207,7 @@ WlSessionLockSurface { - - anchors.centerIn: parent - text: "lock" -- font.pointSize: Appearance.font.size.extraLarge * 4 -+ font.pointSize: Tokens.font.size.extraLarge * 4 - font.bold: true - rotation: 180 - } -@@ -219,8 +216,8 @@ WlSessionLockSurface { - id: content - - anchors.centerIn: parent -- width: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult * Config.lock.sizes.ratio - Appearance.padding.large * 2 -- height: (root.screen?.height ?? 0) * Config.lock.sizes.heightMult - Appearance.padding.large * 2 -+ width: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult * Tokens.sizes.lock.ratio - Tokens.padding.large * 2 -+ height: (root.screen?.height ?? 0) * Tokens.sizes.lock.heightMult - Tokens.padding.large * 2 - - lock: root - opacity: 0 -diff --git a/modules/lock/Media.qml b/modules/lock/Media.qml -index b7e58bbc..c55333c7 100644 ---- a/modules/lock/Media.qml -+++ b/modules/lock/Media.qml -@@ -1,11 +1,12 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - Item { - id: root -@@ -18,12 +19,14 @@ Item { - - Image { - anchors.fill: parent -- source: Players.active?.trackArtUrl ?? "" -+ source: Players.getArtUrl(Players.active) - - asynchronous: true - fillMode: Image.PreserveAspectCrop -- sourceSize.width: width -- sourceSize.height: height -+ sourceSize: { -+ const dpr = (QsWindow.window as QsWindow)?.devicePixelRatio ?? 1; -+ return Qt.size(width * dpr, height * dpr); -+ } - - layer.enabled: true - layer.effect: OpacityMask { -@@ -34,7 +37,7 @@ Item { - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.extraLarge -+ type: Anim.StandardExtraLarge - } - } - } -@@ -69,14 +72,14 @@ Item { - - anchors.left: parent.left - anchors.right: parent.right -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - - StyledText { -- Layout.topMargin: Appearance.padding.large -- Layout.bottomMargin: Appearance.spacing.larger -+ Layout.topMargin: Tokens.padding.large -+ Layout.bottomMargin: Tokens.spacing.larger - text: qsTr("Now playing") - color: Colours.palette.m3onSurfaceVariant -- font.family: Appearance.font.family.mono -+ font.family: Tokens.font.family.mono - font.weight: 500 - } - -@@ -86,8 +89,8 @@ Item { - text: Players.active?.trackArtist ?? qsTr("No media") - color: Colours.palette.m3primary - horizontalAlignment: Text.AlignHCenter -- font.pointSize: Appearance.font.size.large -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.large -+ font.family: Tokens.font.family.mono - font.weight: 600 - elide: Text.ElideRight - } -@@ -97,22 +100,21 @@ Item { - animate: true - text: Players.active?.trackTitle ?? qsTr("No media") - horizontalAlignment: Text.AlignHCenter -- font.pointSize: Appearance.font.size.larger -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.larger -+ font.family: Tokens.font.family.mono - elide: Text.ElideRight - } - - RowLayout { - Layout.alignment: Qt.AlignHCenter -- Layout.topMargin: Appearance.spacing.large * 1.2 -- Layout.bottomMargin: Appearance.padding.large -+ Layout.topMargin: Tokens.spacing.large * 1.2 -+ Layout.bottomMargin: Tokens.padding.large - -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - PlayerControl { - icon: "skip_previous" -- -- function onClicked(): void { -+ onClicked: { - if (Players.active?.canGoPrevious) - Players.active.previous(); - } -@@ -124,8 +126,7 @@ Item { - colour: "Primary" - level: active ? 2 : 1 - active: Players.active?.isPlaying ?? false -- -- function onClicked(): void { -+ onClicked: { - if (Players.active?.canTogglePlaying) - Players.active.togglePlaying(); - } -@@ -133,8 +134,7 @@ Item { - - PlayerControl { - icon: "skip_next" -- -- function onClicked(): void { -+ onClicked: { - if (Players.active?.canGoNext) - Players.active.next(); - } -@@ -151,15 +151,14 @@ Item { - property string colour: "Secondary" - property int level: 1 - -- function onClicked(): void { -- } -+ signal clicked - -- Layout.preferredWidth: implicitWidth + (controlState.pressed ? Appearance.padding.normal * 2 : active ? Appearance.padding.small * 2 : 0) -- implicitWidth: controlIcon.implicitWidth + Appearance.padding.large * 2 -- implicitHeight: controlIcon.implicitHeight + Appearance.padding.normal * 2 -+ Layout.preferredWidth: implicitWidth + (controlState.pressed ? Tokens.padding.normal * 2 : active ? Tokens.padding.small * 2 : 0) -+ implicitWidth: controlIcon.implicitWidth + Tokens.padding.large * 2 -+ implicitHeight: controlIcon.implicitHeight + Tokens.padding.normal * 2 - - color: active ? Colours.palette[`m3${colour.toLowerCase()}`] : Colours.palette[`m3${colour.toLowerCase()}Container`] -- radius: active || controlState.pressed ? Appearance.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Appearance.rounding.scale) -+ radius: active || controlState.pressed ? Tokens.rounding.normal : Math.min(implicitWidth, implicitHeight) / 2 * Math.min(1, Tokens.rounding.scale) - - Elevation { - anchors.fill: parent -@@ -172,10 +171,7 @@ Item { - id: controlState - - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] -- -- function onClicked(): void { -- control.onClicked(); -- } -+ onClicked: control.clicked() - } - - MaterialIcon { -@@ -183,7 +179,7 @@ Item { - - anchors.centerIn: parent - color: control.active ? Colours.palette[`m3on${control.colour}`] : Colours.palette[`m3on${control.colour}Container`] -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - fill: control.active ? 1 : 0 - - Behavior on fill { -@@ -193,15 +189,13 @@ Item { - - Behavior on Layout.preferredWidth { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - Behavior on radius { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -diff --git a/modules/lock/NotifDock.qml b/modules/lock/NotifDock.qml -index 01f7e4b4..2254dfe0 100644 ---- a/modules/lock/NotifDock.qml -+++ b/modules/lock/NotifDock.qml -@@ -1,14 +1,15 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components - import qs.components.containers - import qs.components.effects - import qs.services --import qs.config --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts -+import qs.utils - - ColumnLayout { - id: root -@@ -16,15 +17,15 @@ ColumnLayout { - required property var lock - - anchors.fill: parent -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: Notifs.list.length > 0 ? qsTr("%1 notification%2").arg(Notifs.list.length).arg(Notifs.list.length === 1 ? "" : "s") : qsTr("Notifications") - color: Colours.palette.m3outline -- font.family: Appearance.font.family.mono -+ font.family: Tokens.font.family.mono - font.weight: 500 - elide: Text.ElideRight - } -@@ -35,22 +36,23 @@ ColumnLayout { - Layout.fillWidth: true - Layout.fillHeight: true - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: "transparent" - - Loader { -+ asynchronous: true - anchors.centerIn: parent - active: opacity > 0 -- opacity: Notifs.list.length > 0 ? 0 : 1 -+ opacity: Notifs.list.length > 0 && !Config.lock.hideNotifs ? 0 : 1 - - sourceComponent: ColumnLayout { -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - Image { - asynchronous: true -- source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) -+ source: Paths.absolutePath(Config.paths.lockNoNotifsPic) - fillMode: Image.PreserveAspectFit -- sourceSize.width: clipRect.width * 0.8 -+ sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) - - layer.enabled: true - layer.effect: Colouriser { -@@ -61,25 +63,25 @@ ColumnLayout { - - StyledText { - Layout.alignment: Qt.AlignHCenter -- text: qsTr("No Notifications") -+ text: Config.lock.hideNotifs ? qsTr("Unlock for Notifications") : qsTr("No Notifications") - color: Colours.palette.m3outlineVariant -- font.pointSize: Appearance.font.size.large -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.large -+ font.family: Tokens.font.family.mono - font.weight: 500 - } - } - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.extraLarge -+ type: Anim.StandardExtraLarge - } - } - } - - StyledListView { - anchors.fill: parent -- -- spacing: Appearance.spacing.small -+ visible: !Config.lock.hideNotifs -+ spacing: Tokens.spacing.small - clip: true - - model: ScriptModel { -@@ -101,8 +103,7 @@ ColumnLayout { - property: "scale" - from: 0 - to: 1 -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - -@@ -124,8 +125,7 @@ ColumnLayout { - } - Anim { - property: "y" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - -@@ -136,8 +136,7 @@ ColumnLayout { - } - Anim { - property: "y" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -diff --git a/modules/lock/NotifGroup.qml b/modules/lock/NotifGroup.qml -index 77960906..971b921a 100644 ---- a/modules/lock/NotifGroup.qml -+++ b/modules/lock/NotifGroup.qml -@@ -1,15 +1,15 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Quickshell.Services.Notifications -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services --import qs.config - import qs.utils --import Quickshell --import Quickshell.Widgets --import Quickshell.Services.Notifications --import QtQuick --import QtQuick.Layouts - - StyledRect { - id: root -@@ -17,18 +17,39 @@ StyledRect { - required property string modelData - - readonly property list notifs: Notifs.list.filter(notif => notif.appName === modelData) -- readonly property string image: notifs.find(n => n.image.length > 0)?.image ?? "" -- readonly property string appIcon: notifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" -- readonly property string urgency: notifs.some(n => n.urgency === NotificationUrgency.Critical) ? "critical" : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? "normal" : "low" -+ readonly property var props: { -+ let img = ""; -+ let icon = ""; -+ let hasCritical = false; -+ let hasNormal = false; -+ for (const n of notifs) { -+ if (!img && n.image.length > 0) -+ img = n.image; -+ if (!icon && n.appIcon.length > 0) -+ icon = n.appIcon; -+ if (n.urgency === NotificationUrgency.Critical) -+ hasCritical = true; -+ else if (n.urgency === NotificationUrgency.Normal) -+ hasNormal = true; -+ } -+ return { -+ img, -+ icon, -+ urgency: hasCritical ? "critical" : hasNormal ? "normal" : "low" -+ }; -+ } -+ readonly property string image: props.img -+ readonly property string appIcon: props.icon -+ readonly property string urgency: props.urgency - - property bool expanded - - anchors.left: parent?.left - anchors.right: parent?.right -- implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: content.implicitHeight + Tokens.padding.normal * 2 - - clip: true -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: root.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - - RowLayout { -@@ -37,14 +58,14 @@ StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Item { - Layout.alignment: Qt.AlignLeft | Qt.AlignTop -- implicitWidth: Config.notifs.sizes.image -- implicitHeight: Config.notifs.sizes.image -+ implicitWidth: TokenConfig.sizes.notifs.image -+ implicitHeight: TokenConfig.sizes.notifs.image - - Component { - id: imageComp -@@ -52,10 +73,14 @@ StyledRect { - Image { - source: Qt.resolvedUrl(root.image) - fillMode: Image.PreserveAspectCrop -+ sourceSize: { -+ const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); -+ return Qt.size(size, size); -+ } - cache: false - asynchronous: true -- width: Config.notifs.sizes.image -- height: Config.notifs.sizes.image -+ width: TokenConfig.sizes.notifs.image -+ height: TokenConfig.sizes.notifs.image - } - } - -@@ -63,7 +88,7 @@ StyledRect { - id: appIconComp - - ColouredIcon { -- implicitSize: Math.round(Config.notifs.sizes.image * 0.6) -+ implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) - source: Quickshell.iconPath(root.appIcon) - colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - layer.enabled: root.appIcon.endsWith("symbolic") -@@ -76,36 +101,38 @@ StyledRect { - MaterialIcon { - text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) - color: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - - ClippingRectangle { - anchors.fill: parent - color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) : Colours.palette.m3secondaryContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - Loader { -+ asynchronous: true - anchors.centerIn: parent - sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp - } - } - - Loader { -+ asynchronous: true - anchors.right: parent.right - anchors.bottom: parent.bottom - active: root.appIcon && root.image - - sourceComponent: StyledRect { -- implicitWidth: Config.notifs.sizes.badge -- implicitHeight: Config.notifs.sizes.badge -+ implicitWidth: Tokens.sizes.notifs.badge -+ implicitHeight: Tokens.sizes.notifs.badge - - color: root.urgency === "critical" ? Colours.palette.m3error : root.urgency === "low" ? Colours.palette.m3surfaceContainerHighest : Colours.palette.m3secondaryContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - ColouredIcon { - anchors.centerIn: parent -- implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) -+ implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) - source: Quickshell.iconPath(root.appIcon) - colour: root.urgency === "critical" ? Colours.palette.m3onError : root.urgency === "low" ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - layer.enabled: root.appIcon.endsWith("symbolic") -@@ -115,21 +142,21 @@ StyledRect { - } - - ColumnLayout { -- Layout.topMargin: -Appearance.padding.small -- Layout.bottomMargin: -Appearance.padding.small / 2 - (root.expanded ? 0 : spacing) -+ Layout.topMargin: -Tokens.padding.small -+ Layout.bottomMargin: -Tokens.padding.small / 2 - (root.expanded ? 0 : spacing) - Layout.fillWidth: true -- spacing: Math.round(Appearance.spacing.small / 2) -+ spacing: Math.round(Tokens.spacing.small / 2) - - RowLayout { - Layout.bottomMargin: -parent.spacing - Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: root.modelData - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - -@@ -137,45 +164,42 @@ StyledRect { - animate: true - text: root.notifs[0]?.timeStr ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledRect { -- implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 -- implicitHeight: groupCount.implicitHeight + Appearance.padding.small -+ implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 -+ implicitHeight: groupCount.implicitHeight + Tokens.padding.small - - color: root.urgency === "critical" ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - opacity: root.notifs.length > Config.notifs.groupPreviewNum ? 1 : 0 - Layout.preferredWidth: root.notifs.length > Config.notifs.groupPreviewNum ? implicitWidth : 0 - - StateLayer { - color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface -- -- function onClicked(): void { -- root.expanded = !root.expanded; -- } -+ onClicked: root.expanded = !root.expanded - } - - RowLayout { - id: expandBtn - - anchors.centerIn: parent -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { - id: groupCount - -- Layout.leftMargin: Appearance.padding.small / 2 -+ Layout.leftMargin: Tokens.padding.small / 2 - animate: true - text: root.notifs.length - color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - MaterialIcon { -- Layout.rightMargin: -Appearance.padding.small / 2 -+ Layout.rightMargin: -Tokens.padding.small / 2 - animate: true - text: root.expanded ? "expand_less" : "expand_more" - color: root.urgency === "critical" ? Colours.palette.m3onError : Colours.palette.m3onSurface -@@ -194,7 +218,7 @@ StyledRect { - - Repeater { - model: ScriptModel { -- values: root.notifs.slice(0, Config.notifs.groupPreviewNum) -+ values: root.notifs.slice(0, root.Config.notifs.groupPreviewNum) - } - - NotifLine { -@@ -247,6 +271,7 @@ StyledRect { - } - - Loader { -+ asynchronous: true - Layout.fillWidth: true - - opacity: root.expanded ? 1 : 0 -@@ -256,7 +281,7 @@ StyledRect { - sourceComponent: ColumnLayout { - Repeater { - model: ScriptModel { -- values: root.notifs.slice(Config.notifs.groupPreviewNum) -+ values: root.notifs.slice(root.Config.notifs.groupPreviewNum) - } - - NotifLine {} -@@ -272,15 +297,14 @@ StyledRect { - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - component NotifLine: StyledText { - id: notifLine - -- required property Notifs.Notif modelData -+ required property NotifData modelData - - Layout.fillWidth: true - textFormat: Text.MarkdownText -diff --git a/modules/lock/Pam.qml b/modules/lock/Pam.qml -index 0186c2f8..f4c61747 100644 ---- a/modules/lock/Pam.qml -+++ b/modules/lock/Pam.qml -@@ -1,9 +1,9 @@ --import qs.config -+import QtQuick - import Quickshell - import Quickshell.Io - import Quickshell.Wayland - import Quickshell.Services.Pam --import QtQuick -+import Caelestia.Config - - Scope { - id: root -@@ -31,8 +31,8 @@ Scope { - } else { - buffer = buffer.slice(0, -1); - } -- } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { -- // No illegal characters (you are insane if you use unicode in your password) -+ } else if (/^[^\x00-\x1F\x7F-\x9F]+$/.test(event.text)) { -+ // Allow anything except control characters - buffer += event.text; - } - } -@@ -82,7 +82,7 @@ Scope { - property int errorTries - - function checkAvail(): void { -- if (!available || !Config.lock.enableFprint || !root.lock.secure) { -+ if (!available || !GlobalConfig.lock.enableFprint || !root.lock.secure) { - abort(); - return; - } -@@ -113,7 +113,7 @@ Scope { - // Isn't actually the real max tries as pam only reports completed - // when max tries is reached. - tries++; -- if (tries < Config.lock.maxFprintTries) { -+ if (tries < GlobalConfig.lock.maxFprintTries) { - // Restart if not actually real max tries - root.fprintState = "fail"; - start(); -@@ -132,7 +132,7 @@ Scope { - id: availProc - - command: ["sh", "-c", "fprintd-list $USER"] -- onExited: code => { -+ onExited: code => { // qmllint disable signal-handler-parameters - fprint.available = code === 0; - fprint.checkAvail(); - } -@@ -166,8 +166,6 @@ Scope { - } - - Connections { -- target: root.lock -- - function onSecureChanged(): void { - if (root.lock.secure) { - availProc.running = true; -@@ -181,13 +179,15 @@ Scope { - function onUnlock(): void { - fprint.abort(); - } -+ -+ target: root.lock - } - - Connections { -- target: Config.lock -- - function onEnableFprintChanged(): void { - fprint.checkAvail(); - } -+ -+ target: GlobalConfig.lock - } - } -diff --git a/modules/lock/Resources.qml b/modules/lock/Resources.qml -index 82c004c2..1f38aa00 100644 ---- a/modules/lock/Resources.qml -+++ b/modules/lock/Resources.qml -@@ -1,20 +1,20 @@ -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.misc - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - GridLayout { - id: root - - anchors.left: parent.left - anchors.right: parent.right -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- rowSpacing: Appearance.spacing.large -- columnSpacing: Appearance.spacing.large -+ rowSpacing: Tokens.spacing.large -+ columnSpacing: Tokens.spacing.large - rows: 2 - columns: 2 - -@@ -23,28 +23,28 @@ GridLayout { - } - - Resource { -- Layout.topMargin: Appearance.padding.large -+ Layout.topMargin: Tokens.padding.large - icon: "memory" - value: SystemUsage.cpuPerc - colour: Colours.palette.m3primary - } - - Resource { -- Layout.topMargin: Appearance.padding.large -+ Layout.topMargin: Tokens.padding.large - icon: "thermostat" - value: Math.min(1, SystemUsage.cpuTemp / 90) - colour: Colours.palette.m3secondary - } - - Resource { -- Layout.bottomMargin: Appearance.padding.large -+ Layout.bottomMargin: Tokens.padding.large - icon: "memory_alt" - value: SystemUsage.memPerc - colour: Colours.palette.m3secondary - } - - Resource { -- Layout.bottomMargin: Appearance.padding.large -+ Layout.bottomMargin: Tokens.padding.large - icon: "hard_disk" - value: SystemUsage.storagePerc - colour: Colours.palette.m3tertiary -@@ -61,17 +61,17 @@ GridLayout { - implicitHeight: width - - color: Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - - CircularProgress { - id: circ - - anchors.fill: parent - value: res.value -- padding: Appearance.padding.large * 3 -+ padding: Tokens.padding.large * 3 - fgColour: res.colour - bgColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 3) -- strokeWidth: width < 200 ? Appearance.padding.smaller : Appearance.padding.normal -+ strokeWidth: width < 200 ? Tokens.padding.smaller : Tokens.padding.normal - } - - MaterialIcon { -@@ -86,7 +86,7 @@ GridLayout { - - Behavior on value { - Anim { -- duration: Appearance.anim.durations.large -+ type: Anim.StandardLarge - } - } - } -diff --git a/modules/lock/WeatherInfo.qml b/modules/lock/WeatherInfo.qml -index d6c25af2..d96a54df 100644 ---- a/modules/lock/WeatherInfo.qml -+++ b/modules/lock/WeatherInfo.qml -@@ -1,11 +1,10 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import qs.utils - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root -@@ -14,13 +13,14 @@ ColumnLayout { - - anchors.left: parent.left - anchors.right: parent.right -- anchors.margins: Appearance.padding.large * 2 -+ anchors.margins: Tokens.padding.large * 2 - -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Loader { -- Layout.topMargin: Appearance.padding.large * 2 -- Layout.bottomMargin: -Appearance.padding.large -+ asynchronous: true -+ Layout.topMargin: Tokens.padding.large * 2 -+ Layout.bottomMargin: -Tokens.padding.large - Layout.alignment: Qt.AlignHCenter - - active: root.rootHeight > 610 -@@ -29,24 +29,24 @@ ColumnLayout { - sourceComponent: StyledText { - text: qsTr("Weather") - color: Colours.palette.m3primary -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } - } - - RowLayout { - Layout.fillWidth: true -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - MaterialIcon { - animate: true - text: Weather.icon - color: Colours.palette.m3secondary -- font.pointSize: Appearance.font.size.extraLarge * 2.5 -+ font.pointSize: Tokens.font.size.extraLarge * 2.5 - } - - ColumnLayout { -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true -@@ -54,7 +54,7 @@ ColumnLayout { - animate: true - text: Weather.description - color: Colours.palette.m3secondary -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - elide: Text.ElideRight - } -@@ -65,18 +65,19 @@ ColumnLayout { - animate: true - text: qsTr("Humidity: %1%").arg(Weather.humidity) - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - } - - Loader { -- Layout.rightMargin: Appearance.padding.smaller -+ asynchronous: true -+ Layout.rightMargin: Tokens.padding.smaller - active: root.width > 400 - visible: active - - sourceComponent: ColumnLayout { -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true -@@ -85,7 +86,7 @@ ColumnLayout { - text: Weather.temp - color: Colours.palette.m3primary - horizontalAlignment: Text.AlignRight -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - elide: Text.ElideLeft - } -@@ -97,7 +98,7 @@ ColumnLayout { - text: qsTr("Feels like: %1").arg(Weather.feelsLike) - color: Colours.palette.m3outline - horizontalAlignment: Text.AlignRight -- font.pointSize: Appearance.font.size.smaller -+ font.pointSize: Tokens.font.size.smaller - elide: Text.ElideLeft - } - } -@@ -107,15 +108,16 @@ ColumnLayout { - Loader { - id: forecastLoader - -- Layout.topMargin: Appearance.spacing.smaller -- Layout.bottomMargin: Appearance.padding.large * 2 -+ asynchronous: true -+ Layout.topMargin: Tokens.spacing.smaller -+ Layout.bottomMargin: Tokens.padding.large * 2 - Layout.fillWidth: true - - active: root.rootHeight > 820 - visible: active - - sourceComponent: RowLayout { -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - Repeater { - model: { -@@ -135,7 +137,7 @@ ColumnLayout { - required property var modelData - - Layout.fillWidth: true -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - StyledText { - Layout.fillWidth: true -@@ -145,21 +147,21 @@ ColumnLayout { - } - color: Colours.palette.m3outline - horizontalAlignment: Text.AlignHCenter -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - } - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: forecastHour.modelData?.icon ?? "cloud_alert" -- font.pointSize: Appearance.font.size.extraLarge * 1.5 -+ font.pointSize: Tokens.font.size.extraLarge * 1.5 - font.weight: 500 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter -- text: Config.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` -+ text: GlobalConfig.services.useFahrenheit ? `${forecastHour.modelData?.tempF ?? 0}°F` : `${forecastHour.modelData?.tempC ?? 0}°C` - color: Colours.palette.m3secondary -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - } - } - } -diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml -deleted file mode 100644 -index a44cb19b..00000000 ---- a/modules/notifications/Background.qml -+++ /dev/null -@@ -1,54 +0,0 @@ --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Shapes -- --ShapePath { -- id: root -- -- required property Wrapper wrapper -- required property var sidebar -- readonly property real rounding: Config.border.rounding -- readonly property bool flatten: wrapper.height < rounding * 2 -- readonly property real roundingY: flatten ? wrapper.height / 2 : rounding -- -- strokeWidth: -1 -- fillColor: Colours.palette.m3surface -- -- PathLine { -- relativeX: -(root.wrapper.width + root.rounding) -- relativeY: 0 -- } -- PathArc { -- relativeX: root.rounding -- relativeY: root.roundingY -- radiusX: root.rounding -- radiusY: Math.min(root.rounding, root.wrapper.height) -- } -- PathLine { -- relativeX: 0 -- relativeY: root.wrapper.height - root.roundingY * 2 -- } -- PathArc { -- relativeX: root.sidebar.notifsRoundingX -- relativeY: root.roundingY -- radiusX: root.sidebar.notifsRoundingX -- radiusY: Math.min(root.rounding, root.wrapper.height) -- direction: PathArc.Counterclockwise -- } -- PathLine { -- relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width -- relativeY: 0 -- } -- PathArc { -- relativeX: root.rounding -- relativeY: root.rounding -- radiusX: root.rounding -- radiusY: root.rounding -- } -- -- Behavior on fillColor { -- CAnim {} -- } --} -diff --git a/modules/notifications/Content.qml b/modules/notifications/Content.qml -index 2d4590e0..aeaa7dea 100644 ---- a/modules/notifications/Content.qml -+++ b/modules/notifications/Content.qml -@@ -1,47 +1,47 @@ -+import QtQuick -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components - import qs.components.containers - import qs.components.widgets - import qs.services --import qs.config --import Quickshell --import Quickshell.Widgets --import QtQuick - - Item { - id: root - -- required property PersistentProperties visibilities -- required property Item panels -- readonly property int padding: Appearance.padding.large -+ required property DrawerVisibilities visibilities -+ required property Item osdPanel -+ required property Item sessionPanel -+ readonly property int padding: Tokens.padding.large - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.right: parent.right - -- implicitWidth: Config.notifs.sizes.width + padding * 2 -+ implicitWidth: Tokens.sizes.notifs.width + padding * 2 - implicitHeight: { - const count = list.count; - if (count === 0) - return 0; - -- let height = (count - 1) * Appearance.spacing.smaller; -+ let height = (count - 1) * Tokens.spacing.smaller; - for (let i = 0; i < count; i++) -- height += list.itemAtIndex(i)?.nonAnimHeight ?? 0; -+ height += (list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0; - -- if (visibilities && panels) { -- if (visibilities.osd) { -- const h = panels.osd.y - Config.border.rounding * 2 - padding * 2; -- if (height > h) -- height = h; -- } -+ if (visibilities.osd) { -+ const h = osdPanel.y - Config.border.rounding * 2 - padding * 2; -+ if (height > h) -+ height = h; -+ } - -- if (visibilities.session) { -- const h = panels.session.y - Config.border.rounding * 2 - padding * 2; -- if (height > h) -- height = h; -- } -+ if (visibilities.session) { -+ const h = sessionPanel.y - Config.border.rounding * 2 - padding * 2; -+ if (height > h) -+ height = h; - } - -- return Math.min((QsWindow.window?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); -+ return Math.min(((QsWindow.window as QsWindow)?.screen?.height ?? 0) - Config.border.thickness * 2, height + padding * 2); - } - - ClippingWrapperRectangle { -@@ -49,7 +49,7 @@ Item { - anchors.margins: root.padding - - color: "transparent" -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - StyledListView { - id: list -@@ -62,79 +62,9 @@ Item { - - orientation: Qt.Vertical - spacing: 0 -- cacheBuffer: QsWindow.window?.screen.height ?? 0 -- -- delegate: Item { -- id: wrapper -- -- required property Notifs.Notif modelData -- required property int index -- readonly property alias nonAnimHeight: notif.nonAnimHeight -- property int idx -- -- onIndexChanged: { -- if (index !== -1) -- idx = index; -- } -- -- implicitWidth: notif.implicitWidth -- implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Appearance.spacing.smaller) -- -- ListView.onRemove: removeAnim.start() -- -- SequentialAnimation { -- id: removeAnim -- -- PropertyAction { -- target: wrapper -- property: "ListView.delayRemove" -- value: true -- } -- PropertyAction { -- target: wrapper -- property: "enabled" -- value: false -- } -- PropertyAction { -- target: wrapper -- property: "implicitHeight" -- value: 0 -- } -- PropertyAction { -- target: wrapper -- property: "z" -- value: 1 -- } -- Anim { -- target: notif -- property: "x" -- to: (notif.x >= 0 ? Config.notifs.sizes.width : -Config.notifs.sizes.width) * 2 -- duration: Appearance.anim.durations.normal -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- PropertyAction { -- target: wrapper -- property: "ListView.delayRemove" -- value: false -- } -- } -- -- ClippingRectangle { -- anchors.top: parent.top -- anchors.topMargin: wrapper.idx === 0 ? 0 : Appearance.spacing.smaller -- -- color: "transparent" -- radius: notif.radius -- implicitWidth: notif.implicitWidth -- implicitHeight: notif.implicitHeight -+ cacheBuffer: (QsWindow.window as QsWindow)?.screen.height ?? 0 - -- Notification { -- id: notif -- -- modelData: wrapper.modelData -- } -- } -- } -+ delegate: NotifWrapper {} - - move: Transition { - Anim { -@@ -159,9 +89,9 @@ Item { - - let height = 0; - for (let i = 0; i < count; i++) { -- height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; -+ height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; - -- if (height - Appearance.spacing.smaller >= scrollY) -+ if (height - Tokens.spacing.smaller >= scrollY) - return i; - } - -@@ -180,9 +110,9 @@ Item { - - let height = 0; - for (let i = count - 1; i >= 0; i--) { -- height += (list.itemAtIndex(i)?.nonAnimHeight ?? 0) + Appearance.spacing.smaller; -+ height += ((list.itemAtIndex(i) as NotifWrapper)?.nonAnimHeight ?? 0) + Tokens.spacing.smaller; - -- if (height - Appearance.spacing.smaller >= scrollY) -+ if (height - Tokens.spacing.smaller >= scrollY) - return count - i - 1; - } - -@@ -196,9 +126,80 @@ Item { - Anim {} - } - -+ component NotifWrapper: Item { -+ id: wrapper -+ -+ required property NotifData modelData -+ required property int index -+ readonly property alias nonAnimHeight: notif.nonAnimHeight -+ property int idx -+ -+ onIndexChanged: { -+ if (index !== -1) -+ idx = index; -+ } -+ -+ implicitWidth: notif.implicitWidth -+ implicitHeight: notif.implicitHeight + (idx === 0 ? 0 : Tokens.spacing.smaller) -+ -+ ListView.onRemove: removeAnim.start() -+ -+ SequentialAnimation { -+ id: removeAnim -+ -+ PropertyAction { -+ target: wrapper -+ property: "ListView.delayRemove" -+ value: true -+ } -+ PropertyAction { -+ target: wrapper -+ property: "enabled" -+ value: false -+ } -+ PropertyAction { -+ target: wrapper -+ property: "implicitHeight" -+ value: 0 -+ } -+ PropertyAction { -+ target: wrapper -+ property: "z" -+ value: 1 -+ } -+ Anim { -+ target: notif -+ property: "x" -+ to: (notif.x >= 0 ? wrapper.Tokens.sizes.notifs.width : -wrapper.Tokens.sizes.notifs.width) * 2 -+ duration: Tokens.anim.durations.normal -+ easing: Tokens.anim.emphasized -+ } -+ PropertyAction { -+ target: wrapper -+ property: "ListView.delayRemove" -+ value: false -+ } -+ } -+ -+ ClippingRectangle { -+ anchors.top: parent.top -+ anchors.topMargin: wrapper.idx === 0 ? 0 : Tokens.spacing.smaller -+ -+ color: "transparent" -+ radius: notif.radius -+ implicitWidth: notif.implicitWidth -+ implicitHeight: notif.implicitHeight -+ -+ Notification { -+ id: notif -+ -+ modelData: wrapper.modelData -+ } -+ } -+ } -+ - component Anim: NumberAnimation { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ duration: Tokens.anim.durations.expressiveDefaultSpatial -+ easing: Tokens.anim.expressiveDefaultSpatial - } - } -diff --git a/modules/notifications/Notification.qml b/modules/notifications/Notification.qml -index 8c2d3ec2..fe6fd056 100644 ---- a/modules/notifications/Notification.qml -+++ b/modules/notifications/Notification.qml -@@ -1,31 +1,32 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import QtQuick.Shapes -+import Quickshell -+import Quickshell.Services.Notifications -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services --import qs.config - import qs.utils --import Quickshell --import Quickshell.Widgets --import Quickshell.Services.Notifications --import QtQuick --import QtQuick.Layouts - - StyledRect { - id: root - -- required property Notifs.Notif modelData -+ required property NotifData modelData - readonly property bool hasImage: modelData.image.length > 0 - readonly property bool hasAppIcon: modelData.appIcon.length > 0 -+ readonly property int bodyTextFormat: /[<*_`#\[\]]/.test(modelData.body) ? Text.MarkdownText : Text.PlainText - readonly property int nonAnimHeight: summary.implicitHeight + (root.expanded ? appName.height + body.height + actions.height + actions.anchors.topMargin : bodyPreview.height) + inner.anchors.margins * 2 - property bool expanded: Config.notifs.openExpanded - - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.normal -- implicitWidth: Config.notifs.sizes.width -+ radius: Tokens.rounding.normal -+ implicitWidth: Tokens.sizes.notifs.width - implicitHeight: inner.implicitHeight - -- x: Config.notifs.sizes.width -+ x: Tokens.sizes.notifs.width - Component.onCompleted: { - x = 0; - modelData.lock(this); -@@ -34,7 +35,7 @@ StyledRect { - - Behavior on x { - Anim { -- easing.bezierCurve: Appearance.anim.curves.emphasizedDecel -+ easing: Tokens.anim.emphasizedDecel - } - } - -@@ -66,7 +67,7 @@ StyledRect { - if (!containsMouse) - root.modelData.timer.start(); - -- if (Math.abs(root.x) < Config.notifs.sizes.width * Config.notifs.clearThreshold) -+ if (Math.abs(root.x) < Tokens.sizes.notifs.width * Config.notifs.clearThreshold) - root.x = 0; - else - root.modelData.popup = false; -@@ -79,11 +80,11 @@ StyledRect { - } - } - onClicked: event => { -- if (!Config.notifs.actionOnClick || event.button !== Qt.LeftButton) -+ if (!GlobalConfig.notifs.actionOnClick || event.button !== Qt.LeftButton) - return; - - const actions = root.modelData.actions; -- if (actions?.length === 1) -+ if (actions.length === 1) - actions[0].invoke(); - } - -@@ -93,37 +94,42 @@ StyledRect { - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - implicitHeight: root.nonAnimHeight - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - Loader { - id: image - -+ asynchronous: true - active: root.hasImage - - anchors.left: parent.left - anchors.top: parent.top -- width: Config.notifs.sizes.image -- height: Config.notifs.sizes.image -+ width: TokenConfig.sizes.notifs.image -+ height: TokenConfig.sizes.notifs.image - visible: root.hasImage || root.hasAppIcon - -- sourceComponent: ClippingRectangle { -- radius: Appearance.rounding.full -- implicitWidth: Config.notifs.sizes.image -- implicitHeight: Config.notifs.sizes.image -+ sourceComponent: StyledClippingRect { -+ radius: Tokens.rounding.full -+ color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer -+ implicitWidth: TokenConfig.sizes.notifs.image -+ implicitHeight: TokenConfig.sizes.notifs.image - - Image { - anchors.fill: parent - source: Qt.resolvedUrl(root.modelData.image) - fillMode: Image.PreserveAspectCrop -+ sourceSize: { -+ const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); -+ return Qt.size(size, size); -+ } - cache: false - asynchronous: true - } -@@ -133,6 +139,7 @@ StyledRect { - Loader { - id: appIcon - -+ asynchronous: true - active: root.hasAppIcon || !root.hasImage - - anchors.horizontalCenter: root.hasImage ? undefined : image.horizontalCenter -@@ -141,14 +148,15 @@ StyledRect { - anchors.bottom: root.hasImage ? image.bottom : undefined - - sourceComponent: StyledRect { -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.modelData.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) : Colours.palette.m3secondaryContainer -- implicitWidth: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image -- implicitHeight: root.hasImage ? Config.notifs.sizes.badge : Config.notifs.sizes.image -+ implicitWidth: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image -+ implicitHeight: root.hasImage ? Tokens.sizes.notifs.badge : TokenConfig.sizes.notifs.image - - Loader { - id: icon - -+ asynchronous: true - active: root.hasAppIcon - - anchors.centerIn: parent -@@ -165,16 +173,53 @@ StyledRect { - } - - Loader { -+ asynchronous: true - active: !root.hasAppIcon - anchors.centerIn: parent -- anchors.horizontalCenterOffset: -Appearance.font.size.large * 0.02 -- anchors.verticalCenterOffset: Appearance.font.size.large * 0.02 -+ anchors.horizontalCenterOffset: -Tokens.font.size.large * 0.02 -+ anchors.verticalCenterOffset: Tokens.font.size.large * 0.02 - - sourceComponent: MaterialIcon { - text: Icons.getNotifIcon(root.modelData.summary, root.modelData.urgency) - - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.modelData.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large -+ } -+ } -+ } -+ } -+ -+ Shape { -+ id: progressIndicator -+ -+ anchors.centerIn: appIcon -+ width: appIcon.implicitWidth + progressShape.strokeWidth * 2 -+ height: appIcon.implicitHeight + progressShape.strokeWidth * 2 -+ preferredRendererType: Shape.CurveRenderer -+ -+ ShapePath { -+ id: progressShape -+ -+ capStyle: ShapePath.RoundCap -+ fillColor: "transparent" -+ strokeWidth: 2 -+ strokeColor: Colours.palette.m3primary -+ -+ PathAngleArc { -+ id: progressArc -+ -+ radiusX: progressIndicator.width / 2 - root.Tokens.padding.small / 2 -+ centerX: progressIndicator.width / 2 -+ radiusY: progressIndicator.height / 2 - root.Tokens.padding.small / 2 -+ centerY: progressIndicator.height / 2 -+ -+ startAngle: -90 -+ sweepAngle: ((root.modelData.hints.value ?? 0) / 100) * 360 -+ -+ Behavior on sweepAngle { -+ Anim { -+ easing: Tokens.anim.emphasizedDecel -+ } - } - } - } -@@ -185,13 +230,13 @@ StyledRect { - - anchors.top: parent.top - anchors.left: image.right -- anchors.leftMargin: Appearance.spacing.smaller -+ anchors.leftMargin: Tokens.spacing.smaller - - animate: true - text: appNameMetrics.elidedText - maximumLineCount: 1 - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - - opacity: root.expanded ? 1 : 0 - -@@ -207,7 +252,7 @@ StyledRect { - font.family: appName.font.family - font.pointSize: appName.font.pointSize - elide: Text.ElideRight -- elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 -+ elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 - } - - StyledText { -@@ -215,7 +260,7 @@ StyledRect { - - anchors.top: parent.top - anchors.left: image.right -- anchors.leftMargin: Appearance.spacing.smaller -+ anchors.leftMargin: Tokens.spacing.smaller - - animate: true - text: summaryMetrics.elidedText -@@ -241,10 +286,8 @@ StyledRect { - target: summary - property: "maximumLineCount" - } -- AnchorAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard -+ AnchorAnim { -+ type: AnchorAnim.Standard - } - } - -@@ -260,7 +303,7 @@ StyledRect { - font.family: summary.font.family - font.pointSize: summary.font.pointSize - elide: Text.ElideRight -- elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - Appearance.spacing.small * 3 -+ elideWidth: expandBtn.x - time.width - timeSep.width - summary.x - root.Tokens.spacing.small * 3 - } - - StyledText { -@@ -268,11 +311,11 @@ StyledRect { - - anchors.top: parent.top - anchors.left: summary.right -- anchors.leftMargin: Appearance.spacing.small -+ anchors.leftMargin: Tokens.spacing.small - - text: "•" - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - - states: State { - name: "expanded" -@@ -285,10 +328,8 @@ StyledRect { - } - - transitions: Transition { -- AnchorAnimation { -- duration: Appearance.anim.durations.normal -- easing.type: Easing.BezierSpline -- easing.bezierCurve: Appearance.anim.curves.standard -+ AnchorAnim { -+ type: AnchorAnim.Standard - } - } - } -@@ -298,13 +339,13 @@ StyledRect { - - anchors.top: parent.top - anchors.left: timeSep.right -- anchors.leftMargin: Appearance.spacing.small -+ anchors.leftMargin: Tokens.spacing.small - - animate: true - horizontalAlignment: Text.AlignLeft - text: root.modelData.timeStr - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - Item { -@@ -317,12 +358,9 @@ StyledRect { - implicitHeight: expandIcon.height - - StateLayer { -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- -- function onClicked() { -- root.expanded = !root.expanded; -- } -+ onClicked: root.expanded = !root.expanded - } - - MaterialIcon { -@@ -332,7 +370,7 @@ StyledRect { - - animate: true - text: root.expanded ? "expand_less" : "expand_more" -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - } - -@@ -342,13 +380,13 @@ StyledRect { - anchors.left: summary.left - anchors.right: expandBtn.left - anchors.top: summary.bottom -- anchors.rightMargin: Appearance.spacing.small -+ anchors.rightMargin: Tokens.spacing.small - - animate: true -- textFormat: Text.MarkdownText -+ textFormat: root.bodyTextFormat - text: bodyPreviewMetrics.elidedText - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - - opacity: root.expanded ? 0 : 1 - -@@ -373,13 +411,13 @@ StyledRect { - anchors.left: summary.left - anchors.right: expandBtn.left - anchors.top: summary.bottom -- anchors.rightMargin: Appearance.spacing.small -+ anchors.rightMargin: Tokens.spacing.small - - animate: true -- textFormat: Text.MarkdownText -+ textFormat: root.bodyTextFormat - text: root.modelData.body - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - height: text ? implicitHeight : 0 - -@@ -403,9 +441,9 @@ StyledRect { - - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: body.bottom -- anchors.topMargin: Appearance.spacing.small -+ anchors.topMargin: Tokens.spacing.small - -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - opacity: root.expanded ? 1 : 0 - -@@ -416,6 +454,7 @@ StyledRect { - Action { - modelData: QtObject { - readonly property string text: qsTr("Close") -+ - function invoke(): void { - root.modelData.close(); - } -@@ -438,21 +477,18 @@ StyledRect { - - required property var modelData - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3secondary : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2) - -- Layout.preferredWidth: actionText.width + Appearance.padding.normal * 2 -- Layout.preferredHeight: actionText.height + Appearance.padding.small * 2 -- implicitWidth: actionText.width + Appearance.padding.normal * 2 -- implicitHeight: actionText.height + Appearance.padding.small * 2 -+ Layout.preferredWidth: actionText.width + Tokens.padding.normal * 2 -+ Layout.preferredHeight: actionText.height + Tokens.padding.small * 2 -+ implicitWidth: actionText.width + Tokens.padding.normal * 2 -+ implicitHeight: actionText.height + Tokens.padding.small * 2 - - StateLayer { -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurface -- -- function onClicked(): void { -- action.modelData.invoke(); -- } -+ onClicked: action.modelData.invoke() - } - - StyledText { -@@ -461,7 +497,7 @@ StyledRect { - anchors.centerIn: parent - text: actionTextMetrics.elidedText - color: root.modelData.urgency === NotificationUrgency.Critical ? Colours.palette.m3onSecondary : Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - TextMetrics { -@@ -473,7 +509,7 @@ StyledRect { - elide: Text.ElideRight - elideWidth: { - const numActions = root.modelData.actions.length + 1; -- return (inner.width - actions.spacing * (numActions - 1)) / numActions - Appearance.padding.normal * 2; -+ return (inner.width - actions.spacing * (numActions - 1)) / numActions - root.Tokens.padding.normal * 2; - } - } - } -diff --git a/modules/notifications/Wrapper.qml b/modules/notifications/Wrapper.qml -index 61acc56e..97417e9c 100644 ---- a/modules/notifications/Wrapper.qml -+++ b/modules/notifications/Wrapper.qml -@@ -1,39 +1,22 @@ --import qs.components --import qs.config - import QtQuick -+import qs.components - - Item { - id: root - -- required property var visibilities -- required property Item panels -+ required property DrawerVisibilities visibilities -+ required property Item sidebarPanel -+ property alias osdPanel: content.osdPanel -+ property alias sessionPanel: content.sessionPanel - - visible: height > 0 -- implicitWidth: Math.max(panels.sidebar.width, content.implicitWidth) -+ anchors.topMargin: -5 -+ implicitWidth: Math.max(sidebarPanel.width, content.implicitWidth) - implicitHeight: content.implicitHeight - -- states: State { -- name: "hidden" -- when: root.visibilities.sidebar && Config.sidebar.enabled -- -- PropertyChanges { -- root.implicitHeight: 0 -- } -- } -- -- transitions: Transition { -- Anim { -- target: root -- property: "implicitHeight" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- } -- - Content { - id: content - - visibilities: root.visibilities -- panels: root.panels - } - } -diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml -deleted file mode 100644 -index 78955c7a..00000000 ---- a/modules/osd/Background.qml -+++ /dev/null -@@ -1,60 +0,0 @@ --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Shapes -- --ShapePath { -- id: root -- -- required property Wrapper wrapper -- readonly property real rounding: Config.border.rounding -- readonly property bool flatten: wrapper.width < rounding * 2 -- readonly property real roundingX: flatten ? wrapper.width / 2 : rounding -- -- strokeWidth: -1 -- fillColor: Colours.palette.m3surface -- -- PathArc { -- relativeX: -root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- } -- PathLine { -- relativeX: -(root.wrapper.width - root.roundingX * 2) -- relativeY: 0 -- } -- PathArc { -- relativeX: -root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- direction: PathArc.Counterclockwise -- } -- PathLine { -- relativeX: 0 -- relativeY: root.wrapper.height - root.rounding * 2 -- } -- PathArc { -- relativeX: root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- direction: PathArc.Counterclockwise -- } -- PathLine { -- relativeX: root.wrapper.width - root.roundingX * 2 -- relativeY: 0 -- } -- PathArc { -- relativeX: root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- } -- -- Behavior on fillColor { -- CAnim {} -- } --} -diff --git a/modules/osd/Content.qml b/modules/osd/Content.qml -index 770fb696..3ad3ef88 100644 ---- a/modules/osd/Content.qml -+++ b/modules/osd/Content.qml -@@ -1,18 +1,18 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config - import qs.utils --import QtQuick --import QtQuick.Layouts - - Item { - id: root - - required property Brightness.Monitor monitor -- required property var visibilities -+ required property DrawerVisibilities visibilities - - required property real volume - required property bool muted -@@ -20,20 +20,17 @@ Item { - required property bool sourceMuted - required property real brightness - -- implicitWidth: layout.implicitWidth + Appearance.padding.large * 2 -- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 -+ implicitWidth: layout.implicitWidth + Tokens.padding.large * 2 -+ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - - ColumnLayout { - id: layout - - anchors.centerIn: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - // Speaker volume - CustomMouseArea { -- implicitWidth: Config.osd.sizes.sliderWidth -- implicitHeight: Config.osd.sizes.sliderHeight -- - function onWheel(event: WheelEvent) { - if (event.angleDelta.y > 0) - Audio.incrementVolume(); -@@ -41,12 +38,15 @@ Item { - Audio.decrementVolume(); - } - -+ implicitWidth: Tokens.sizes.osd.sliderWidth -+ implicitHeight: Tokens.sizes.osd.sliderHeight -+ - FilledSlider { - anchors.fill: parent - - icon: Icons.getVolumeIcon(value, root.muted) - value: root.volume -- to: Config.services.maxVolume -+ to: GlobalConfig.services.maxVolume - onMoved: Audio.setVolume(value) - } - } -@@ -56,9 +56,6 @@ Item { - shouldBeActive: Config.osd.enableMicrophone && (!Config.osd.enableBrightness || !root.visibilities.session) - - sourceComponent: CustomMouseArea { -- implicitWidth: Config.osd.sizes.sliderWidth -- implicitHeight: Config.osd.sizes.sliderHeight -- - function onWheel(event: WheelEvent) { - if (event.angleDelta.y > 0) - Audio.incrementSourceVolume(); -@@ -66,12 +63,15 @@ Item { - Audio.decrementSourceVolume(); - } - -+ implicitWidth: Tokens.sizes.osd.sliderWidth -+ implicitHeight: Tokens.sizes.osd.sliderHeight -+ - FilledSlider { - anchors.fill: parent - - icon: Icons.getMicVolumeIcon(value, root.sourceMuted) - value: root.sourceVolume -- to: Config.services.maxVolume -+ to: GlobalConfig.services.maxVolume - onMoved: Audio.setSourceVolume(value) - } - } -@@ -82,19 +82,19 @@ Item { - shouldBeActive: Config.osd.enableBrightness - - sourceComponent: CustomMouseArea { -- implicitWidth: Config.osd.sizes.sliderWidth -- implicitHeight: Config.osd.sizes.sliderHeight -- - function onWheel(event: WheelEvent) { - const monitor = root.monitor; - if (!monitor) - return; - if (event.angleDelta.y > 0) -- monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); -+ monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); - else if (event.angleDelta.y < 0) -- monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); -+ monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); - } - -+ implicitWidth: Tokens.sizes.osd.sliderWidth -+ implicitHeight: Tokens.sizes.osd.sliderHeight -+ - FilledSlider { - anchors.fill: parent - -@@ -109,14 +109,15 @@ Item { - component WrappedLoader: Loader { - required property bool shouldBeActive - -- Layout.preferredHeight: shouldBeActive ? Config.osd.sizes.sliderHeight : 0 -+ asynchronous: true -+ Layout.preferredHeight: shouldBeActive ? Tokens.sizes.osd.sliderHeight : 0 - opacity: shouldBeActive ? 1 : 0 - active: opacity > 0 - visible: active - - Behavior on Layout.preferredHeight { - Anim { -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ type: Anim.Emphasized - } - } - -diff --git a/modules/osd/Wrapper.qml b/modules/osd/Wrapper.qml -index 2519609d..3ea0e1a3 100644 ---- a/modules/osd/Wrapper.qml -+++ b/modules/osd/Wrapper.qml -@@ -1,19 +1,23 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell --import QtQuick - - Item { - id: root - - required property ShellScreen screen -- required property var visibilities -+ required property DrawerVisibilities visibilities -+ required property bool sidebarOrSessionVisible -+ - property bool hovered - readonly property Brightness.Monitor monitor: Brightness.getMonitorForScreen(root.screen) - readonly property bool shouldBeActive: visibilities.osd && Config.osd.enabled && !(visibilities.utilities && Config.utilities.enabled) -+ property real offsetScale: shouldBeActive ? 0 : 1 -+ property real sidebarOffset: sidebarOrSessionVisible ? 12 : 0 - - property real volume - property bool muted -@@ -34,45 +38,19 @@ Item { - brightness = root.monitor?.brightness ?? 0; - } - -- visible: width > 0 -- implicitWidth: 0 -+ visible: offsetScale < 1 -+ anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale -+ implicitWidth: content.implicitWidth - implicitHeight: content.implicitHeight -+ opacity: 1 - offsetScale - -- states: State { -- name: "visible" -- when: root.shouldBeActive -- -- PropertyChanges { -- root.implicitWidth: content.implicitWidth -+ Behavior on offsetScale { -+ Anim { -+ type: Anim.DefaultSpatial - } - } - -- transitions: [ -- Transition { -- from: "" -- to: "visible" -- -- Anim { -- target: root -- property: "implicitWidth" -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- }, -- Transition { -- from: "visible" -- to: "" -- -- Anim { -- target: root -- property: "implicitWidth" -- easing.bezierCurve: Appearance.anim.curves.emphasized -- } -- } -- ] -- - Connections { -- target: Audio -- - function onMutedChanged(): void { - root.show(); - root.muted = Audio.muted; -@@ -92,21 +70,23 @@ Item { - root.show(); - root.sourceVolume = Audio.sourceVolume; - } -+ -+ target: Audio - } - - Connections { -- target: root.monitor -- - function onBrightnessChanged(): void { - root.show(); - root.brightness = root.monitor?.brightness ?? 0; - } -+ -+ target: root.monitor - } - - Timer { - id: timer - -- interval: Config.osd.hideDelay -+ interval: root.Config.osd.hideDelay - onTriggered: { - if (!root.hovered) - root.visibilities.osd = false; -@@ -119,7 +99,8 @@ Item { - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - -- Component.onCompleted: active = Qt.binding(() => root.shouldBeActive || root.visible) -+ asynchronous: true -+ active: root.shouldBeActive || root.visible - - sourceComponent: Content { - monitor: root.monitor -diff --git a/modules/session/Background.qml b/modules/session/Background.qml -deleted file mode 100644 -index 78955c7a..00000000 ---- a/modules/session/Background.qml -+++ /dev/null -@@ -1,60 +0,0 @@ --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Shapes -- --ShapePath { -- id: root -- -- required property Wrapper wrapper -- readonly property real rounding: Config.border.rounding -- readonly property bool flatten: wrapper.width < rounding * 2 -- readonly property real roundingX: flatten ? wrapper.width / 2 : rounding -- -- strokeWidth: -1 -- fillColor: Colours.palette.m3surface -- -- PathArc { -- relativeX: -root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- } -- PathLine { -- relativeX: -(root.wrapper.width - root.roundingX * 2) -- relativeY: 0 -- } -- PathArc { -- relativeX: -root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- direction: PathArc.Counterclockwise -- } -- PathLine { -- relativeX: 0 -- relativeY: root.wrapper.height - root.rounding * 2 -- } -- PathArc { -- relativeX: root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- direction: PathArc.Counterclockwise -- } -- PathLine { -- relativeX: root.wrapper.width - root.roundingX * 2 -- relativeY: 0 -- } -- PathArc { -- relativeX: root.roundingX -- relativeY: root.rounding -- radiusX: Math.min(root.rounding, root.wrapper.width) -- radiusY: root.rounding -- } -- -- Behavior on fillColor { -- CAnim {} -- } --} -diff --git a/modules/session/Content.qml b/modules/session/Content.qml -index 6c56d442..e1ba2809 100644 ---- a/modules/session/Content.qml -+++ b/modules/session/Content.qml -@@ -1,24 +1,24 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Caelestia.Config - import qs.components - import qs.services --import qs.config - import qs.utils --import Quickshell --import QtQuick - - Column { - id: root - -- required property PersistentProperties visibilities -+ required property DrawerVisibilities visibilities - -- padding: Appearance.padding.large -- spacing: Appearance.spacing.large -+ padding: Tokens.padding.large -+ spacing: Tokens.spacing.large - - SessionButton { - id: logout - -- icon: "logout" -+ icon: Config.session.icons.logout - command: Config.session.commands.logout - - KeyNavigation.down: shutdown -@@ -26,19 +26,19 @@ Column { - Component.onCompleted: forceActiveFocus() - - Connections { -- target: root.visibilities -- - function onLauncherChanged(): void { - if (!root.visibilities.launcher) - logout.forceActiveFocus(); - } -+ -+ target: root.visibilities - } - } - - SessionButton { - id: shutdown - -- icon: "power_settings_new" -+ icon: Config.session.icons.shutdown - command: Config.session.commands.shutdown - - KeyNavigation.up: logout -@@ -46,21 +46,21 @@ Column { - } - - AnimatedImage { -- width: Config.session.sizes.button -- height: Config.session.sizes.button -- sourceSize.width: width -- sourceSize.height: height -+ width: Tokens.sizes.session.button -+ height: Tokens.sizes.session.button -+ sourceSize.width: width * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) - - playing: visible - asynchronous: true -- speed: 0.7 -+ speed: Config.general.sessionGifSpeed - source: Paths.absolutePath(Config.paths.sessionGif) -+ fillMode: AnimatedImage.PreserveAspectFit - } - - SessionButton { - id: hibernate - -- icon: "downloading" -+ icon: Config.session.icons.hibernate - command: Config.session.commands.hibernate - - KeyNavigation.up: shutdown -@@ -70,7 +70,7 @@ Column { - SessionButton { - id: reboot - -- icon: "cached" -+ icon: Config.session.icons.reboot - command: Config.session.commands.reboot - - KeyNavigation.up: hibernate -@@ -82,10 +82,10 @@ Column { - required property string icon - required property list command - -- implicitWidth: Config.session.sizes.button -- implicitHeight: Config.session.sizes.button -+ implicitWidth: Tokens.sizes.session.button -+ implicitHeight: Tokens.sizes.session.button - -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - color: button.activeFocus ? Colours.palette.m3secondaryContainer : Colours.tPalette.m3surfaceContainer - - Keys.onEnterPressed: Quickshell.execDetached(button.command) -@@ -96,10 +96,10 @@ Column { - return; - - if (event.modifiers & Qt.ControlModifier) { -- if (event.key === Qt.Key_J && KeyNavigation.down) { -+ if ((event.key === Qt.Key_J || event.key === Qt.Key_N) && KeyNavigation.down) { - KeyNavigation.down.focus = true; - event.accepted = true; -- } else if (event.key === Qt.Key_K && KeyNavigation.up) { -+ } else if ((event.key === Qt.Key_K || event.key === Qt.Key_P) && KeyNavigation.up) { - KeyNavigation.up.focus = true; - event.accepted = true; - } -@@ -117,10 +117,7 @@ Column { - StateLayer { - radius: parent.radius - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- -- function onClicked(): void { -- Quickshell.execDetached(button.command); -- } -+ onClicked: Quickshell.execDetached(button.command) - } - - MaterialIcon { -@@ -128,7 +125,7 @@ Column { - - text: button.icon - color: button.activeFocus ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } - } -diff --git a/modules/session/Wrapper.qml b/modules/session/Wrapper.qml -index 14b03a80..41d9ba1a 100644 ---- a/modules/session/Wrapper.qml -+++ b/modules/session/Wrapper.qml -@@ -1,60 +1,39 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config --import Quickshell - import QtQuick -+import Caelestia.Config -+import qs.components - - Item { - id: root - -- required property PersistentProperties visibilities -- required property var panels -+ required property DrawerVisibilities visibilities -+ required property bool sidebarVisible - readonly property real nonAnimWidth: content.implicitWidth - -- visible: width > 0 -- implicitWidth: 0 -- implicitHeight: content.implicitHeight -+ readonly property bool shouldBeActive: visibilities.session && Config.session.enabled -+ property real offsetScale: shouldBeActive ? 0 : 1 -+ property real sidebarOffset: sidebarVisible ? 14 : 0 - -- states: State { -- name: "visible" -- when: root.visibilities.session && Config.session.enabled -+ visible: offsetScale < 1 -+ anchors.rightMargin: (-implicitWidth - 5 - sidebarOffset) * offsetScale -+ implicitWidth: content.implicitWidth -+ implicitHeight: content.implicitHeight || 510 // Hard coded fallback for first open -+ opacity: 1 - offsetScale - -- PropertyChanges { -- root.implicitWidth: root.nonAnimWidth -+ Behavior on offsetScale { -+ Anim { -+ type: Anim.DefaultSpatial - } - } - -- transitions: [ -- Transition { -- from: "" -- to: "visible" -- -- Anim { -- target: root -- property: "implicitWidth" -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- }, -- Transition { -- from: "visible" -- to: "" -- -- Anim { -- target: root -- property: "implicitWidth" -- easing.bezierCurve: root.panels.osd.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized -- } -- } -- ] -- - Loader { - id: content - - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - -- Component.onCompleted: active = Qt.binding(() => (root.visibilities.session && Config.session.enabled) || root.visible) -+ active: root.shouldBeActive || root.visible - - sourceComponent: Content { - visibilities: root.visibilities -diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml -deleted file mode 100644 -index beefdf5c..00000000 ---- a/modules/sidebar/Background.qml -+++ /dev/null -@@ -1,52 +0,0 @@ --import qs.components --import qs.services --import qs.config --import QtQuick --import QtQuick.Shapes -- --ShapePath { -- id: root -- -- required property Wrapper wrapper -- required property var panels -- -- readonly property real rounding: Config.border.rounding -- -- readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width -- readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding -- -- readonly property real utilsWidthDiff: panels.utilities.width - wrapper.width -- readonly property real utilsRoundingX: utilsWidthDiff < rounding * 2 ? utilsWidthDiff / 2 : rounding -- -- strokeWidth: -1 -- fillColor: Colours.palette.m3surface -- -- PathLine { -- relativeX: -root.wrapper.width - root.notifsRoundingX -- relativeY: 0 -- } -- PathArc { -- relativeX: root.notifsRoundingX -- relativeY: root.rounding -- radiusX: root.notifsRoundingX -- radiusY: root.rounding -- } -- PathLine { -- relativeX: 0 -- relativeY: root.wrapper.height - root.rounding * 2 -- } -- PathArc { -- relativeX: -root.utilsRoundingX -- relativeY: root.rounding -- radiusX: root.utilsRoundingX -- radiusY: root.rounding -- } -- PathLine { -- relativeX: root.wrapper.width + root.utilsRoundingX -- relativeY: 0 -- } -- -- Behavior on fillColor { -- CAnim {} -- } --} -diff --git a/modules/sidebar/Content.qml b/modules/sidebar/Content.qml -index 1b7feed6..f6b8eb05 100644 ---- a/modules/sidebar/Content.qml -+++ b/modules/sidebar/Content.qml -@@ -1,26 +1,26 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root - - required property Props props -- required property var visibilities -+ required property DrawerVisibilities visibilities - - ColumnLayout { - id: layout - - anchors.fill: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledRect { - Layout.fillWidth: true - Layout.fillHeight: true - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainerLow - - NotifDock { -@@ -30,7 +30,7 @@ Item { - } - - StyledRect { -- Layout.topMargin: Appearance.padding.large - layout.spacing -+ Layout.topMargin: Tokens.padding.large - layout.spacing - Layout.fillWidth: true - implicitHeight: 1 - -diff --git a/modules/sidebar/Notif.qml b/modules/sidebar/Notif.qml -index 5a317640..2a390926 100644 ---- a/modules/sidebar/Notif.qml -+++ b/modules/sidebar/Notif.qml -@@ -1,42 +1,43 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import Quickshell - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.services - - StyledRect { - id: root - -- required property Notifs.Notif modelData -+ required property NotifData modelData - required property Props props - required property bool expanded -- required property var visibilities -+ required property DrawerVisibilities visibilities - -- readonly property StyledText body: expandedContent.item?.body ?? null -- readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Appearance.padding.normal * 2 : summaryHeightMetrics.height -+ readonly property StyledText body: (expandedContent.item as ExpandedBody)?.body ?? null -+ readonly property real nonAnimHeight: expanded ? summary.implicitHeight + expandedContent.implicitHeight + expandedContent.anchors.topMargin + Tokens.padding.normal * 2 : summaryHeightMetrics.height - - implicitHeight: nonAnimHeight - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: { -- const c = root.modelData.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); -+ const c = root.modelData?.urgency === "critical" ? Colours.palette.m3secondaryContainer : Colours.layer(Colours.palette.m3surfaceContainerHigh, 2); - return expanded ? c : Qt.alpha(c, 0); - } - -+ state: expanded ? "expanded" : "" -+ - states: State { - name: "expanded" -- when: root.expanded - - PropertyChanges { -- summary.anchors.margins: Appearance.padding.normal -- dummySummary.anchors.margins: Appearance.padding.normal -- compactBody.anchors.margins: Appearance.padding.normal -- timeStr.anchors.margins: Appearance.padding.normal -- expandedContent.anchors.margins: Appearance.padding.normal -- summary.width: root.width - Appearance.padding.normal * 2 - timeStr.implicitWidth - Appearance.spacing.small -+ summary.anchors.margins: root.Tokens.padding.normal -+ dummySummary.anchors.margins: root.Tokens.padding.normal -+ compactBody.anchors.margins: root.Tokens.padding.normal -+ timeStr.anchors.margins: root.Tokens.padding.normal -+ expandedContent.anchors.margins: root.Tokens.padding.normal -+ summary.width: root.width - root.Tokens.padding.normal * 2 - timeStr.implicitWidth - root.Tokens.spacing.small - summary.maximumLineCount: Number.MAX_SAFE_INTEGER - } - } -@@ -61,8 +62,8 @@ StyledRect { - anchors.left: parent.left - - width: parent.width -- text: root.modelData.summary -- color: root.modelData.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface -+ text: root.modelData?.summary ?? "" -+ color: root.modelData?.urgency === "critical" ? Colours.palette.m3onSecondaryContainer : Colours.palette.m3onSurface - elide: Text.ElideRight - wrapMode: Text.WordWrap - maximumLineCount: 1 -@@ -75,7 +76,7 @@ StyledRect { - anchors.left: parent.left - - visible: false -- text: root.modelData.summary -+ text: root.modelData?.summary ?? "" - } - - WrappedLoader { -@@ -85,11 +86,11 @@ StyledRect { - anchors.top: parent.top - anchors.left: dummySummary.right - anchors.right: parent.right -- anchors.leftMargin: Appearance.spacing.small -+ anchors.leftMargin: Tokens.spacing.small - - sourceComponent: StyledText { -- text: root.modelData.body.replace(/\n/g, " ") -- color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline -+ text: String(root.modelData?.body ?? "").replace(/\n/g, " ") -+ color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline - elide: Text.ElideRight - } - } -@@ -103,9 +104,9 @@ StyledRect { - - sourceComponent: StyledText { - animate: true -- text: root.modelData.timeStr -+ text: root.modelData?.timeStr ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - } - -@@ -116,49 +117,88 @@ StyledRect { - anchors.top: summary.bottom - anchors.left: parent.left - anchors.right: parent.right -- anchors.topMargin: Appearance.spacing.small / 2 -+ anchors.topMargin: Tokens.spacing.small / 2 - -- sourceComponent: ColumnLayout { -- readonly property alias body: body -+ sourceComponent: ExpandedBody {} -+ } - -- spacing: Appearance.spacing.smaller -+ Behavior on implicitHeight { -+ Anim { -+ type: Anim.DefaultSpatial -+ } -+ } - -- StyledText { -- id: body -+ component ExpandedBody: ColumnLayout { -+ readonly property alias body: bodyText - -- Layout.fillWidth: true -- textFormat: Text.MarkdownText -- text: root.modelData.body.replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") -- color: root.modelData.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline -- wrapMode: Text.WordWrap -+ spacing: Tokens.spacing.smaller - -- onLinkActivated: link => { -- Quickshell.execDetached(["app2unit", "-O", "--", link]); -- root.visibilities.sidebar = false; -- } -- } -+ StyledText { -+ id: bodyText -+ -+ Layout.fillWidth: true -+ textFormat: Text.MarkdownText -+ text: String(root.modelData?.body ?? "").replace(/(.)\n(?!\n)/g, "$1\n\n") || qsTr("No body here! :/") -+ color: root.modelData?.urgency === "critical" ? Colours.palette.m3secondary : Colours.palette.m3outline -+ wrapMode: Text.WordWrap - -- NotifActionList { -- notif: root.modelData -+ onLinkActivated: link => { -+ Quickshell.execDetached(["app2unit", "-O", "--", link]); -+ root.visibilities.sidebar = false; - } - } -- } - -- Behavior on implicitHeight { -- Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ NotifActionList { -+ notif: root.modelData - } - } - - component WrappedLoader: Loader { -+ id: comp -+ - required property bool shouldBeActive - -- opacity: shouldBeActive ? 1 : 0 -- active: opacity > 0 -+ active: false -+ opacity: 0 - -- Behavior on opacity { -- Anim {} -+ // Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set -+ states: State { -+ name: "active" -+ when: comp.shouldBeActive -+ -+ PropertyChanges { -+ comp.opacity: 1 -+ comp.active: true -+ } - } -+ -+ transitions: [ -+ Transition { -+ from: "" -+ to: "active" -+ -+ SequentialAnimation { -+ PropertyAction { -+ property: "active" -+ } -+ Anim { -+ property: "opacity" -+ } -+ } -+ }, -+ Transition { -+ from: "active" -+ to: "" -+ -+ SequentialAnimation { -+ Anim { -+ property: "opacity" -+ } -+ PropertyAction { -+ property: "active" -+ } -+ } -+ } -+ ] - } - } -diff --git a/modules/sidebar/NotifActionList.qml b/modules/sidebar/NotifActionList.qml -index d1f1e1f5..084fe678 100644 ---- a/modules/sidebar/NotifActionList.qml -+++ b/modules/sidebar/NotifActionList.qml -@@ -1,19 +1,19 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components - import qs.components.containers - import qs.components.effects - import qs.services --import qs.config --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts - - Item { - id: root - -- required property Notifs.Notif notif -+ required property NotifData notif - - Layout.fillWidth: true - implicitHeight: flickable.contentHeight -@@ -94,14 +94,14 @@ Item { - id: actionList - - anchors.fill: parent -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Repeater { - model: [ - { - isClose: true - }, -- ...root.notif.actions, -+ ...(root.notif?.actions ?? []), - { - isCopy: true - } -@@ -114,11 +114,11 @@ Item { - - Layout.fillWidth: true - Layout.fillHeight: true -- implicitWidth: actionInner.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: actionInner.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: actionInner.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: actionInner.implicitHeight + Tokens.padding.small * 2 - -- Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Appearance.padding.large : 0) -- radius: actionStateLayer.pressed ? Appearance.rounding.small / 2 : Appearance.rounding.small -+ Layout.preferredWidth: implicitWidth + (actionStateLayer.pressed ? Tokens.padding.large : 0) -+ radius: actionStateLayer.pressed ? Tokens.rounding.small / 2 : Tokens.rounding.small - color: Colours.layer(Colours.palette.m3surfaceContainerHighest, 4) - - Timer { -@@ -131,7 +131,7 @@ Item { - StateLayer { - id: actionStateLayer - -- function onClicked(): void { -+ onClicked: { - if (action.modelData.isClose) { - root.notif.close(); - } else if (action.modelData.isCopy) { -@@ -150,7 +150,7 @@ Item { - id: actionInner - - anchors.centerIn: parent -- sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif.hasActionIcons ? iconComp : textComp -+ sourceComponent: action.modelData.isClose || action.modelData.isCopy ? iconBtn : root.notif?.hasActionIcons ? iconComp : textComp - } - - Component { -@@ -167,6 +167,7 @@ Item { - id: iconComp - - IconImage { -+ asynchronous: true - source: Quickshell.iconPath(action.modelData.identifier) - } - } -@@ -182,15 +183,13 @@ Item { - - Behavior on Layout.preferredWidth { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - Behavior on radius { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -diff --git a/modules/sidebar/NotifDock.qml b/modules/sidebar/NotifDock.qml -index d039d15d..4509ce26 100644 ---- a/modules/sidebar/NotifDock.qml -+++ b/modules/sidebar/NotifDock.qml -@@ -1,25 +1,26 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts -+import qs.utils - - Item { - id: root - - required property Props props -- required property var visibilities -+ required property DrawerVisibilities visibilities - readonly property int notifCount: Notifs.list.reduce((acc, n) => n.closed ? acc : acc + 1, 0) - - anchors.fill: parent -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - Component.onCompleted: Notifs.list.forEach(n => n.popup = false) - -@@ -29,7 +30,7 @@ Item { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right -- anchors.margins: Appearance.padding.small -+ anchors.margins: Tokens.padding.small - - implicitHeight: Math.max(count.implicitHeight, titleText.implicitHeight) - -@@ -43,8 +44,8 @@ Item { - - text: root.notifCount - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.normal -+ font.family: Tokens.font.family.mono - font.weight: 500 - - Behavior on anchors.leftMargin { -@@ -62,12 +63,12 @@ Item { - anchors.verticalCenter: parent.verticalCenter - anchors.left: count.right - anchors.right: parent.right -- anchors.leftMargin: Appearance.spacing.small -+ anchors.leftMargin: Tokens.spacing.small - - text: root.notifCount > 0 ? qsTr("notification%1").arg(root.notifCount === 1 ? "" : "s") : qsTr("Notifications") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.normal -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.normal -+ font.family: Tokens.font.family.mono - font.weight: 500 - elide: Text.ElideRight - } -@@ -80,24 +81,25 @@ Item { - anchors.right: parent.right - anchors.top: title.bottom - anchors.bottom: parent.bottom -- anchors.topMargin: Appearance.spacing.smaller -+ anchors.topMargin: Tokens.spacing.smaller - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - color: "transparent" - - Loader { -+ asynchronous: true - anchors.centerIn: parent - active: opacity > 0 - opacity: root.notifCount > 0 ? 0 : 1 - - sourceComponent: ColumnLayout { -- spacing: Appearance.spacing.large -+ spacing: Tokens.spacing.large - - Image { - asynchronous: true -- source: Qt.resolvedUrl(`${Quickshell.shellDir}/assets/dino.png`) -+ source: Paths.absolutePath(Config.paths.noNotifsPic) - fillMode: Image.PreserveAspectFit -- sourceSize.width: clipRect.width * 0.8 -+ sourceSize.width: clipRect.width * 0.8 * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1) - - layer.enabled: true - layer.effect: Colouriser { -@@ -110,15 +112,15 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: qsTr("No Notifications") - color: Colours.palette.m3outlineVariant -- font.pointSize: Appearance.font.size.large -- font.family: Appearance.font.family.mono -+ font.pointSize: Tokens.font.size.large -+ font.family: Tokens.font.family.mono - font.weight: 500 - } - } - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.extraLarge -+ type: Anim.StandardExtraLarge - } - } - } -@@ -150,25 +152,33 @@ Item { - id: clearTimer - - repeat: true -- interval: 50 -+ triggeredOnStart: true -+ interval: Math.max(15, Math.min(80, 69.8 - 12.3 * Math.log(Notifs.notClosed.length))) - onTriggered: { -- let next = null; -- for (let i = 0; i < notifList.repeater.count; i++) { -- next = notifList.repeater.itemAt(i); -- if (!next?.closed) -- break; -- } -- if (next) -- next.closeAll(); -- else -+ const first = Notifs.notClosed[0]; -+ if (!first) { - stop(); -+ return; -+ } -+ -+ const appName = first.appName; -+ let cleared = 0; -+ for (const n of Notifs.notClosed.filter(n => n.appName === appName)) { -+ n.close(); -+ cleared++; -+ if (cleared > 30) { -+ interval = 5; -+ return; -+ } -+ } - } - } - - Loader { -+ asynchronous: true - anchors.right: parent.right - anchors.bottom: parent.bottom -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - - scale: root.notifCount > 0 ? 1 : 0.5 - opacity: root.notifCount > 0 ? 1 : 0 -@@ -178,9 +188,9 @@ Item { - id: clearBtn - - icon: "clear_all" -- radius: Appearance.rounding.normal -- padding: Appearance.padding.normal -- font.pointSize: Math.round(Appearance.font.size.large * 1.2) -+ radius: Tokens.rounding.normal -+ padding: Tokens.padding.normal -+ font.pointSize: Math.round(Tokens.font.size.large * 1.2) - onClicked: clearTimer.start() - - Elevation { -@@ -193,14 +203,13 @@ Item { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -+ duration: Tokens.anim.durations.expressiveFastSpatial - } - } - } -diff --git a/modules/sidebar/NotifDockList.qml b/modules/sidebar/NotifDockList.qml -index b927e91a..2677698b 100644 ---- a/modules/sidebar/NotifDockList.qml -+++ b/modules/sidebar/NotifDockList.qml -@@ -1,44 +1,52 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Caelestia.Components -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell --import QtQuick - --Item { -+LazyListView { - id: root - - required property Props props - required property Flickable container -- required property var visibilities -+ required property DrawerVisibilities visibilities -+ -+ anchors.left: parent?.left -+ anchors.right: parent?.right -+ implicitHeight: contentHeight -+ -+ spacing: Tokens.spacing.small -+ readyDelay: 1 -+ cacheBuffer: 400 -+ asynchronous: true -+ -+ onViewportAdjustNeeded: d => { -+ if (contentYAnim.running) -+ contentYAnim.complete(); -+ contentYAnim.to = Math.max(0, container.contentY + d); -+ contentYAnim.start(); -+ } - -- readonly property alias repeater: repeater -- readonly property int spacing: Appearance.spacing.small -- property bool flag -+ useCustomViewport: true -+ viewport: Qt.rect(0, container.contentY, width, container.height) - -- anchors.left: parent.left -- anchors.right: parent.right -- implicitHeight: { -- const item = repeater.itemAt(repeater.count - 1); -- return item ? item.y + item.implicitHeight : 0; -- } -+ removeDuration: Tokens.anim.durations.normal - -- Repeater { -- id: repeater -- -- model: ScriptModel { -- values: { -- const map = new Map(); -- for (const n of Notifs.notClosed) -- map.set(n.appName, null); -- for (const n of Notifs.list) -- map.set(n.appName, null); -- return [...map.keys()]; -- } -- onValuesChanged: root.flagChanged() -+ model: ScriptModel { -+ values: { -+ const map = new Map(); -+ for (const n of Notifs.notClosed) -+ map.set(n.appName, null); -+ for (const n of Notifs.list) -+ map.set(n.appName, null); -+ return [...map.keys()]; - } -+ } - -+ delegate: Component { - MouseArea { - id: notif - -@@ -46,36 +54,20 @@ Item { - required property string modelData - - readonly property bool closed: notifInner.notifCount === 0 -- readonly property alias nonAnimHeight: notifInner.nonAnimHeight - property int startY - - function closeAll(): void { -- for (const n of Notifs.notClosed.filter(n => n.appName === modelData)) -- n.close(); -- } -- -- y: { -- root.flag; // Force update -- let y = 0; -- for (let i = 0; i < index; i++) { -- const item = repeater.itemAt(i); -- if (!item.closed) -- y += item.nonAnimHeight + root.spacing; -- } -- return y; -+ clearTimer.start(); - } - -- containmentMask: QtObject { -- function contains(p: point): bool { -- if (!root.container.contains(notif.mapToItem(root.container, p))) -- return false; -- return notifInner.contains(p); -- } -- } -- -- implicitWidth: root.width -+ LazyListView.trackViewport: !notifInner.expanded && notifInner.nonAnimHeight < notifInner.implicitHeight -+ LazyListView.preferredHeight: closed ? 0 : notifInner.nonAnimHeight -+ LazyListView.visibleHeight: notifInner.implicitHeight - implicitHeight: notifInner.implicitHeight - -+ opacity: LazyListView.removing || closed || LazyListView.adding ? 0 : 1 -+ scale: LazyListView.removing || closed ? 0.6 : LazyListView.adding ? 0 : 1 -+ - hoverEnabled: true - cursorShape: pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton -@@ -106,37 +98,21 @@ Item { - closeAll(); - } - -- ParallelAnimation { -- running: true -- -- Anim { -- target: notif -- property: "opacity" -- from: 0 -- to: 1 -- } -- Anim { -- target: notif -- property: "scale" -- from: 0 -- to: 1 -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- } -- -- ParallelAnimation { -- running: notif.closed -- -- Anim { -- target: notif -- property: "opacity" -- to: 0 -- } -- Anim { -- target: notif -- property: "scale" -- to: 0.6 -+ Timer { -+ id: clearTimer -+ -+ interval: 15 -+ repeat: true -+ triggeredOnStart: true -+ onTriggered: { -+ const notifs = Notifs.notClosed.filter(n => n.appName === notif.modelData); -+ if (notifs.length === 0) { -+ stop(); -+ return; -+ } -+ -+ for (const n of notifs.slice(0, 30)) -+ n.close(); - } - } - -@@ -149,19 +125,37 @@ Item { - visibilities: root.visibilities - } - -- Behavior on x { -+ Behavior on y { -+ enabled: notif.LazyListView.ready -+ - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - -- Behavior on y { -+ Behavior on opacity { -+ Anim {} -+ } -+ -+ Behavior on scale { -+ Anim { -+ type: Anim.DefaultSpatial -+ } -+ } -+ -+ Behavior on x { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } - } -+ -+ Anim { -+ id: contentYAnim -+ -+ target: root.container -+ property: "contentY" -+ type: Anim.DefaultSpatial -+ } - } -diff --git a/modules/sidebar/NotifGroup.qml b/modules/sidebar/NotifGroup.qml -index 16aac33b..2920af74 100644 ---- a/modules/sidebar/NotifGroup.qml -+++ b/modules/sidebar/NotifGroup.qml -@@ -1,14 +1,14 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Services.Notifications -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services --import qs.config - import qs.utils --import Quickshell --import Quickshell.Services.Notifications --import QtQuick --import QtQuick.Layouts - - StyledRect { - id: root -@@ -16,18 +16,25 @@ StyledRect { - required property string modelData - required property Props props - required property Flickable container -- required property var visibilities -+ required property DrawerVisibilities visibilities - - readonly property list notifs: Notifs.list.filter(n => n.appName === modelData) -- readonly property int notifCount: notifs.reduce((acc, n) => n.closed ? acc : acc + 1, 0) -- readonly property string image: notifs.find(n => !n.closed && n.image.length > 0)?.image ?? "" -- readonly property string appIcon: notifs.find(n => !n.closed && n.appIcon.length > 0)?.appIcon ?? "" -- readonly property int urgency: notifs.some(n => !n.closed && n.urgency === NotificationUrgency.Critical) ? NotificationUrgency.Critical : notifs.some(n => n.urgency === NotificationUrgency.Normal) ? NotificationUrgency.Normal : NotificationUrgency.Low -+ readonly property list activeNotifs: notifs.filter(n => !n.closed) -+ readonly property int notifCount: activeNotifs.length -+ readonly property string image: activeNotifs.find(n => n.image.length > 0)?.image ?? "" -+ readonly property string appIcon: activeNotifs.find(n => n.appIcon.length > 0)?.appIcon ?? "" -+ readonly property int urgency: { -+ if (activeNotifs.find(n => n.urgency === NotificationUrgency.Critical)) -+ return NotificationUrgency.Critical; -+ if (activeNotifs.find(n => n.urgency === NotificationUrgency.Normal)) -+ return NotificationUrgency.Normal; -+ return NotificationUrgency.Low; -+ } - - readonly property int nonAnimHeight: { -- const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Appearance.spacing.small / 2) : 0); -- const columnHeight = headerHeight + notifList.nonAnimHeight + column.Layout.topMargin + column.Layout.bottomMargin; -- return Math.round(Math.max(Config.notifs.sizes.image, columnHeight) + Appearance.padding.normal * 2); -+ const headerHeight = header.implicitHeight + (root.expanded ? Math.round(Tokens.spacing.small / 2) : 0); -+ const columnHeight = headerHeight + notifList.layoutHeight; -+ return Math.round(Math.max(TokenConfig.sizes.notifs.image, columnHeight) + Tokens.padding.normal * 2); - } - readonly property bool expanded: props.expandedNotifs.includes(modelData) - -@@ -47,26 +54,32 @@ StyledRect { - - anchors.left: parent?.left - anchors.right: parent?.right -- implicitHeight: content.implicitHeight + Appearance.padding.normal * 2 -+ implicitHeight: nonAnimHeight - - clip: true -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - -+ Behavior on implicitHeight { -+ Anim { -+ type: Anim.DefaultSpatial -+ } -+ } -+ - RowLayout { - id: content - - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top -- anchors.margins: Appearance.padding.normal -+ anchors.margins: Tokens.padding.normal - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Item { - Layout.alignment: Qt.AlignLeft | Qt.AlignTop -- implicitWidth: Config.notifs.sizes.image -- implicitHeight: Config.notifs.sizes.image -+ implicitWidth: TokenConfig.sizes.notifs.image -+ implicitHeight: TokenConfig.sizes.notifs.image - - Component { - id: imageComp -@@ -74,10 +87,14 @@ StyledRect { - Image { - source: Qt.resolvedUrl(root.image) - fillMode: Image.PreserveAspectCrop -+ sourceSize: { -+ const size = TokenConfig.sizes.notifs.image * ((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1); -+ return Qt.size(size, size); -+ } - cache: false - asynchronous: true -- width: Config.notifs.sizes.image -- height: Config.notifs.sizes.image -+ width: TokenConfig.sizes.notifs.image -+ height: TokenConfig.sizes.notifs.image - } - } - -@@ -85,7 +102,7 @@ StyledRect { - id: appIconComp - - ColouredIcon { -- implicitSize: Math.round(Config.notifs.sizes.image * 0.6) -+ implicitSize: Math.round(TokenConfig.sizes.notifs.image * 0.6) - source: Quickshell.iconPath(root.appIcon) - colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - layer.enabled: root.appIcon.endsWith("symbolic") -@@ -96,38 +113,40 @@ StyledRect { - id: materialIconComp - - MaterialIcon { -- text: Icons.getNotifIcon(root.notifs[0]?.summary, root.urgency) -+ text: Icons.getNotifIcon(root.activeNotifs[0]?.summary, root.urgency) - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - - StyledClippingRect { - anchors.fill: parent - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) : Colours.palette.m3secondaryContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - Loader { -+ asynchronous: true - anchors.centerIn: parent - sourceComponent: root.image ? imageComp : root.appIcon ? appIconComp : materialIconComp - } - } - - Loader { -+ asynchronous: true - anchors.right: parent.right - anchors.bottom: parent.bottom - active: root.appIcon && root.image - - sourceComponent: StyledRect { -- implicitWidth: Config.notifs.sizes.badge -- implicitHeight: Config.notifs.sizes.badge -+ implicitWidth: Tokens.sizes.notifs.badge -+ implicitHeight: Tokens.sizes.notifs.badge - - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : root.urgency === NotificationUrgency.Low ? Colours.palette.m3surfaceContainerHigh : Colours.palette.m3secondaryContainer -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - ColouredIcon { - anchors.centerIn: parent -- implicitSize: Math.round(Config.notifs.sizes.badge * 0.6) -+ implicitSize: Math.round(Tokens.sizes.notifs.badge * 0.6) - source: Quickshell.iconPath(root.appIcon) - colour: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : root.urgency === NotificationUrgency.Low ? Colours.palette.m3onSurface : Colours.palette.m3onSecondaryContainer - layer.enabled: root.appIcon.endsWith("symbolic") -@@ -136,94 +155,87 @@ StyledRect { - } - } - -- ColumnLayout { -+ Column { - id: column - -- Layout.topMargin: -Appearance.padding.small -- Layout.bottomMargin: -Appearance.padding.small / 2 - Layout.fillWidth: true -- spacing: 0 -+ spacing: root.expanded ? Math.round(Tokens.spacing.small / 2) : 0 -+ -+ Behavior on spacing { -+ Anim {} -+ } - - RowLayout { - id: header - -- Layout.bottomMargin: root.expanded ? Math.round(Appearance.spacing.small / 2) : 0 -- Layout.fillWidth: true -- spacing: Appearance.spacing.smaller -+ anchors.left: parent.left -+ anchors.right: parent.right -+ spacing: Tokens.spacing.smaller - - StyledText { - Layout.fillWidth: true - text: root.modelData - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - - StyledText { - animate: true -- text: root.notifs.find(n => !n.closed)?.timeStr ?? "" -+ text: root.activeNotifs[0]?.timeStr ?? "" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - StyledRect { -- implicitWidth: expandBtn.implicitWidth + Appearance.padding.smaller * 2 -- implicitHeight: groupCount.implicitHeight + Appearance.padding.small -+ implicitWidth: expandBtn.implicitWidth + Tokens.padding.smaller * 2 -+ implicitHeight: groupCount.implicitHeight + Tokens.padding.small - - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3error : Colours.layer(Colours.palette.m3surfaceContainerHigh, 3) -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - - StateLayer { - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface -- -- function onClicked(): void { -- root.toggleExpand(!root.expanded); -- } -+ onClicked: root.toggleExpand(!root.expanded) - } - - RowLayout { - id: expandBtn - - anchors.centerIn: parent -- spacing: Appearance.spacing.small / 2 -+ spacing: Tokens.spacing.small / 2 - - StyledText { - id: groupCount - -- Layout.leftMargin: Appearance.padding.small / 2 -+ Layout.leftMargin: Tokens.padding.small / 2 - animate: true - text: root.notifCount - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - } - - MaterialIcon { -- Layout.rightMargin: -Appearance.padding.small / 2 -+ Layout.rightMargin: -Tokens.padding.small / 2 - text: "expand_more" - color: root.urgency === NotificationUrgency.Critical ? Colours.palette.m3onError : Colours.palette.m3onSurface - rotation: root.expanded ? 180 : 0 -- Layout.topMargin: root.expanded ? -Math.floor(Appearance.padding.smaller / 2) : 0 -+ Layout.topMargin: root.expanded ? -Math.floor(Tokens.padding.smaller / 2) : 0 - - Behavior on rotation { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - Behavior on Layout.topMargin { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } - } - } -- -- Behavior on Layout.bottomMargin { -- Anim {} -- } - } - - NotifGroupList { -diff --git a/modules/sidebar/NotifGroupList.qml b/modules/sidebar/NotifGroupList.qml -index e586b5f7..72b8b3a8 100644 ---- a/modules/sidebar/NotifGroupList.qml -+++ b/modules/sidebar/NotifGroupList.qml -@@ -1,114 +1,82 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import Quickshell -+import Caelestia.Components -+import Caelestia.Config - import qs.components - import qs.services --import qs.config --import Quickshell --import QtQuick --import QtQuick.Layouts - --Item { -+LazyListView { - id: root - - required property Props props - required property list notifs - required property bool expanded - required property Flickable container -- required property var visibilities -- -- readonly property real nonAnimHeight: { -- let h = -root.spacing; -- for (let i = 0; i < repeater.count; i++) { -- const item = repeater.itemAt(i); -- if (!item.modelData.closed && !item.previewHidden) -- h += item.nonAnimHeight + root.spacing; -- } -- return h; -- } -- -- readonly property int spacing: Math.round(Appearance.spacing.small / 2) -- property bool showAllNotifs -- property bool flag -+ required property DrawerVisibilities visibilities - - signal requestToggleExpand(expand: bool) - -- onExpandedChanged: { -- if (expanded) { -- clearTimer.stop(); -- showAllNotifs = true; -- } else { -- clearTimer.start(); -- } -- } -+ anchors.left: parent.left -+ anchors.right: parent.right -+ implicitHeight: contentHeight - -- Layout.fillWidth: true -- implicitHeight: nonAnimHeight -+ spacing: Math.round(Tokens.spacing.small / 2) -+ asynchronous: true - -- Timer { -- id: clearTimer -+ readyDelay: 1 -+ cacheBuffer: 400 -+ removeDuration: Tokens.anim.durations.normal - -- interval: Appearance.anim.durations.normal -- onTriggered: root.showAllNotifs = false -+ useCustomViewport: true -+ viewport: { -+ tWatcher.transform; // mapToItem is not reactive so use this to trigger updates -+ return Qt.rect(0, container.contentY - mapToItem(container.contentItem, 0, 0).y, width, container.height); - } - -- Repeater { -- id: repeater -+ model: ScriptModel { -+ values: { -+ if (root.expanded) -+ return root.notifs; -+ -+ let count = 0; -+ let i = 0; -+ const previewNum = root.Config.notifs.groupPreviewNum; -+ while (i < root.notifs.length && count < previewNum) { -+ if (!(root.notifs[i]?.closed ?? true)) -+ count++; -+ i++; -+ } - -- model: ScriptModel { -- values: root.showAllNotifs ? root.notifs : root.notifs.slice(0, Config.notifs.groupPreviewNum + 1) -- onValuesChanged: root.flagChanged() -+ return root.notifs.slice(0, i); - } -+ } - -+ delegate: Component { - MouseArea { - id: notif - - required property int index -- required property Notifs.Notif modelData -- -- readonly property alias nonAnimHeight: notifInner.nonAnimHeight -- readonly property bool previewHidden: { -- if (root.expanded) -- return false; -+ required property NotifData modelData - -- let extraHidden = 0; -- for (let i = 0; i < index; i++) -- if (root.notifs[i].closed) -- extraHidden++; -- -- return index >= Config.notifs.groupPreviewNum + extraHidden; -- } - property int startY - -- y: { -- root.flag; // Force update -- let y = 0; -- for (let i = 0; i < index; i++) { -- const item = repeater.itemAt(i); -- if (!item.modelData.closed && !item.previewHidden) -- y += item.nonAnimHeight + root.spacing; -- } -- return y; -- } -+ Component.onCompleted: modelData?.lock(this) -+ Component.onDestruction: modelData?.unlock(this) - -- containmentMask: QtObject { -- function contains(p: point): bool { -- if (!root.container.contains(notif.mapToItem(root.container, p))) -- return false; -- return notifInner.contains(p); -- } -- } -- -- opacity: previewHidden ? 0 : 1 -- scale: previewHidden ? 0.7 : 1 -- -- implicitWidth: root.width -+ LazyListView.preferredHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.nonAnimHeight -+ LazyListView.visibleHeight: modelData?.closed || LazyListView.removing ? 0 : notifInner.implicitHeight - implicitHeight: notifInner.implicitHeight - -+ opacity: LazyListView.removing || LazyListView.adding ? 0 : 1 -+ scale: LazyListView.removing || LazyListView.adding ? 0.7 : 1 -+ - hoverEnabled: true - cursorShape: notifInner.body?.hoveredLink ? Qt.PointingHandCursor : pressed ? Qt.ClosedHandCursor : undefined - acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton - preventStealing: !root.expanded -- enabled: !modelData.closed -+ enabled: !(modelData?.closed ?? true) - - drag.target: this - drag.axis: Drag.XAxis -@@ -118,7 +86,7 @@ Item { - if (event.button === Qt.RightButton) - root.requestToggleExpand(!root.expanded); - else if (event.button === Qt.MiddleButton) -- modelData.close(); -+ modelData?.close(); - } - onPositionChanged: event => { - if (pressed && !root.expanded) { -@@ -131,32 +99,12 @@ Item { - if (Math.abs(x) < width * Config.notifs.clearThreshold) - x = 0; - else -- modelData.close(); -- } -- -- Component.onCompleted: modelData.lock(this) -- Component.onDestruction: modelData.unlock(this) -- -- ParallelAnimation { -- Component.onCompleted: running = !notif.previewHidden -- -- Anim { -- target: notif -- property: "opacity" -- from: 0 -- to: 1 -- } -- Anim { -- target: notif -- property: "scale" -- from: 0.7 -- to: 1 -- } -+ modelData?.close(); - } - - ParallelAnimation { -- running: notif.modelData.closed -- onFinished: notif.modelData.unlock(notif) -+ running: notif.modelData?.closed ?? false -+ onFinished: notif.modelData?.unlock(notif) - - Anim { - target: notif -@@ -180,6 +128,14 @@ Item { - visibilities: root.visibilities - } - -+ Behavior on y { -+ enabled: notif.LazyListView.ready -+ -+ Anim { -+ type: Anim.DefaultSpatial -+ } -+ } -+ - Behavior on opacity { - Anim {} - } -@@ -190,24 +146,16 @@ Item { - - Behavior on x { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- } -- -- Behavior on y { -- Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } - } - -- Behavior on implicitHeight { -- Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -+ TransformWatcher { -+ id: tWatcher -+ -+ a: root.container.contentItem -+ b: root - } - } -diff --git a/modules/sidebar/Wrapper.qml b/modules/sidebar/Wrapper.qml -index 9303c6b9..108c51a2 100644 ---- a/modules/sidebar/Wrapper.qml -+++ b/modules/sidebar/Wrapper.qml -@@ -1,66 +1,42 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config - import QtQuick -+import Caelestia.Config -+import qs.components - - Item { - id: root - -- required property var visibilities -- required property var panels -+ required property DrawerVisibilities visibilities - readonly property Props props: Props {} - -- visible: width > 0 -- implicitWidth: 0 -+ readonly property bool shouldBeActive: visibilities.sidebar && Config.sidebar.enabled -+ property real offsetScale: shouldBeActive ? 0 : 1 - -- states: State { -- name: "visible" -- when: root.visibilities.sidebar && Config.sidebar.enabled -+ visible: offsetScale < 1 -+ anchors.rightMargin: (-implicitWidth - 5) * offsetScale -+ implicitWidth: Tokens.sizes.sidebar.width -+ opacity: 1 - offsetScale - -- PropertyChanges { -- root.implicitWidth: Config.sidebar.sizes.width -+ Behavior on offsetScale { -+ Anim { -+ type: Anim.DefaultSpatial - } - } - -- transitions: [ -- Transition { -- from: "" -- to: "visible" -- -- Anim { -- target: root -- property: "implicitWidth" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -- } -- }, -- Transition { -- from: "visible" -- to: "" -- -- Anim { -- target: root -- property: "implicitWidth" -- easing.bezierCurve: root.panels.osd.width > 0 || root.panels.session.width > 0 ? Appearance.anim.curves.expressiveDefaultSpatial : Appearance.anim.curves.emphasized -- } -- } -- ] -- - Loader { - id: content - - anchors.top: parent.top - anchors.bottom: parent.bottom - anchors.left: parent.left -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - anchors.bottomMargin: 0 - -- active: true -- Component.onCompleted: active = Qt.binding(() => (root.visibilities.sidebar && Config.sidebar.enabled) || root.visible) -+ active: root.shouldBeActive || root.visible - - sourceComponent: Content { -- implicitWidth: Config.sidebar.sizes.width - Appearance.padding.large * 2 -+ implicitWidth: Tokens.sizes.sidebar.width - Tokens.padding.large * 2 - props: root.props - visibilities: root.visibilities - } -diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml -index fbce8961..5b58b41e 100644 ---- a/modules/utilities/Background.qml -+++ b/modules/utilities/Background.qml -@@ -1,15 +1,14 @@ --import qs.components --import qs.services --import qs.config - import QtQuick - import QtQuick.Shapes -+import qs.components -+import qs.services - - ShapePath { - id: root - - required property Wrapper wrapper - required property var sidebar -- readonly property real rounding: Config.border.rounding -+ required property real rounding - readonly property bool flatten: wrapper.height < rounding * 2 - readonly property real roundingY: flatten ? wrapper.height / 2 : rounding - -diff --git a/modules/utilities/Content.qml b/modules/utilities/Content.qml -index 902656de..e03c546f 100644 ---- a/modules/utilities/Content.qml -+++ b/modules/utilities/Content.qml -@@ -1,14 +1,17 @@ - import "cards" --import qs.config - import QtQuick - import QtQuick.Layouts -+import Caelestia.Config -+import qs.components -+import qs.modules.bar.popouts as BarPopouts - - Item { - id: root - - required property var props -- required property var visibilities -- required property Item popouts -+ required property DrawerVisibilities visibilities -+ required property BarPopouts.Wrapper popouts -+ required property matrix4x4 deformMatrix - - implicitWidth: layout.implicitWidth - implicitHeight: layout.implicitHeight -@@ -17,7 +20,7 @@ Item { - id: layout - - anchors.fill: parent -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - IdleInhibit {} - -@@ -35,5 +38,6 @@ Item { - - RecordingDeleteModal { - props: root.props -+ deformMatrix: root.deformMatrix - } - } -diff --git a/modules/utilities/RecordingDeleteModal.qml b/modules/utilities/RecordingDeleteModal.qml -index 127afe93..13125f2c 100644 ---- a/modules/utilities/RecordingDeleteModal.qml -+++ b/modules/utilities/RecordingDeleteModal.qml -@@ -1,20 +1,22 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import QtQuick.Shapes -+import Caelestia -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.components.effects - import qs.services --import qs.config --import Caelestia --import QtQuick --import QtQuick.Layouts --import QtQuick.Shapes - - Loader { - id: root - - required property var props -+ required property matrix4x4 deformMatrix - -+ asynchronous: true - anchors.fill: parent - - opacity: root.props.recordingConfirmDelete ? 1 : 0 -@@ -32,14 +34,16 @@ Loader { - - Item { - anchors.fill: parent -- anchors.margins: -Appearance.padding.large -- anchors.rightMargin: -Appearance.padding.large - Config.border.thickness -- anchors.bottomMargin: -Appearance.padding.large - Config.border.thickness -+ anchors.margins: -Tokens.padding.large -+ anchors.rightMargin: -Tokens.padding.large - Config.border.thickness -+ anchors.bottomMargin: -Tokens.padding.large - Config.border.thickness - opacity: 0.5 - - StyledRect { - anchors.fill: parent -- topLeftRadius: Config.border.rounding -+ anchors.rightMargin: -parent.width * (1 - root.deformMatrix.m11) / 2 // Additional bit to account for deform -+ anchors.bottomMargin: -parent.height * 0.1 // Additional bit to account for overshoot -+ topLeftRadius: Tokens.rounding.large - color: Colours.palette.m3scrim - } - -@@ -50,13 +54,14 @@ Loader { - preferredRendererType: Shape.CurveRenderer - asynchronous: true - -+ // Bottom left - ShapePath { -- startX: -Config.border.rounding * 2 -- startY: shape.height - Config.border.thickness -+ startX: -root.Config.border.smoothing * 2 -+ startY: shape.height - root.Config.border.thickness - strokeWidth: 0 - fillGradient: LinearGradient { - orientation: LinearGradient.Horizontal -- x1: -Config.border.rounding * 2 -+ x1: -root.Config.border.smoothing * 2 - - GradientStop { - position: 0 -@@ -69,31 +74,34 @@ Loader { - } - - PathLine { -- relativeX: Config.border.rounding -+ relativeX: root.Config.border.smoothing - relativeY: 0 - } -- PathArc { -- relativeY: -Config.border.rounding -- radiusX: Config.border.rounding -- radiusY: Config.border.rounding -- direction: PathArc.Counterclockwise -+ PathCubic { -+ relativeX: root.Config.border.smoothing -+ relativeY: -root.Config.border.smoothing -+ relativeControl1X: root.Config.border.smoothing * 0.93 -+ relativeControl1Y: -root.Config.border.smoothing * 0.07 -+ relativeControl2X: root.Config.border.smoothing * 0.93 -+ relativeControl2Y: -root.Config.border.smoothing * 0.07 - } - PathLine { - relativeX: 0 -- relativeY: Config.border.rounding + Config.border.thickness -+ relativeY: root.Config.border.smoothing + root.Config.border.thickness - } - PathLine { -- relativeX: -Config.border.rounding * 2 -+ relativeX: -root.Config.border.smoothing * 2 - relativeY: 0 - } - } - -+ // Top right curve - ShapePath { -- startX: shape.width - Config.border.rounding - Config.border.thickness -+ startX: shape.width - root.Config.border.smoothing - root.Config.border.thickness + (1 - root.deformMatrix.m11) * shape.width / 2 - strokeWidth: 0 - fillGradient: LinearGradient { - orientation: LinearGradient.Vertical -- y1: -Config.border.rounding * 2 -+ y1: -root.Config.border.smoothing * 2 - - GradientStop { - position: 0 -@@ -105,19 +113,20 @@ Loader { - } - } - -- PathArc { -- relativeX: Config.border.rounding -- relativeY: -Config.border.rounding -- radiusX: Config.border.rounding -- radiusY: Config.border.rounding -- direction: PathArc.Counterclockwise -+ PathCubic { -+ relativeX: root.Config.border.smoothing -+ relativeY: -root.Config.border.smoothing -+ relativeControl1X: root.Config.border.smoothing * 0.93 -+ relativeControl1Y: -root.Config.border.smoothing * 0.07 -+ relativeControl2X: root.Config.border.smoothing * 0.93 -+ relativeControl2Y: -root.Config.border.smoothing * 0.07 - } - PathLine { - relativeX: 0 -- relativeY: -Config.border.rounding -+ relativeY: -root.Config.border.smoothing - } - PathLine { -- relativeX: Config.border.thickness -+ relativeX: root.Config.border.thickness - relativeY: 0 - } - PathLine { -@@ -129,15 +138,15 @@ Loader { - - StyledRect { - anchors.centerIn: parent -- radius: Appearance.rounding.large -+ radius: Tokens.rounding.large - color: Colours.palette.m3surfaceContainerHigh - - scale: 0 - Component.onCompleted: scale = Qt.binding(() => root.props.recordingConfirmDelete ? 1 : 0) - -- width: Math.min(parent.width - Appearance.padding.large * 2, implicitWidth) -- implicitWidth: deleteConfirmationLayout.implicitWidth + Appearance.padding.large * 3 -- implicitHeight: deleteConfirmationLayout.implicitHeight + Appearance.padding.large * 3 -+ width: Math.min(parent.width - Tokens.padding.large * 2, implicitWidth) -+ implicitWidth: deleteConfirmationLayout.implicitWidth + Tokens.padding.large * 3 -+ implicitHeight: deleteConfirmationLayout.implicitHeight + Tokens.padding.large * 3 - - MouseArea { - anchors.fill: parent -@@ -154,26 +163,26 @@ Loader { - id: deleteConfirmationLayout - - anchors.fill: parent -- anchors.margins: Appearance.padding.large * 1.5 -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.large * 1.5 -+ spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Delete recording?") -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - - StyledText { - Layout.fillWidth: true - text: qsTr("Recording '%1' will be permanently deleted.").arg(deleteConfirmation.path) - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - } - - RowLayout { -- Layout.topMargin: Appearance.spacing.normal -+ Layout.topMargin: Tokens.spacing.normal - Layout.alignment: Qt.AlignRight -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - TextButton { - text: qsTr("Cancel") -@@ -194,8 +203,7 @@ Loader { - - Behavior on scale { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -diff --git a/modules/utilities/Wrapper.qml b/modules/utilities/Wrapper.qml -index 842a550f..cace5685 100644 ---- a/modules/utilities/Wrapper.qml -+++ b/modules/utilities/Wrapper.qml -@@ -1,16 +1,20 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia.Config -+import qs.components -+import qs.modules.sidebar as Sidebar -+import qs.modules.bar.popouts as BarPopouts - - Item { - id: root - -- required property var visibilities -- required property Item sidebar -- required property Item popouts -+ required property DrawerVisibilities visibilities -+ required property Sidebar.Wrapper sidebar -+ required property BarPopouts.Wrapper popouts -+ property real horizontalStretch -+ property matrix4x4 deformMatrix - - readonly property PersistentProperties props: PersistentProperties { - property bool recordingListExpanded: false -@@ -21,59 +25,48 @@ Item { - reloadableId: "utilities" - } - readonly property bool shouldBeActive: visibilities.sidebar || (visibilities.utilities && Config.utilities.enabled && !(visibilities.session && Config.session.enabled)) -+ property real offsetScale: shouldBeActive ? 0 : 1 -+ property real sidebarLerp - -- visible: height > 0 -- implicitHeight: 0 -- implicitWidth: sidebar.visible ? sidebar.width : Config.utilities.sizes.width -- -- onStateChanged: { -- if (state === "visible" && timer.running) { -- timer.triggered(); -- timer.stop(); -- } -- } -+ visible: offsetScale < 1 -+ anchors.bottomMargin: (-implicitHeight - 5) * offsetScale -+ implicitHeight: content.implicitHeight + content.anchors.margins * 2 -+ implicitWidth: sidebar.width * (1 - sidebar.offsetScale) * horizontalStretch * sidebarLerp + Tokens.sizes.utilities.width * (1 - sidebarLerp) -+ opacity: 1 - offsetScale - - states: State { -- name: "visible" -- when: root.shouldBeActive -+ name: "attachedToSidebar" -+ when: root.visibilities.sidebar - - PropertyChanges { -- root.implicitHeight: content.implicitHeight + Appearance.padding.large * 2 -+ root.sidebarLerp: 1 - } - } - - transitions: [ - Transition { - from: "" -- to: "visible" - - Anim { -- target: root -- property: "implicitHeight" -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ property: "sidebarLerp" -+ duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 -+ easing: Tokens.anim.standardAccel - } - }, - Transition { -- from: "visible" - to: "" - - Anim { -- target: root -- property: "implicitHeight" -- easing.bezierCurve: Appearance.anim.curves.emphasized -+ property: "sidebarLerp" -+ duration: Tokens.anim.durations.expressiveDefaultSpatial / 2 -+ easing: Tokens.anim.standardDecel - } - } - ] - -- Timer { -- id: timer -- -- running: true -- interval: Appearance.anim.durations.extraLarge -- onTriggered: { -- content.active = Qt.binding(() => root.shouldBeActive || root.visible); -- content.visible = true; -+ Behavior on offsetScale { -+ Anim { -+ type: Anim.DefaultSpatial - } - } - -@@ -82,16 +75,17 @@ Item { - - anchors.top: parent.top - anchors.left: parent.left -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- visible: false -- active: true -+ asynchronous: true -+ active: root.shouldBeActive || root.visible - - sourceComponent: Content { -- implicitWidth: root.implicitWidth - Appearance.padding.large * 2 -+ implicitWidth: root.implicitWidth - content.anchors.margins * 2 - props: root.props - visibilities: root.visibilities - popouts: root.popouts -+ deformMatrix: root.deformMatrix - } - } - } -diff --git a/modules/utilities/cards/IdleInhibit.qml b/modules/utilities/cards/IdleInhibit.qml -index 0344e3ad..c37ef3c5 100644 ---- a/modules/utilities/cards/IdleInhibit.qml -+++ b/modules/utilities/cards/IdleInhibit.qml -@@ -1,17 +1,17 @@ -+import QtQuick -+import QtQuick.Layouts -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config --import QtQuick --import QtQuick.Layouts - - StyledRect { - id: root - - Layout.fillWidth: true -- implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Appearance.padding.large * 2 -+ implicitHeight: layout.implicitHeight + (IdleInhibitor.enabled ? activeChip.implicitHeight + activeChip.anchors.topMargin : 0) + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - clip: true - -@@ -21,14 +21,14 @@ StyledRect { - anchors.top: parent.top - anchors.left: parent.left - anchors.right: parent.right -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - StyledRect { - implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: IdleInhibitor.enabled ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - MaterialIcon { -@@ -37,7 +37,7 @@ StyledRect { - anchors.centerIn: parent - text: "coffee" - color: IdleInhibitor.enabled ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - -@@ -48,7 +48,7 @@ StyledRect { - StyledText { - Layout.fillWidth: true - text: qsTr("Keep Awake") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - -@@ -56,7 +56,7 @@ StyledRect { - Layout.fillWidth: true - text: IdleInhibitor.enabled ? qsTr("Preventing sleep mode") : qsTr("Normal power management") - color: Colours.palette.m3onSurfaceVariant -- font.pointSize: Appearance.font.size.small -+ font.pointSize: Tokens.font.size.small - elide: Text.ElideRight - } - } -@@ -70,11 +70,12 @@ StyledRect { - Loader { - id: activeChip - -+ asynchronous: true - anchors.bottom: parent.bottom - anchors.left: parent.left -- anchors.topMargin: Appearance.spacing.larger -- anchors.bottomMargin: IdleInhibitor.enabled ? Appearance.padding.large : -implicitHeight -- anchors.leftMargin: Appearance.padding.large -+ anchors.topMargin: Tokens.spacing.larger -+ anchors.bottomMargin: IdleInhibitor.enabled ? Tokens.padding.large : -implicitHeight -+ anchors.leftMargin: Tokens.padding.large - - opacity: IdleInhibitor.enabled ? 1 : 0 - scale: IdleInhibitor.enabled ? 1 : 0.5 -@@ -82,32 +83,31 @@ StyledRect { - Component.onCompleted: active = Qt.binding(() => opacity > 0) - - sourceComponent: StyledRect { -- implicitWidth: activeText.implicitWidth + Appearance.padding.normal * 2 -- implicitHeight: activeText.implicitHeight + Appearance.padding.small * 2 -+ implicitWidth: activeText.implicitWidth + Tokens.padding.normal * 2 -+ implicitHeight: activeText.implicitHeight + Tokens.padding.small * 2 - -- radius: Appearance.rounding.full -+ radius: Tokens.rounding.full - color: Colours.palette.m3primary - - StyledText { - id: activeText - - anchors.centerIn: parent -- text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, Config.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) -+ text: qsTr("Active since %1").arg(Qt.formatTime(IdleInhibitor.enabledSince, GlobalConfig.services.useTwelveHourClock ? "hh:mm a" : "hh:mm")) - color: Colours.palette.m3onPrimary -- font.pointSize: Math.round(Appearance.font.size.small * 0.9) -+ font.pointSize: Math.round(Tokens.font.size.small * 0.9) - } - } - - Behavior on anchors.bottomMargin { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - - Behavior on opacity { - Anim { -- duration: Appearance.anim.durations.small -+ type: Anim.StandardSmall - } - } - -@@ -118,8 +118,7 @@ StyledRect { - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -diff --git a/modules/utilities/cards/Record.qml b/modules/utilities/cards/Record.qml -index 7caccc83..417f4147 100644 ---- a/modules/utilities/cards/Record.qml -+++ b/modules/utilities/cards/Record.qml -@@ -20,8 +20,9 @@ StyledRect { - color: Colours.tPalette.m3surfaceContainer - - property bool actuallyRecording: Recorder.running -+ readonly property bool recordingBusy: Recorder.running || Recorder.starting - property string lastError: "" -- property string currentVideoMode: Config.utilities.recording.videoMode -+ property string currentVideoMode: Recorder.videoMode || Config.utilities.recording.videoMode || "fullscreen" - - // Computed audio mode based on settings - readonly property string currentAudioMode: { -@@ -52,7 +53,7 @@ StyledRect { - } - - radius: Appearance.rounding.full -- color: root.actuallyRecording ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer -+ color: root.recordingBusy ? Colours.palette.m3secondary : Colours.palette.m3secondaryContainer - - MaterialIcon { - id: icon -@@ -61,7 +62,7 @@ StyledRect { - anchors.horizontalCenterOffset: -0.5 - anchors.verticalCenterOffset: 1.5 - text: "screen_record" -- color: root.actuallyRecording ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer -+ color: root.recordingBusy ? Colours.palette.m3onSecondary : Colours.palette.m3onSecondaryContainer - font.pointSize: Appearance.font.size.large - } - } -@@ -81,11 +82,12 @@ StyledRect { - Layout.fillWidth: true - text: { - if (root.lastError !== "") return qsTr("Error: %1").arg(root.lastError); -+ if (Recorder.starting) return root.startingText(Recorder.videoMode || root.currentVideoMode); - if (Recorder.paused) return qsTr("Recording paused"); - if (root.actuallyRecording) { -- const videoText = root.currentVideoMode; -- const audioText = root.currentAudioMode === "none" ? "no audio" : root.currentAudioMode; -- return qsTr("Recording %1 - %2").arg(videoText).arg(audioText); -+ const videoText = root.videoModeLabel(Recorder.videoMode || root.currentVideoMode); -+ const audioText = root.audioModeLabel(Recorder.audioMode || root.currentAudioMode); -+ return qsTr("Recording %1 with %2").arg(videoText).arg(audioText); - } - return qsTr("Recording off"); - } -@@ -96,7 +98,7 @@ StyledRect { - } - - SplitButton { -- disabled: root.actuallyRecording -+ disabled: root.recordingBusy - active: menuItems.find(m => m.mode === Config.utilities.recording.videoMode) ?? menuItems[0] - menu.onItemSelected: item => { - Config.utilities.recording.videoMode = item.mode; -@@ -110,21 +112,21 @@ StyledRect { - icon: "fullscreen" - text: qsTr("Record fullscreen") - activeText: qsTr("Fullscreen") -- onClicked: startRecording() -+ onClicked: startRecording(mode) - }, - MenuItem { - property string mode: "region" - icon: "screenshot_region" - text: qsTr("Record region") - activeText: qsTr("Region") -- onClicked: startRecording() -+ onClicked: startRecording(mode) - }, - MenuItem { - property string mode: "window" - icon: "web_asset" - text: qsTr("Record window") - activeText: qsTr("Window") -- onClicked: startRecording() -+ onClicked: startRecording(mode) - } - ] - } -@@ -156,7 +158,7 @@ StyledRect { - // Audio Sources Section - ColumnLayout { - Layout.fillWidth: true -- visible: !root.actuallyRecording -+ visible: !root.recordingBusy - spacing: Appearance.spacing.small - - RowLayout { -@@ -171,19 +173,31 @@ StyledRect { - Item { Layout.fillWidth: true } - - IconButton { -- icon: root.props.recordingAudioExpanded ? "expand_less" : "expand_more" -- type: IconButton.Tonal -- font.pointSize: Appearance.font.size.small -+ icon: root.props.recordingAudioExpanded ? "unfold_less" : "unfold_more" -+ type: IconButton.Text -+ label.animate: true - onClicked: { - root.props.recordingAudioExpanded = !root.props.recordingAudioExpanded; - } - } - } - -- ColumnLayout { -+ Item { -+ id: audioSourcesContainer -+ - Layout.fillWidth: true -- visible: root.props.recordingAudioExpanded -- spacing: Appearance.spacing.smaller -+ Layout.preferredHeight: root.props.recordingAudioExpanded ? audioSourcesLayout.implicitHeight : 0 -+ clip: true -+ enabled: root.props.recordingAudioExpanded -+ opacity: root.props.recordingAudioExpanded ? 1 : 0 -+ visible: root.props.recordingAudioExpanded || height > 0 -+ -+ ColumnLayout { -+ id: audioSourcesLayout -+ -+ width: parent.width -+ y: root.props.recordingAudioExpanded ? 0 : -Appearance.spacing.small -+ spacing: Appearance.spacing.smaller - - // System Audio (Default Sink) - RowLayout { -@@ -288,13 +302,26 @@ StyledRect { - } - } - } -+ -+ Behavior on y { -+ Anim { duration: Appearance.anim.durations.small } -+ } -+ } -+ -+ Behavior on Layout.preferredHeight { -+ Anim { type: Anim.DefaultSpatial } -+ } -+ -+ Behavior on opacity { -+ Anim { duration: Appearance.anim.durations.small } -+ } - } - } - - Loader { - id: listOrControls - -- property bool running: root.actuallyRecording -+ property bool running: root.recordingBusy - - Layout.fillWidth: true - Layout.preferredHeight: implicitHeight -@@ -371,7 +398,7 @@ StyledRect { - - StyledRect { - radius: Appearance.rounding.full -- color: Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error -+ color: Recorder.starting ? Colours.palette.m3secondary : Recorder.paused ? Colours.palette.m3tertiary : Colours.palette.m3error - - implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2 - implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2 -@@ -380,8 +407,8 @@ StyledRect { - id: recText - anchors.centerIn: parent - animate: true -- text: Recorder.paused ? "PAUSED" : "REC" -- color: Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError -+ text: Recorder.starting ? "WAIT" : Recorder.paused ? "PAUSED" : "REC" -+ color: Recorder.starting ? Colours.palette.m3onSecondary : Recorder.paused ? Colours.palette.m3onTertiary : Colours.palette.m3onError - font.family: Appearance.font.family.mono - } - -@@ -390,7 +417,7 @@ StyledRect { - } - - SequentialAnimation on opacity { -- running: !Recorder.paused && root.actuallyRecording -+ running: !Recorder.starting && !Recorder.paused && root.actuallyRecording - alwaysRunToEnd: true - loops: Animation.Infinite - Anim { -@@ -410,6 +437,9 @@ StyledRect { - - StyledText { - text: { -+ if (Recorder.starting) -+ return root.startingText(Recorder.videoMode || root.currentVideoMode); -+ - const elapsed = Recorder.elapsed; - const hours = Math.floor(elapsed / 3600); - const mins = Math.floor((elapsed % 3600) / 60); -@@ -429,6 +459,7 @@ StyledRect { - } - - IconButton { -+ disabled: Recorder.starting - label.animate: true - icon: Recorder.paused ? "play_arrow" : "pause" - toggle: true -@@ -450,19 +481,45 @@ StyledRect { - } - } - -- function startRecording() { -+ function videoModeLabel(mode) { -+ switch (mode) { -+ case "region": return qsTr("Region"); -+ case "window": return qsTr("Window"); -+ default: return qsTr("Fullscreen"); -+ } -+ } -+ -+ function audioModeLabel(mode) { -+ switch (mode) { -+ case "combined": return qsTr("system audio + microphone"); -+ case "system": return qsTr("system audio"); -+ case "mic": return qsTr("microphone"); -+ default: return qsTr("no audio"); -+ } -+ } -+ -+ function startingText(mode) { -+ switch (mode) { -+ case "region": return qsTr("Select a recording region"); -+ case "window": return qsTr("Select a window to record"); -+ default: return qsTr("Starting fullscreen recording"); -+ } -+ } -+ -+ function startRecording(videoMode) { - // Clear any previous errors - root.lastError = ""; - -- const videoMode = Config.utilities.recording.videoMode || "fullscreen"; -+ const selectedVideoMode = videoMode || Config.utilities.recording.videoMode || "fullscreen"; - const audioMode = root.currentAudioMode; - -- root.currentVideoMode = videoMode; -+ Config.utilities.recording.videoMode = selectedVideoMode; -+ root.currentVideoMode = selectedVideoMode; - -- console.log("Starting recording - Video:", videoMode, "Audio:", audioMode); -+ console.log("Starting recording - Video:", selectedVideoMode, "Audio:", audioMode); - - // Call Recorder service -- const success = Recorder.start(videoMode, audioMode); -+ const success = Recorder.start(selectedVideoMode, audioMode); - - if (!success) { - root.lastError = "Failed to start recording"; -@@ -516,6 +573,6 @@ StyledRect { - Component.onCompleted: { - // Sync initial state - root.actuallyRecording = Recorder.running; -- root.currentVideoMode = Config.utilities.recording.videoMode || "fullscreen"; -+ root.currentVideoMode = Recorder.videoMode || Config.utilities.recording.videoMode || "fullscreen"; - } - } -diff --git a/modules/utilities/cards/RecordingList.qml b/modules/utilities/cards/RecordingList.qml -index b9d757a4..951f7f09 100644 ---- a/modules/utilities/cards/RecordingList.qml -+++ b/modules/utilities/cards/RecordingList.qml -@@ -1,23 +1,22 @@ - pragma ComponentBehavior: Bound - -+import QtQuick -+import QtQuick.Layouts -+import Quickshell -+import Quickshell.Widgets -+import Caelestia.Config -+import Caelestia.Models - import qs.components --import qs.components.controls - import qs.components.containers -+import qs.components.controls - import qs.services --import qs.config - import qs.utils --import Caelestia --import Caelestia.Models --import Quickshell --import Quickshell.Widgets --import QtQuick --import QtQuick.Layouts - - ColumnLayout { - id: root - - required property var props -- required property var visibilities -+ required property DrawerVisibilities visibilities - - spacing: 0 - -@@ -28,19 +27,19 @@ ColumnLayout { - onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded - - RowLayout { -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - MaterialIcon { - Layout.alignment: Qt.AlignVCenter - text: "list" -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - - StyledText { - Layout.alignment: Qt.AlignVCenter - Layout.fillWidth: true - text: qsTr("Recordings") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - - IconButton { -@@ -62,8 +61,8 @@ ColumnLayout { - } - - Layout.fillWidth: true -- Layout.rightMargin: -Appearance.spacing.small -- implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3) -+ Layout.rightMargin: -Tokens.spacing.small -+ implicitHeight: (Tokens.font.size.larger + Tokens.padding.small) * (root.props.recordingListExpanded ? 10 : 3) - clip: true - - StyledScrollBar.vertical: StyledScrollBar { -@@ -78,14 +77,14 @@ ColumnLayout { - - anchors.left: list.contentItem.left - anchors.right: list.contentItem.right -- anchors.rightMargin: Appearance.spacing.small -- spacing: Appearance.spacing.small / 2 -+ anchors.rightMargin: Tokens.spacing.small -+ spacing: Tokens.spacing.small / 2 - - Component.onCompleted: baseName = modelData.baseName - - StyledText { - Layout.fillWidth: true -- Layout.rightMargin: Appearance.spacing.small / 2 -+ Layout.rightMargin: Tokens.spacing.small / 2 - text: { - const time = recording.baseName; - const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/); -@@ -105,7 +104,7 @@ ColumnLayout { - onClicked: { - root.visibilities.utilities = false; - root.visibilities.sidebar = false; -- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]); -+ Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.playback, recording.modelData.path]); - } - } - -@@ -115,7 +114,7 @@ ColumnLayout { - onClicked: { - root.visibilities.utilities = false; - root.visibilities.sidebar = false; -- Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]); -+ Quickshell.execDetached(["app2unit", "--", ...GlobalConfig.general.apps.explorer, recording.modelData.path]); - } - } - -@@ -163,19 +162,20 @@ ColumnLayout { - } - - Loader { -+ asynchronous: true - anchors.centerIn: parent - - opacity: list.count === 0 ? 1 : 0 - active: opacity > 0 - - sourceComponent: ColumnLayout { -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter - text: "scan_delete" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - - opacity: root.props.recordingListExpanded ? 1 : 0 - scale: root.props.recordingListExpanded ? 1 : 0 -@@ -195,7 +195,7 @@ ColumnLayout { - } - - RowLayout { -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - MaterialIcon { - Layout.alignment: Qt.AlignHCenter -@@ -233,8 +233,7 @@ ColumnLayout { - - Behavior on implicitHeight { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -diff --git a/modules/utilities/cards/Toggles.qml b/modules/utilities/cards/Toggles.qml -index 5b57528b..8c294eb3 100644 ---- a/modules/utilities/cards/Toggles.qml -+++ b/modules/utilities/cards/Toggles.qml -@@ -1,112 +1,169 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import QtQuick.Layouts -+import Quickshell.Bluetooth -+import Caelestia.Config - import qs.components - import qs.components.controls - import qs.services --import qs.config --import qs.modules.controlcenter --import Quickshell --import Quickshell.Bluetooth --import QtQuick --import QtQuick.Layouts -+import qs.modules.bar.popouts as BarPopouts - - StyledRect { - id: root - -- required property var visibilities -- required property Item popouts -+ required property DrawerVisibilities visibilities -+ required property BarPopouts.Wrapper popouts -+ -+ readonly property var quickToggles: { -+ const seenIds = new Set(); -+ -+ return Config.utilities.quickToggles.filter(item => { -+ if (!(item.enabled ?? true)) -+ return false; -+ -+ if (seenIds.has(item.id)) { -+ return false; -+ } -+ -+ if (item.id === "vpn") { -+ return GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false); -+ } -+ -+ seenIds.add(item.id); -+ return true; -+ }); -+ } -+ readonly property int splitIndex: Math.ceil(quickToggles.length / 2) -+ readonly property bool needExtraRow: quickToggles.length > 6 - - Layout.fillWidth: true -- implicitHeight: layout.implicitHeight + Appearance.padding.large * 2 -+ implicitHeight: layout.implicitHeight + Tokens.padding.large * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: Colours.tPalette.m3surfaceContainer - - ColumnLayout { - id: layout - - anchors.fill: parent -- anchors.margins: Appearance.padding.large -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.large -+ spacing: Tokens.spacing.normal - - StyledText { - text: qsTr("Quick Toggles") -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - -- RowLayout { -- Layout.alignment: Qt.AlignHCenter -- spacing: Appearance.spacing.small -- -- Toggle { -- icon: "wifi" -- checked: Network.wifiEnabled -- onClicked: Network.toggleWifi() -- } -+ QuickToggleRow { -+ rowModel: root.needExtraRow ? root.quickToggles.slice(0, root.splitIndex) : root.quickToggles -+ } - -- Toggle { -- icon: "bluetooth" -- checked: Bluetooth.defaultAdapter?.enabled ?? false -- onClicked: { -- const adapter = Bluetooth.defaultAdapter; -- if (adapter) -- adapter.enabled = !adapter.enabled; -- } -- } -+ QuickToggleRow { -+ visible: root.needExtraRow -+ rowModel: root.needExtraRow ? root.quickToggles.slice(root.splitIndex) : [] -+ } -+ } - -- Toggle { -- icon: "mic" -- checked: !Audio.sourceMuted -- onClicked: { -- const audio = Audio.source?.audio; -- if (audio) -- audio.muted = !audio.muted; -- } -- } -+ component QuickToggleRow: RowLayout { -+ property var rowModel: [] - -- Toggle { -- icon: "settings" -- inactiveOnColour: Colours.palette.m3onSurfaceVariant -- toggle: false -- onClicked: { -- root.visibilities.utilities = false; -- root.popouts.detach("network"); -- } -- } -+ Layout.fillWidth: true -+ spacing: Tokens.spacing.small - -- Toggle { -- icon: "gamepad" -- checked: GameMode.enabled -- onClicked: GameMode.enabled = !GameMode.enabled -- } -+ Repeater { -+ model: parent.rowModel - -- Toggle { -- icon: "notifications_off" -- checked: Notifs.dnd -- onClicked: Notifs.dnd = !Notifs.dnd -- } -+ delegate: DelegateChooser { -+ role: "id" - -- Toggle { -- icon: "vpn_key" -- checked: VPN.connected -- enabled: !VPN.connecting -- visible: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) -- onClicked: VPN.toggle() -+ DelegateChoice { -+ roleValue: "wifi" -+ delegate: Toggle { -+ icon: "wifi" -+ checked: Nmcli.wifiEnabled -+ onClicked: Nmcli.toggleWifi() -+ } -+ } -+ DelegateChoice { -+ roleValue: "bluetooth" -+ delegate: Toggle { -+ icon: "bluetooth" -+ checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type -+ onClicked: { -+ const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type -+ if (adapter) -+ adapter.enabled = !adapter.enabled; -+ } -+ } -+ } -+ DelegateChoice { -+ roleValue: "mic" -+ delegate: Toggle { -+ icon: "mic" -+ checked: !Audio.sourceMuted -+ onClicked: { -+ const audio = Audio.source?.audio; -+ if (audio) -+ audio.muted = !audio.muted; -+ } -+ } -+ } -+ DelegateChoice { -+ roleValue: "settings" -+ delegate: Toggle { -+ icon: "settings" -+ inactiveOnColour: Colours.palette.m3onSurfaceVariant -+ toggle: false -+ onClicked: { -+ root.visibilities.utilities = false; -+ root.popouts.detach("network"); -+ } -+ } -+ } -+ DelegateChoice { -+ roleValue: "gameMode" -+ delegate: Toggle { -+ icon: "gamepad" -+ checked: GameMode.enabled -+ onClicked: GameMode.enabled = !GameMode.enabled -+ } -+ } -+ DelegateChoice { -+ roleValue: "dnd" -+ delegate: Toggle { -+ icon: "notifications_off" -+ checked: Notifs.dnd -+ onClicked: Notifs.dnd = !Notifs.dnd -+ } -+ } -+ DelegateChoice { -+ roleValue: "vpn" -+ delegate: Toggle { -+ icon: "vpn_key" -+ checked: VPN.connected && VPN.status.state !== "needs-auth" && VPN.status.state !== "error" -+ enabled: !VPN.connecting -+ toggle: VPN.status.state !== "needs-auth" && VPN.status.state !== "error" -+ inactiveOnColour: Colours.palette.m3onSurfaceVariant -+ onClicked: VPN.toggle() -+ } -+ } - } - } - } - - component Toggle: IconButton { - Layout.fillWidth: true -- Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Appearance.padding.large : internalChecked ? Appearance.padding.smaller : 0) -- radius: stateLayer.pressed ? Appearance.rounding.small / 2 : internalChecked ? Appearance.rounding.small : Appearance.rounding.normal -+ Layout.preferredWidth: implicitWidth + (stateLayer.pressed ? Tokens.padding.large : internalChecked ? Tokens.padding.smaller : 0) -+ radius: stateLayer.pressed ? Tokens.rounding.small / 2 : internalChecked ? Tokens.rounding.small : Tokens.rounding.normal - inactiveColour: Colours.layer(Colours.palette.m3surfaceContainerHighest, 2) - toggle: true -- radiusAnim.duration: Appearance.anim.durations.expressiveFastSpatial -- radiusAnim.easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ radiusAnim.duration: Tokens.anim.durations.expressiveFastSpatial -+ radiusAnim.easing: Tokens.anim.expressiveFastSpatial - - Behavior on Layout.preferredWidth { - Anim { -- duration: Appearance.anim.durations.expressiveFastSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveFastSpatial -+ type: Anim.FastSpatial - } - } - } -diff --git a/modules/utilities/toasts/ToastItem.qml b/modules/utilities/toasts/ToastItem.qml -index f4755000..5247e773 100644 ---- a/modules/utilities/toasts/ToastItem.qml -+++ b/modules/utilities/toasts/ToastItem.qml -@@ -1,10 +1,10 @@ -+import QtQuick -+import QtQuick.Layouts -+import Caelestia -+import Caelestia.Config - import qs.components - import qs.components.effects - import qs.services --import qs.config --import Caelestia --import QtQuick --import QtQuick.Layouts - - StyledRect { - id: root -@@ -13,9 +13,9 @@ StyledRect { - - anchors.left: parent.left - anchors.right: parent.right -- implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: layout.implicitHeight + Tokens.padding.smaller * 2 - -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: { - if (root.modelData.type === Toast.Success) - return Colours.palette.m3successContainer; -@@ -50,13 +50,13 @@ StyledRect { - id: layout - - anchors.fill: parent -- anchors.margins: Appearance.padding.smaller -- anchors.leftMargin: Appearance.padding.normal -- anchors.rightMargin: Appearance.padding.normal -- spacing: Appearance.spacing.normal -+ anchors.margins: Tokens.padding.smaller -+ anchors.leftMargin: Tokens.padding.normal -+ anchors.rightMargin: Tokens.padding.normal -+ spacing: Tokens.spacing.normal - - StyledRect { -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - color: { - if (root.modelData.type === Toast.Success) - return Colours.palette.m3success; -@@ -68,7 +68,7 @@ StyledRect { - } - - implicitWidth: implicitHeight -- implicitHeight: icon.implicitHeight + Appearance.padding.smaller * 2 -+ implicitHeight: icon.implicitHeight + Tokens.padding.smaller * 2 - - MaterialIcon { - id: icon -@@ -84,7 +84,7 @@ StyledRect { - return Colours.palette.m3onError; - return Colours.palette.m3onSurfaceVariant; - } -- font.pointSize: Math.round(Appearance.font.size.large * 1.2) -+ font.pointSize: Math.round(Tokens.font.size.large * 1.2) - } - } - -@@ -106,7 +106,7 @@ StyledRect { - return Colours.palette.m3onErrorContainer; - return Colours.palette.m3onSurface; - } -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - elide: Text.ElideRight - } - -diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml -index 2915404e..21f2934e 100644 ---- a/modules/utilities/toasts/Toasts.qml -+++ b/modules/utilities/toasts/Toasts.qml -@@ -1,18 +1,29 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.config --import Caelestia --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root - -- readonly property int spacing: Appearance.spacing.small -+ readonly property int spacing: Tokens.spacing.small - property bool flag - -- implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 -+ function shouldShowToast(toast: Toast): bool { -+ if (!Notifs.hasFullscreen()) -+ return true; -+ if (Config.utilities.toasts.fullscreen === "all") -+ return true; -+ if (Config.utilities.toasts.fullscreen === "important") -+ return toast.type === Toast.Warning || toast.type === Toast.Error; -+ return false; -+ } -+ -+ implicitWidth: Tokens.sizes.utilities.toastWidth - Tokens.padding.normal * 2 - implicitHeight: { - let h = -spacing; - for (let i = 0; i < repeater.count; i++) { -@@ -31,10 +42,12 @@ Item { - const toasts = []; - let count = 0; - for (const toast of Toaster.toasts) { -+ if (!root.shouldShowToast(toast)) -+ continue; - toasts.push(toast); - if (!toast.closed) { - count++; -- if (count > Config.utilities.maxToasts) -+ if (count > root.Config.utilities.maxToasts) - break; - } - } -@@ -98,8 +111,7 @@ Item { - properties: "opacity,scale" - from: 0 - to: 1 -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - - ParallelAnimation { -@@ -135,8 +147,7 @@ Item { - - Behavior on anchors.bottomMargin { - Anim { -- duration: Appearance.anim.durations.expressiveDefaultSpatial -- easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial -+ type: Anim.DefaultSpatial - } - } - } -diff --git a/modules/windowinfo/Buttons.qml b/modules/windowinfo/Buttons.qml -index 89acfe6d..c4fbbd53 100644 ---- a/modules/windowinfo/Buttons.qml -+++ b/modules/windowinfo/Buttons.qml -@@ -1,9 +1,11 @@ --import qs.components --import qs.services --import qs.config --import Quickshell.Widgets -+pragma ComponentBehavior: Bound -+ - import QtQuick - import QtQuick.Layouts -+import Quickshell.Widgets -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root -@@ -12,14 +14,14 @@ ColumnLayout { - property bool moveToWsExpanded - - anchors.fill: parent -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - RowLayout { -- Layout.topMargin: Appearance.padding.large -- Layout.leftMargin: Appearance.padding.large -- Layout.rightMargin: Appearance.padding.large -+ Layout.topMargin: Tokens.padding.large -+ Layout.leftMargin: Tokens.padding.large -+ Layout.rightMargin: Tokens.padding.large - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true -@@ -29,17 +31,14 @@ ColumnLayout { - - StyledRect { - color: Colours.palette.m3primary -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - -- implicitWidth: moveToWsIcon.implicitWidth + Appearance.padding.small * 2 -- implicitHeight: moveToWsIcon.implicitHeight + Appearance.padding.small -+ implicitWidth: moveToWsIcon.implicitWidth + Tokens.padding.small * 2 -+ implicitHeight: moveToWsIcon.implicitHeight + Tokens.padding.small - - StateLayer { - color: Colours.palette.m3onPrimary -- -- function onClicked(): void { -- root.moveToWsExpanded = !root.moveToWsExpanded; -- } -+ onClicked: root.moveToWsExpanded = !root.moveToWsExpanded - } - - MaterialIcon { -@@ -50,27 +49,27 @@ ColumnLayout { - animate: true - text: root.moveToWsExpanded ? "expand_more" : "keyboard_arrow_right" - color: Colours.palette.m3onPrimary -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } - - WrapperItem { - Layout.fillWidth: true -- Layout.leftMargin: Appearance.padding.large * 2 -- Layout.rightMargin: Appearance.padding.large * 2 -+ Layout.leftMargin: Tokens.padding.large * 2 -+ Layout.rightMargin: Tokens.padding.large * 2 - - Layout.preferredHeight: root.moveToWsExpanded ? implicitHeight : 0 - clip: true - -- topMargin: Appearance.spacing.normal -- bottomMargin: Appearance.spacing.normal -+ topMargin: Tokens.spacing.normal -+ bottomMargin: Tokens.spacing.normal - - GridLayout { - id: wsGrid - -- rowSpacing: Appearance.spacing.smaller -- columnSpacing: Appearance.spacing.normal -+ rowSpacing: Tokens.spacing.smaller -+ columnSpacing: Tokens.spacing.normal - columns: 5 - - Repeater { -@@ -81,14 +80,14 @@ ColumnLayout { - readonly property int wsId: Math.floor((Hypr.activeWsId - 1) / 10) * 10 + index + 1 - readonly property bool isCurrent: root.client?.workspace.id === wsId - -+ onClicked: { -+ Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); -+ } -+ - color: isCurrent ? Colours.tPalette.m3surfaceContainerHighest : Colours.palette.m3tertiaryContainer - onColor: isCurrent ? Colours.palette.m3onSurface : Colours.palette.m3onTertiaryContainer - text: wsId - disabled: isCurrent -- -- function onClicked(): void { -- Hypr.dispatch(`movetoworkspace ${wsId},address:0x${root.client?.address}`); -- } - } - } - } -@@ -100,24 +99,22 @@ ColumnLayout { - - RowLayout { - Layout.fillWidth: true -- Layout.leftMargin: Appearance.padding.large -- Layout.rightMargin: Appearance.padding.large -- Layout.bottomMargin: Appearance.padding.large -+ Layout.leftMargin: Tokens.padding.large -+ Layout.rightMargin: Tokens.padding.large -+ Layout.bottomMargin: Tokens.padding.large - -- spacing: root.client?.lastIpcObject.floating ? Appearance.spacing.normal : Appearance.spacing.small -+ spacing: root.client?.lastIpcObject.floating ? Tokens.spacing.normal : Tokens.spacing.small - - Button { - color: Colours.palette.m3secondaryContainer - onColor: Colours.palette.m3onSecondaryContainer - text: root.client?.lastIpcObject.floating ? qsTr("Tile") : qsTr("Float") -- -- function onClicked(): void { -- Hypr.dispatch(`togglefloating address:0x${root.client?.address}`); -- } -+ onClicked: Hypr.dispatch(`togglefloating address:0x${root.client?.address}`) - } - - Loader { -- active: root.client?.lastIpcObject.floating -+ asynchronous: true -+ active: root.client?.lastIpcObject.floating ?? false - Layout.fillWidth: active - Layout.leftMargin: active ? 0 : -parent.spacing - Layout.rightMargin: active ? 0 : -parent.spacing -@@ -126,10 +123,7 @@ ColumnLayout { - color: Colours.palette.m3secondaryContainer - onColor: Colours.palette.m3onSecondaryContainer - text: root.client?.lastIpcObject.pinned ? qsTr("Unpin") : qsTr("Pin") -- -- function onClicked(): void { -- Hypr.dispatch(`pin address:0x${root.client?.address}`); -- } -+ onClicked: Hypr.dispatch(`pin address:0x${root.client?.address}`) - } - } - -@@ -137,10 +131,7 @@ ColumnLayout { - color: Colours.palette.m3errorContainer - onColor: Colours.palette.m3onErrorContainer - text: qsTr("Kill") -- -- function onClicked(): void { -- Hypr.dispatch(`killwindow address:0x${root.client?.address}`); -- } -+ onClicked: Hypr.dispatch(`killwindow address:0x${root.client?.address}`) - } - } - -@@ -149,22 +140,18 @@ ColumnLayout { - property alias disabled: stateLayer.disabled - property alias text: label.text - -- function onClicked(): void { -- } -+ signal clicked - -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - - Layout.fillWidth: true -- implicitHeight: label.implicitHeight + Appearance.padding.small * 2 -+ implicitHeight: label.implicitHeight + Tokens.padding.small * 2 - - StateLayer { - id: stateLayer - - color: parent.onColor -- -- function onClicked(): void { -- parent.onClicked(); -- } -+ onClicked: parent.clicked() - } - - StyledText { -@@ -174,7 +161,7 @@ ColumnLayout { - - animate: true - color: parent.onColor -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - } - } -diff --git a/modules/windowinfo/Details.qml b/modules/windowinfo/Details.qml -index f9ee66a6..1820409f 100644 ---- a/modules/windowinfo/Details.qml -+++ b/modules/windowinfo/Details.qml -@@ -1,9 +1,9 @@ --import qs.components --import qs.services --import qs.config --import Quickshell.Hyprland - import QtQuick - import QtQuick.Layouts -+import Quickshell.Hyprland -+import Caelestia.Config -+import qs.components -+import qs.services - - ColumnLayout { - id: root -@@ -11,15 +11,15 @@ ColumnLayout { - required property HyprlandToplevel client - - anchors.fill: parent -- spacing: Appearance.spacing.small -+ spacing: Tokens.spacing.small - - Label { -- Layout.topMargin: Appearance.padding.large * 2 -+ Layout.topMargin: Tokens.padding.large * 2 - - text: root.client?.title ?? qsTr("No active client") - wrapMode: Text.WrapAtWordBoundaryOrAnywhere - -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - font.weight: 500 - } - -@@ -27,16 +27,16 @@ ColumnLayout { - text: root.client?.lastIpcObject.class ?? qsTr("No active client") - color: Colours.palette.m3tertiary - -- font.pointSize: Appearance.font.size.larger -+ font.pointSize: Tokens.font.size.larger - } - - StyledRect { - Layout.fillWidth: true - Layout.preferredHeight: 1 -- Layout.leftMargin: Appearance.padding.large * 2 -- Layout.rightMargin: Appearance.padding.large * 2 -- Layout.topMargin: Appearance.spacing.normal -- Layout.bottomMargin: Appearance.spacing.large -+ Layout.leftMargin: Tokens.padding.large * 2 -+ Layout.rightMargin: Tokens.padding.large * 2 -+ Layout.topMargin: Tokens.spacing.normal -+ Layout.bottomMargin: Tokens.spacing.large - - color: Colours.palette.m3secondary - } -@@ -130,11 +130,11 @@ ColumnLayout { - required property string text - property alias color: icon.color - -- Layout.leftMargin: Appearance.padding.large -- Layout.rightMargin: Appearance.padding.large -+ Layout.leftMargin: Tokens.padding.large -+ Layout.rightMargin: Tokens.padding.large - Layout.fillWidth: true - -- spacing: Appearance.spacing.smaller -+ spacing: Tokens.spacing.smaller - - MaterialIcon { - id: icon -@@ -149,13 +149,13 @@ ColumnLayout { - - text: detail.text - elide: Text.ElideRight -- font.pointSize: Appearance.font.size.normal -+ font.pointSize: Tokens.font.size.normal - } - } - - component Label: StyledText { -- Layout.leftMargin: Appearance.padding.large -- Layout.rightMargin: Appearance.padding.large -+ Layout.leftMargin: Tokens.padding.large -+ Layout.rightMargin: Tokens.padding.large - Layout.fillWidth: true - elide: Text.ElideRight - horizontalAlignment: Text.AlignHCenter -diff --git a/modules/windowinfo/Preview.qml b/modules/windowinfo/Preview.qml -index 4cc0aab8..fed85891 100644 ---- a/modules/windowinfo/Preview.qml -+++ b/modules/windowinfo/Preview.qml -@@ -1,13 +1,13 @@ - pragma ComponentBehavior: Bound - --import qs.components --import qs.services --import qs.config --import Quickshell --import Quickshell.Wayland --import Quickshell.Hyprland - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Quickshell.Hyprland -+import Quickshell.Wayland -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root -@@ -15,7 +15,7 @@ Item { - required property ShellScreen screen - required property HyprlandToplevel client - -- Layout.preferredWidth: preview.implicitWidth + Appearance.padding.large * 2 -+ Layout.preferredWidth: preview.implicitWidth + Tokens.padding.large * 2 - Layout.fillHeight: true - - StyledClippingRect { -@@ -24,15 +24,16 @@ Item { - anchors.horizontalCenter: parent.horizontalCenter - anchors.top: parent.top - anchors.bottom: label.top -- anchors.topMargin: Appearance.padding.large -- anchors.bottomMargin: Appearance.spacing.normal -+ anchors.topMargin: Tokens.padding.large -+ anchors.bottomMargin: Tokens.spacing.normal - - implicitWidth: view.implicitWidth - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.small -+ radius: Tokens.rounding.small - - Loader { -+ asynchronous: true - anchors.centerIn: parent - active: !root.client - -@@ -43,14 +44,14 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: "web_asset_off" - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.extraLarge * 3 -+ font.pointSize: Tokens.font.size.extraLarge * 3 - } - - StyledText { - Layout.alignment: Qt.AlignHCenter - text: qsTr("No active client") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.extraLarge -+ font.pointSize: Tokens.font.size.extraLarge - font.weight: 500 - } - -@@ -58,7 +59,7 @@ Item { - Layout.alignment: Qt.AlignHCenter - text: qsTr("Try switching to a window") - color: Colours.palette.m3outline -- font.pointSize: Appearance.font.size.large -+ font.pointSize: Tokens.font.size.large - } - } - } -@@ -68,7 +69,7 @@ Item { - - anchors.centerIn: parent - -- captureSource: root.client?.wayland ?? null -+ captureSource: root.client?.wayland ?? null // qmllint disable unresolved-type - live: true - - constraintSize.width: root.client ? parent.height * Math.min(root.screen.width / root.screen.height, root.client?.lastIpcObject.size[0] / root.client?.lastIpcObject.size[1]) : parent.height -@@ -81,7 +82,7 @@ Item { - - anchors.horizontalCenter: parent.horizontalCenter - anchors.bottom: parent.bottom -- anchors.bottomMargin: Appearance.padding.large -+ anchors.bottomMargin: Tokens.padding.large - - animate: true - text: { -diff --git a/modules/windowinfo/WindowInfo.qml b/modules/windowinfo/WindowInfo.qml -index 919b3fbb..7ee35c29 100644 ---- a/modules/windowinfo/WindowInfo.qml -+++ b/modules/windowinfo/WindowInfo.qml -@@ -1,10 +1,10 @@ --import qs.components --import qs.services --import qs.config --import Quickshell --import Quickshell.Hyprland - import QtQuick - import QtQuick.Layouts -+import Quickshell -+import Quickshell.Hyprland -+import Caelestia.Config -+import qs.components -+import qs.services - - Item { - id: root -@@ -13,15 +13,15 @@ Item { - required property HyprlandToplevel client - - implicitWidth: child.implicitWidth -- implicitHeight: screen.height * Config.winfo.sizes.heightMult -+ implicitHeight: screen.height * Tokens.sizes.winfo.heightMult - - RowLayout { - id: child - - anchors.fill: parent -- anchors.margins: Appearance.padding.large -+ anchors.margins: Tokens.padding.large - -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - - Preview { - screen: root.screen -@@ -29,9 +29,9 @@ Item { - } - - ColumnLayout { -- spacing: Appearance.spacing.normal -+ spacing: Tokens.spacing.normal - -- Layout.preferredWidth: Config.winfo.sizes.detailsWidth -+ Layout.preferredWidth: Tokens.sizes.winfo.detailsWidth - Layout.fillHeight: true - - StyledRect { -@@ -39,7 +39,7 @@ Item { - Layout.fillHeight: true - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - Details { - client: root.client -@@ -51,7 +51,7 @@ Item { - Layout.preferredHeight: buttons.implicitHeight - - color: Colours.tPalette.m3surfaceContainer -- radius: Appearance.rounding.normal -+ radius: Tokens.rounding.normal - - Buttons { - id: buttons -diff --git a/nix/app2unit.nix b/nix/app2unit.nix -deleted file mode 100644 -index 51b4241c..00000000 ---- a/nix/app2unit.nix -+++ /dev/null -@@ -1,14 +0,0 @@ --{ -- pkgs, # To ensure the nixpkgs version of app2unit -- fetchFromGitHub, -- ... --}: --pkgs.app2unit.overrideAttrs (final: prev: rec { -- version = "1.0.3"; # Fix old issue related to missing env var -- src = fetchFromGitHub { -- owner = "Vladimir-csp"; -- repo = "app2unit"; -- tag = "v${version}"; -- hash = "sha256-7eEVjgs+8k+/NLteSBKgn4gPaPLHC+3Uzlmz6XB0930="; -- }; --}) -diff --git a/nix/default.nix b/nix/default.nix -index 67747b2d..3a153dd0 100644 ---- a/nix/default.nix -+++ b/nix/default.nix -@@ -126,8 +126,6 @@ in - prePatch = '' - substituteInPlace assets/pam.d/fprint \ - --replace-fail pam_fprintd.so /run/current-system/sw/lib/security/pam_fprintd.so -- substituteInPlace shell.qml \ -- --replace-fail 'ShellRoot {' 'ShellRoot { settings.watchFiles: false' - ''; - - postInstall = '' -diff --git a/plugin/src/Caelestia/Blobs/CMakeLists.txt b/plugin/src/Caelestia/Blobs/CMakeLists.txt -new file mode 100644 -index 00000000..9506f7f4 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/CMakeLists.txt -@@ -0,0 +1,20 @@ -+qml_module(caelestia-blobs -+ URI Caelestia.Blobs -+ SOURCES -+ blobgroup.cpp -+ blobshape.cpp -+ blobrect.cpp -+ blobinvertedrect.cpp -+ blobmaterial.cpp -+ LIBRARIES -+ Qt::Quick -+) -+ -+qt_add_shaders(caelestia-blobs "blob_shaders" -+ BATCHABLE OPTIMIZED NOHLSL NOMSL -+ GLSL "300es,330" -+ PREFIX "/" -+ FILES -+ shaders/blob.frag -+ shaders/blob.vert -+) -diff --git a/plugin/src/Caelestia/Blobs/blobgroup.cpp b/plugin/src/Caelestia/Blobs/blobgroup.cpp -new file mode 100644 -index 00000000..a4703c86 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobgroup.cpp -@@ -0,0 +1,104 @@ -+#include "blobgroup.hpp" -+#include "blobinvertedrect.hpp" -+#include "blobshape.hpp" -+ -+BlobGroup::BlobGroup(QObject* parent) -+ : QObject(parent) {} -+ -+BlobGroup::~BlobGroup() { -+ for (auto* shape : std::as_const(m_shapes)) -+ shape->m_group = nullptr; -+ if (m_invertedRect) -+ static_cast(m_invertedRect)->m_group = nullptr; -+} -+ -+void BlobGroup::setSmoothing(qreal s) { -+ if (qFuzzyCompare(m_smoothing, s)) -+ return; -+ m_smoothing = s; -+ emit smoothingChanged(); -+ markDirty(); -+} -+ -+void BlobGroup::setColor(const QColor& c) { -+ if (m_color == c) -+ return; -+ m_color = c; -+ emit colorChanged(); -+ markDirty(); -+} -+ -+void BlobGroup::addShape(BlobShape* shape) { -+ if (!shape || m_shapes.contains(shape)) -+ return; -+ m_shapes.append(shape); -+ markDirty(); -+} -+ -+void BlobGroup::removeShape(BlobShape* shape) { -+ m_shapes.removeOne(shape); -+ markDirty(); -+} -+ -+void BlobGroup::setInvertedRect(BlobInvertedRect* rect) { -+ if (m_invertedRect == rect) -+ return; -+ m_invertedRect = rect; -+ markDirty(); -+} -+ -+void BlobGroup::clearInvertedRect(BlobInvertedRect* rect) { -+ if (m_invertedRect != rect) -+ return; -+ m_invertedRect = nullptr; -+ markDirty(); -+} -+ -+void BlobGroup::markDirty() { -+ m_physicsUpdated = false; -+ for (auto* shape : std::as_const(m_shapes)) { -+ shape->polish(); -+ shape->update(); -+ } -+ if (m_invertedRect) { -+ static_cast(m_invertedRect)->polish(); -+ static_cast(m_invertedRect)->update(); -+ } -+} -+ -+void BlobGroup::markShapeDirty(BlobShape* source) { -+ m_physicsUpdated = false; -+ -+ source->polish(); -+ source->update(); -+ -+ // Use cached padded rects to find spatial neighbors -+ const float pad = static_cast(m_smoothing) * 2.0f; -+ const QRectF srcRect(static_cast(source->m_cachedPaddedX - pad), -+ static_cast(source->m_cachedPaddedY - pad), static_cast(source->m_cachedPaddedW + pad * 2.0f), -+ static_cast(source->m_cachedPaddedH + pad * 2.0f)); -+ -+ for (auto* shape : std::as_const(m_shapes)) { -+ if (shape == source) -+ continue; -+ const QRectF otherRect(static_cast(shape->m_cachedPaddedX), static_cast(shape->m_cachedPaddedY), -+ static_cast(shape->m_cachedPaddedW), static_cast(shape->m_cachedPaddedH)); -+ if (srcRect.intersects(otherRect)) { -+ shape->polish(); -+ shape->update(); -+ } -+ } -+ -+ if (m_invertedRect && static_cast(m_invertedRect) != source) { -+ static_cast(m_invertedRect)->polish(); -+ static_cast(m_invertedRect)->update(); -+ } -+} -+ -+void BlobGroup::ensurePhysicsUpdated() { -+ if (m_physicsUpdated) -+ return; -+ m_physicsUpdated = true; -+ for (auto* shape : std::as_const(m_shapes)) -+ shape->updatePhysics(); -+} -diff --git a/plugin/src/Caelestia/Blobs/blobgroup.hpp b/plugin/src/Caelestia/Blobs/blobgroup.hpp -new file mode 100644 -index 00000000..e09125a9 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobgroup.hpp -@@ -0,0 +1,53 @@ -+#pragma once -+ -+#include -+#include -+#include -+#include -+ -+class BlobShape; -+class BlobInvertedRect; -+ -+class BlobGroup : public QObject { -+ Q_OBJECT -+ QML_ELEMENT -+ Q_PROPERTY(qreal smoothing READ smoothing WRITE setSmoothing NOTIFY smoothingChanged) -+ Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged) -+ -+public: -+ explicit BlobGroup(QObject* parent = nullptr); -+ ~BlobGroup() override; -+ -+ qreal smoothing() const { return m_smoothing; } -+ -+ void setSmoothing(qreal s); -+ -+ QColor color() const { return m_color; } -+ -+ void setColor(const QColor& c); -+ -+ void addShape(BlobShape* shape); -+ void removeShape(BlobShape* shape); -+ -+ void setInvertedRect(BlobInvertedRect* rect); -+ void clearInvertedRect(BlobInvertedRect* rect); -+ -+ const QList& shapes() const { return m_shapes; } -+ -+ BlobInvertedRect* invertedRect() const { return m_invertedRect; } -+ -+ void markDirty(); -+ void markShapeDirty(BlobShape* source); -+ void ensurePhysicsUpdated(); -+ -+signals: -+ void smoothingChanged(); -+ void colorChanged(); -+ -+private: -+ qreal m_smoothing = 32.0; -+ QColor m_color{ 0x44, 0x88, 0xff }; -+ QList m_shapes; -+ BlobInvertedRect* m_invertedRect = nullptr; -+ bool m_physicsUpdated = false; -+}; -diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp -new file mode 100644 -index 00000000..46ee73a4 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.cpp -@@ -0,0 +1,184 @@ -+#include "blobinvertedrect.hpp" -+#include "blobgroup.hpp" -+#include "blobmaterial.hpp" -+ -+#include -+#include -+ -+#include -+#include -+ -+BlobInvertedRect::BlobInvertedRect(QQuickItem* parent) -+ : BlobShape(parent) {} -+ -+static void setFrameIndices(quint16* idx) { -+ // Top strip: 0-1-4, 1-5-4 -+ idx[0] = 0; -+ idx[1] = 1; -+ idx[2] = 4; -+ idx[3] = 1; -+ idx[4] = 5; -+ idx[5] = 4; -+ // Right strip: 1-2-5, 2-6-5 -+ idx[6] = 1; -+ idx[7] = 2; -+ idx[8] = 5; -+ idx[9] = 2; -+ idx[10] = 6; -+ idx[11] = 5; -+ // Bottom strip: 2-3-6, 3-7-6 -+ idx[12] = 2; -+ idx[13] = 3; -+ idx[14] = 6; -+ idx[15] = 3; -+ idx[16] = 7; -+ idx[17] = 6; -+ // Left strip: 3-0-7, 0-4-7 -+ idx[18] = 3; -+ idx[19] = 0; -+ idx[20] = 7; -+ idx[21] = 0; -+ idx[22] = 4; -+ idx[23] = 7; -+} -+ -+QSGNode* BlobInvertedRect::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { -+ if (!m_group) { -+ delete oldNode; -+ return nullptr; -+ } -+ -+ const float pad = static_cast(m_group->smoothing()); -+ -+ // Compute inner hole boundary in local coords -+ // Inset past the inner border edge by 2x smoothing to cover the blend zone -+ const float inset = pad * 2.0f; -+ const float holeLeft = static_cast(m_borderLeft) + inset; -+ const float holeTop = static_cast(m_borderTop) + inset; -+ const float holeRight = static_cast(width() - m_borderRight) - inset; -+ const float holeBot = static_cast(height() - m_borderBottom) - inset; -+ -+ // If the hole is too small or invalid, fall back to full quad -+ if (holeLeft >= holeRight || holeTop >= holeBot) -+ return BlobShape::updatePaintNode(oldNode, nullptr); -+ -+ auto* node = static_cast(oldNode); -+ -+ const bool needsRebuild = !node || node->geometry()->vertexCount() != 8; -+ -+ if (needsRebuild) { -+ delete oldNode; -+ node = new QSGGeometryNode; -+ -+ auto* geometry = -+ new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 8, 24, QSGGeometry::UnsignedShortType); -+ geometry->setDrawingMode(QSGGeometry::DrawTriangles); -+ node->setGeometry(geometry); -+ node->setFlag(QSGNode::OwnsGeometry); -+ -+ setFrameIndices(geometry->indexDataAsUShort()); -+ -+ auto* material = new BlobMaterial; -+ material->setFlag(QSGMaterial::Blending); -+ node->setMaterial(material); -+ node->setFlag(QSGNode::OwnsMaterial); -+ } -+ -+ // Outer bounds (local coords) -+ const float x0 = static_cast(m_localPaddedRect.x()); -+ const float y0 = static_cast(m_localPaddedRect.y()); -+ const float x1 = x0 + static_cast(m_localPaddedRect.width()); -+ const float y1 = y0 + static_cast(m_localPaddedRect.height()); -+ const float w = x1 - x0; -+ const float h = y1 - y0; -+ -+ // Update vertex positions and texture coordinates -+ auto* v = node->geometry()->vertexDataAsTexturedPoint2D(); -+ -+ // Outer corners -+ v[0].set(x0, y0, 0.0f, 0.0f); -+ v[1].set(x1, y0, 1.0f, 0.0f); -+ v[2].set(x1, y1, 1.0f, 1.0f); -+ v[3].set(x0, y1, 0.0f, 1.0f); -+ // Inner corners (hole) -+ v[4].set(holeLeft, holeTop, (holeLeft - x0) / w, (holeTop - y0) / h); -+ v[5].set(holeRight, holeTop, (holeRight - x0) / w, (holeTop - y0) / h); -+ v[6].set(holeRight, holeBot, (holeRight - x0) / w, (holeBot - y0) / h); -+ v[7].set(holeLeft, holeBot, (holeLeft - x0) / w, (holeBot - y0) / h); -+ -+ node->markDirty(QSGNode::DirtyGeometry); -+ -+ // Update material uniforms -+ auto* material = static_cast(node->material()); -+ material->m_paddedX = m_cachedPaddedX; -+ material->m_paddedY = m_cachedPaddedY; -+ material->m_paddedW = m_cachedPaddedW; -+ material->m_paddedH = m_cachedPaddedH; -+ material->m_smoothFactor = pad; -+ material->m_myIndex = m_cachedMyIndex; -+ material->m_color = m_group->color(); -+ material->m_hasInverted = m_cachedHasInverted ? 1 : 0; -+ material->m_invertedRadius = m_cachedInvertedRadius; -+ memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); -+ memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); -+ -+ const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); -+ material->m_rectCount = count; -+ for (int i = 0; i < count; ++i) -+ material->m_rects[i] = m_cachedRects[i]; -+ -+ node->markDirty(QSGNode::DirtyMaterial); -+ -+ return node; -+} -+ -+BlobInvertedRect::~BlobInvertedRect() { -+ if (m_group) -+ m_group->clearInvertedRect(this); -+} -+ -+void BlobInvertedRect::setBorderLeft(qreal v) { -+ if (qFuzzyCompare(m_borderLeft, v)) -+ return; -+ m_borderLeft = v; -+ emit borderLeftChanged(); -+ if (m_group) -+ m_group->markDirty(); -+} -+ -+void BlobInvertedRect::setBorderRight(qreal v) { -+ if (qFuzzyCompare(m_borderRight, v)) -+ return; -+ m_borderRight = v; -+ emit borderRightChanged(); -+ if (m_group) -+ m_group->markDirty(); -+} -+ -+void BlobInvertedRect::setBorderTop(qreal v) { -+ if (qFuzzyCompare(m_borderTop, v)) -+ return; -+ m_borderTop = v; -+ emit borderTopChanged(); -+ if (m_group) -+ m_group->markDirty(); -+} -+ -+void BlobInvertedRect::setBorderBottom(qreal v) { -+ if (qFuzzyCompare(m_borderBottom, v)) -+ return; -+ m_borderBottom = v; -+ emit borderBottomChanged(); -+ if (m_group) -+ m_group->markDirty(); -+} -+ -+void BlobInvertedRect::registerWithGroup() { -+ if (m_group) -+ m_group->setInvertedRect(this); -+} -+ -+void BlobInvertedRect::unregisterFromGroup() { -+ if (m_group) -+ m_group->clearInvertedRect(this); -+} -diff --git a/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp -new file mode 100644 -index 00000000..f7fa6c0a ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobinvertedrect.hpp -@@ -0,0 +1,54 @@ -+#pragma once -+ -+#include "blobshape.hpp" -+ -+#include -+ -+class BlobInvertedRect : public BlobShape { -+ Q_OBJECT -+ QML_ELEMENT -+ Q_PROPERTY(qreal borderLeft READ borderLeft WRITE setBorderLeft NOTIFY borderLeftChanged) -+ Q_PROPERTY(qreal borderRight READ borderRight WRITE setBorderRight NOTIFY borderRightChanged) -+ Q_PROPERTY(qreal borderTop READ borderTop WRITE setBorderTop NOTIFY borderTopChanged) -+ Q_PROPERTY(qreal borderBottom READ borderBottom WRITE setBorderBottom NOTIFY borderBottomChanged) -+ -+public: -+ explicit BlobInvertedRect(QQuickItem* parent = nullptr); -+ ~BlobInvertedRect() override; -+ -+ qreal borderLeft() const { return m_borderLeft; } -+ -+ void setBorderLeft(qreal v); -+ -+ qreal borderRight() const { return m_borderRight; } -+ -+ void setBorderRight(qreal v); -+ -+ qreal borderTop() const { return m_borderTop; } -+ -+ void setBorderTop(qreal v); -+ -+ qreal borderBottom() const { return m_borderBottom; } -+ -+ void setBorderBottom(qreal v); -+ -+signals: -+ void borderLeftChanged(); -+ void borderRightChanged(); -+ void borderTopChanged(); -+ void borderBottomChanged(); -+ -+protected: -+ bool isInvertedRect() const override { return true; } -+ -+ QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; -+ -+ void registerWithGroup() override; -+ void unregisterFromGroup() override; -+ -+private: -+ qreal m_borderLeft = 0; -+ qreal m_borderRight = 0; -+ qreal m_borderTop = 0; -+ qreal m_borderBottom = 0; -+}; -diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.cpp b/plugin/src/Caelestia/Blobs/blobmaterial.cpp -new file mode 100644 -index 00000000..721a2532 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobmaterial.cpp -@@ -0,0 +1,102 @@ -+#include "blobmaterial.hpp" -+ -+#include -+ -+static_assert(sizeof(decltype(BlobRectData::excludeMask)) == sizeof(float), -+ "BlobMaterial packs excludeMask into a float slot via memcpy"); -+ -+QSGMaterialType* BlobMaterial::type() const { -+ static QSGMaterialType s_type; -+ return &s_type; -+} -+ -+QSGMaterialShader* BlobMaterial::createShader(QSGRendererInterface::RenderMode) const { -+ return new BlobMaterialShader; -+} -+ -+int BlobMaterial::compare(const QSGMaterial* other) const { -+ if (this < other) -+ return -1; -+ if (this > other) -+ return 1; -+ return 0; -+} -+ -+BlobMaterialShader::BlobMaterialShader() { -+ setShaderFileName(VertexStage, QStringLiteral(":/shaders/blob.vert.qsb")); -+ setShaderFileName(FragmentStage, QStringLiteral(":/shaders/blob.frag.qsb")); -+} -+ -+bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) { -+ Q_UNUSED(oldMaterial); -+ auto* mat = static_cast(newMaterial); -+ QByteArray* buf = state.uniformData(); -+ Q_ASSERT(buf->size() >= 1440); -+ -+ if (state.isMatrixDirty()) { -+ const QMatrix4x4 m = state.combinedMatrix(); -+ memcpy(buf->data(), m.constData(), 64); -+ } -+ if (state.isOpacityDirty()) { -+ const float opacity = state.opacity(); -+ memcpy(buf->data() + 64, &opacity, 4); -+ } -+ -+ // Padded rect (offset 68) -+ memcpy(buf->data() + 68, &mat->m_paddedX, 4); -+ memcpy(buf->data() + 72, &mat->m_paddedY, 4); -+ memcpy(buf->data() + 76, &mat->m_paddedW, 4); -+ memcpy(buf->data() + 80, &mat->m_paddedH, 4); -+ -+ // Smooth factor (offset 84) -+ memcpy(buf->data() + 84, &mat->m_smoothFactor, 4); -+ -+ // Rect count (offset 88) -+ memcpy(buf->data() + 88, &mat->m_rectCount, 4); -+ -+ // My index (offset 92) -+ memcpy(buf->data() + 92, &mat->m_myIndex, 4); -+ -+ // Color as vec4 (offset 96, 16 bytes) -+ const float color[4] = { -+ static_cast(mat->m_color.redF()), -+ static_cast(mat->m_color.greenF()), -+ static_cast(mat->m_color.blueF()), -+ static_cast(mat->m_color.alphaF()), -+ }; -+ memcpy(buf->data() + 96, color, 16); -+ -+ // Has inverted (offset 112) -+ memcpy(buf->data() + 112, &mat->m_hasInverted, 4); -+ -+ // Inverted radius (offset 116) -+ memcpy(buf->data() + 116, &mat->m_invertedRadius, 4); -+ -+ // Padding at 120-127 (skip) -+ -+ // Inverted outer (offset 128, 16 bytes) -+ memcpy(buf->data() + 128, mat->m_invertedOuter, 16); -+ -+ // Inverted inner (offset 144, 16 bytes) -+ memcpy(buf->data() + 144, mat->m_invertedInner, 16); -+ -+ // Rect data (offset 160, each rect = 5 vec4s = 80 bytes) -+ const int count = qMin(mat->m_rectCount, 16); -+ for (int i = 0; i < count; ++i) { -+ const auto& r = mat->m_rects[i]; -+ const int base = 160 + i * 80; -+ // Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt) -+ float maskAsFloat; -+ memcpy(&maskAsFloat, &r.excludeMask, sizeof(float)); -+ const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; -+ const float d1[4] = { maskAsFloat, r.offsetX, r.offsetY, r.minEig }; -+ const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; -+ memcpy(buf->data() + base, d0, 16); -+ memcpy(buf->data() + base + 16, d1, 16); -+ memcpy(buf->data() + base + 32, r.invDeform, 16); -+ memcpy(buf->data() + base + 48, d3, 16); -+ memcpy(buf->data() + base + 64, r.radius, 16); -+ } -+ -+ return true; -+} -diff --git a/plugin/src/Caelestia/Blobs/blobmaterial.hpp b/plugin/src/Caelestia/Blobs/blobmaterial.hpp -new file mode 100644 -index 00000000..bf1eda75 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobmaterial.hpp -@@ -0,0 +1,47 @@ -+#pragma once -+ -+#include -+#include -+#include -+ -+struct BlobRectData { -+ float cx = 0, cy = 0, hw = 0, hh = 0; -+ float offsetX = 0, offsetY = 0; -+ float minEig = 1.0f; -+ // Inverse of 2x2 deformation matrix, column-major for GLSL -+ float invDeform[4] = { 1, 0, 0, 1 }; -+ // Screen-space AABB half-extents of the deformed rect -+ float screenHalfX = 0, screenHalfY = 0; -+ // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU -+ float radius[4] = { 0, 0, 0, 0 }; -+ // Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect. -+ // Used by the shader to skip smin between excluded pairs. -+ int excludeMask = 0; -+}; -+ -+class BlobMaterial : public QSGMaterial { -+public: -+ QSGMaterialType* type() const override; -+ QSGMaterialShader* createShader(QSGRendererInterface::RenderMode) const override; -+ int compare(const QSGMaterial* other) const override; -+ -+ float m_paddedX = 0; -+ float m_paddedY = 0; -+ float m_paddedW = 0; -+ float m_paddedH = 0; -+ float m_smoothFactor = 32.0f; -+ int m_rectCount = 0; -+ int m_myIndex = -2; -+ QColor m_color{ 0x44, 0x88, 0xff }; -+ int m_hasInverted = 0; -+ float m_invertedRadius = 0; -+ float m_invertedOuter[4] = {}; -+ float m_invertedInner[4] = {}; -+ BlobRectData m_rects[16] = {}; -+}; -+ -+class BlobMaterialShader : public QSGMaterialShader { -+public: -+ BlobMaterialShader(); -+ bool updateUniformData(RenderState& state, QSGMaterial* newMaterial, QSGMaterial* oldMaterial) override; -+}; -diff --git a/plugin/src/Caelestia/Blobs/blobrect.cpp b/plugin/src/Caelestia/Blobs/blobrect.cpp -new file mode 100644 -index 00000000..15486d83 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobrect.cpp -@@ -0,0 +1,245 @@ -+#include "blobrect.hpp" -+#include "blobgroup.hpp" -+ -+#include -+#include -+ -+BlobRect::BlobRect(QQuickItem* parent) -+ : BlobShape(parent) {} -+ -+BlobRect::~BlobRect() { -+ if (m_group) -+ m_group->removeShape(this); -+} -+ -+void BlobRect::updatePolish() { -+ BlobShape::updatePolish(); -+ -+ if (m_physicsActive) { -+ // Check if deformation is visually imperceptible -+ float totalDelta = std::abs(m_dm00 - 1.0f) + std::abs(m_dm01) + std::abs(m_dm11 - 1.0f); -+ float totalVel = std::abs(m_dmVel00) + std::abs(m_dmVel01) + std::abs(m_dmVel11); -+ -+ if (totalDelta < 0.004f && totalVel < 0.05f) { -+ // Snap to rest, no visible deformation -+ m_dm00 = 1.0f; -+ m_dm01 = 0.0f; -+ m_dm11 = 1.0f; -+ m_dmVel00 = m_dmVel01 = m_dmVel11 = 0.0f; -+ m_deformMatrix = QMatrix4x4(); -+ emit rawDeformMatrixChanged(); -+ updateCenteredDeformMatrix(); -+ m_physicsActive = false; -+ } else { -+ QMetaObject::invokeMethod( -+ this, -+ [this]() { -+ if (m_physicsActive && m_group) -+ m_group->markDirty(); -+ }, -+ Qt::QueuedConnection); -+ } -+ } -+} -+ -+void BlobRect::updatePhysics() { -+ const QPointF scenePos = mapToScene(QPointF(width() / 2.0, height() / 2.0)); -+ -+ if (!m_hasPrevPos) { -+ m_prevScenePos = scenePos; -+ m_elapsed.start(); -+ m_hasPrevPos = true; -+ return; -+ } -+ -+ const float dt = static_cast(m_elapsed.restart()) / 1000.0f; -+ if (dt > 0.1f || dt < 0.001f) { -+ m_prevScenePos = scenePos; -+ // Still check atRest on skipped frames to avoid getting stuck -+ if (m_physicsActive) -+ checkAtRest(0.0f); -+ return; -+ } -+ -+ const float velX = static_cast(scenePos.x() - m_prevScenePos.x()) / dt; -+ const float velY = static_cast(scenePos.y() - m_prevScenePos.y()) / dt; -+ m_prevScenePos = scenePos; -+ -+ const float speed = std::sqrt(velX * velX + velY * velY); -+ -+ if (!m_physicsActive) { -+ if (speed < 5.0f) -+ return; -+ m_physicsActive = true; -+ } -+ -+ // Compute target deformation matrix from velocity -+ // R(θ) * diag(stretch, compress) * R(θ)^T -+ const float kStretchFactor = static_cast(m_deformScale); -+ constexpr float kMaxStretch = 0.35f; -+ -+ float target00 = 1.0f; -+ float target01 = 0.0f; -+ float target11 = 1.0f; -+ -+ if (speed > 5.0f) { -+ const float targetStretch = 1.0f + std::min(speed * kStretchFactor, kMaxStretch); -+ const float targetCompress = 1.0f / targetStretch; -+ -+ const float cosA = velX / speed; -+ const float sinA = velY / speed; -+ const float cos2 = cosA * cosA; -+ const float sin2 = sinA * sinA; -+ const float cs = cosA * sinA; -+ -+ target00 = targetStretch * cos2 + targetCompress * sin2; -+ target01 = (targetStretch - targetCompress) * cs; -+ target11 = targetStretch * sin2 + targetCompress * cos2; -+ } -+ -+ // Underdamped spring on each matrix component -+ const float kStiffness = static_cast(m_stiffness); -+ const float kDamping = static_cast(m_damping); -+ -+ const float accel00 = -kStiffness * (m_dm00 - target00) - kDamping * m_dmVel00; -+ m_dmVel00 += accel00 * dt; -+ m_dm00 += m_dmVel00 * dt; -+ -+ const float accel01 = -kStiffness * (m_dm01 - target01) - kDamping * m_dmVel01; -+ m_dmVel01 += accel01 * dt; -+ m_dm01 += m_dmVel01 * dt; -+ -+ const float accel11 = -kStiffness * (m_dm11 - target11) - kDamping * m_dmVel11; -+ m_dmVel11 += accel11 * dt; -+ m_dm11 += m_dmVel11 * dt; -+ -+ m_deformMatrix = QMatrix4x4(m_dm00, m_dm01, 0, 0, m_dm01, m_dm11, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1); -+ emit rawDeformMatrixChanged(); -+ updateCenteredDeformMatrix(); -+ -+ checkAtRest(speed); -+} -+ -+void BlobRect::setTopLeftRadius(qreal r) { -+ if (!qFuzzyCompare(m_topLeftRadius, r)) { -+ m_topLeftRadius = r; -+ emit topLeftRadiusChanged(); -+ if (m_group) -+ m_group->markDirty(); -+ } -+} -+ -+void BlobRect::setTopRightRadius(qreal r) { -+ if (!qFuzzyCompare(m_topRightRadius, r)) { -+ m_topRightRadius = r; -+ emit topRightRadiusChanged(); -+ if (m_group) -+ m_group->markDirty(); -+ } -+} -+ -+void BlobRect::setBottomLeftRadius(qreal r) { -+ if (!qFuzzyCompare(m_bottomLeftRadius, r)) { -+ m_bottomLeftRadius = r; -+ emit bottomLeftRadiusChanged(); -+ if (m_group) -+ m_group->markDirty(); -+ } -+} -+ -+void BlobRect::setBottomRightRadius(qreal r) { -+ if (!qFuzzyCompare(m_bottomRightRadius, r)) { -+ m_bottomRightRadius = r; -+ emit bottomRightRadiusChanged(); -+ if (m_group) -+ m_group->markDirty(); -+ } -+} -+ -+void BlobRect::cornerRadii(float out[4]) const { -+ const auto maxR = static_cast(std::min(width(), height())) * 0.5f; -+ const auto base = std::min(static_cast(m_radius), maxR); -+ out[0] = std::min(m_topRightRadius >= 0 ? static_cast(m_topRightRadius) : base, maxR); -+ out[1] = std::min(m_bottomRightRadius >= 0 ? static_cast(m_bottomRightRadius) : base, maxR); -+ out[2] = std::min(m_bottomLeftRadius >= 0 ? static_cast(m_bottomLeftRadius) : base, maxR); -+ out[3] = std::min(m_topLeftRadius >= 0 ? static_cast(m_topLeftRadius) : base, maxR); -+} -+ -+bool BlobRect::isExcluded(const BlobShape* other) const { -+ for (const auto& ptr : m_exclude) { -+ if (ptr == other) -+ return true; -+ } -+ return false; -+} -+ -+QQmlListProperty BlobRect::exclude() { -+ return QQmlListProperty( -+ this, nullptr, &excludeAppend, &excludeCount, &excludeAt, &excludeClear, &excludeReplace, &excludeRemoveLast); -+} -+ -+void BlobRect::excludeAppend(QQmlListProperty* prop, BlobRect* rect) { -+ auto* self = static_cast(prop->object); -+ self->m_exclude.append(rect); -+ if (self->m_group) -+ self->m_group->markDirty(); -+ emit self->excludeChanged(); -+} -+ -+qsizetype BlobRect::excludeCount(QQmlListProperty* prop) { -+ auto* self = static_cast(prop->object); -+ return self->m_exclude.size(); -+} -+ -+BlobRect* BlobRect::excludeAt(QQmlListProperty* prop, qsizetype index) { -+ auto* self = static_cast(prop->object); -+ return self->m_exclude.at(index); -+} -+ -+void BlobRect::excludeClear(QQmlListProperty* prop) { -+ auto* self = static_cast(prop->object); -+ if (self->m_exclude.isEmpty()) -+ return; -+ self->m_exclude.clear(); -+ if (self->m_group) -+ self->m_group->markDirty(); -+ emit self->excludeChanged(); -+} -+ -+void BlobRect::excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect) { -+ auto* self = static_cast(prop->object); -+ self->m_exclude[index] = rect; -+ if (self->m_group) -+ self->m_group->markDirty(); -+ emit self->excludeChanged(); -+} -+ -+void BlobRect::excludeRemoveLast(QQmlListProperty* prop) { -+ auto* self = static_cast(prop->object); -+ if (self->m_exclude.isEmpty()) -+ return; -+ self->m_exclude.removeLast(); -+ if (self->m_group) -+ self->m_group->markDirty(); -+ emit self->excludeChanged(); -+} -+ -+void BlobRect::checkAtRest(float speed) { -+ constexpr float kEpsilon = 0.002f; -+ const bool atRest = std::abs(m_dm00 - 1.0f) < kEpsilon && std::abs(m_dm01) < kEpsilon && -+ std::abs(m_dm11 - 1.0f) < kEpsilon && std::abs(m_dmVel00) < kEpsilon && -+ std::abs(m_dmVel01) < kEpsilon && std::abs(m_dmVel11) < kEpsilon && speed < 5.0f; -+ -+ if (atRest) { -+ m_dm00 = 1.0f; -+ m_dm01 = 0.0f; -+ m_dm11 = 1.0f; -+ m_dmVel00 = 0.0f; -+ m_dmVel01 = 0.0f; -+ m_dmVel11 = 0.0f; -+ m_deformMatrix = QMatrix4x4(); // identity -+ emit rawDeformMatrixChanged(); -+ updateCenteredDeformMatrix(); -+ m_physicsActive = false; -+ } -+} -diff --git a/plugin/src/Caelestia/Blobs/blobrect.hpp b/plugin/src/Caelestia/Blobs/blobrect.hpp -new file mode 100644 -index 00000000..d2d6ad45 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobrect.hpp -@@ -0,0 +1,126 @@ -+#pragma once -+ -+#include "blobshape.hpp" -+ -+#include -+#include -+#include -+#include -+ -+class BlobRect : public BlobShape { -+ Q_OBJECT -+ QML_ELEMENT -+ Q_PROPERTY(qreal stiffness READ stiffness WRITE setStiffness NOTIFY stiffnessChanged) -+ Q_PROPERTY(qreal damping READ damping WRITE setDamping NOTIFY dampingChanged) -+ Q_PROPERTY(qreal deformScale READ deformScale WRITE setDeformScale NOTIFY deformScaleChanged) -+ Q_PROPERTY(QQmlListProperty exclude READ exclude NOTIFY excludeChanged) -+ Q_PROPERTY(qreal topLeftRadius READ topLeftRadius WRITE setTopLeftRadius NOTIFY topLeftRadiusChanged) -+ Q_PROPERTY(qreal topRightRadius READ topRightRadius WRITE setTopRightRadius NOTIFY topRightRadiusChanged) -+ Q_PROPERTY(qreal bottomLeftRadius READ bottomLeftRadius WRITE setBottomLeftRadius NOTIFY bottomLeftRadiusChanged) -+ Q_PROPERTY( -+ qreal bottomRightRadius READ bottomRightRadius WRITE setBottomRightRadius NOTIFY bottomRightRadiusChanged) -+ -+public: -+ explicit BlobRect(QQuickItem* parent = nullptr); -+ ~BlobRect() override; -+ -+ qreal stiffness() const { return m_stiffness; } -+ -+ void setStiffness(qreal s) { -+ if (!qFuzzyCompare(m_stiffness, s)) { -+ m_stiffness = s; -+ emit stiffnessChanged(); -+ } -+ } -+ -+ qreal damping() const { return m_damping; } -+ -+ void setDamping(qreal d) { -+ if (!qFuzzyCompare(m_damping, d)) { -+ m_damping = d; -+ emit dampingChanged(); -+ } -+ } -+ -+ qreal deformScale() const { return m_deformScale; } -+ -+ void setDeformScale(qreal s) { -+ if (!qFuzzyCompare(m_deformScale, s)) { -+ m_deformScale = s; -+ emit deformScaleChanged(); -+ } -+ } -+ -+ QQmlListProperty exclude(); -+ -+ bool isExcluded(const BlobShape* other) const override; -+ void cornerRadii(float out[4]) const override; -+ -+ qreal topLeftRadius() const { return m_topLeftRadius; } -+ -+ void setTopLeftRadius(qreal r); -+ -+ qreal topRightRadius() const { return m_topRightRadius; } -+ -+ void setTopRightRadius(qreal r); -+ -+ qreal bottomLeftRadius() const { return m_bottomLeftRadius; } -+ -+ void setBottomLeftRadius(qreal r); -+ -+ qreal bottomRightRadius() const { return m_bottomRightRadius; } -+ -+ void setBottomRightRadius(qreal r); -+ -+signals: -+ void stiffnessChanged(); -+ void dampingChanged(); -+ void deformScaleChanged(); -+ void excludeChanged(); -+ void topLeftRadiusChanged(); -+ void topRightRadiusChanged(); -+ void bottomLeftRadiusChanged(); -+ void bottomRightRadiusChanged(); -+ -+protected: -+ void updatePolish() override; -+ void updatePhysics() override; -+ -+private: -+ void checkAtRest(float speed); -+ -+ // Physics state -+ QPointF m_prevScenePos; -+ QElapsedTimer m_elapsed; -+ bool m_physicsActive = false; -+ bool m_hasPrevPos = false; -+ -+ // Symmetric 2x2 deformation matrix components (3 independent: m00, m01, -+ // m11) Rest state is identity: m00=1, m01=0, m11=1 -+ float m_dm00 = 1.0f; -+ float m_dm01 = 0.0f; -+ float m_dm11 = 1.0f; -+ -+ // Spring velocities for each component -+ float m_dmVel00 = 0.0f; -+ float m_dmVel01 = 0.0f; -+ float m_dmVel11 = 0.0f; -+ -+ qreal m_stiffness = 200.0; -+ qreal m_damping = 16.0; -+ qreal m_deformScale = 0.0005; -+ -+ qreal m_topLeftRadius = -1; -+ qreal m_topRightRadius = -1; -+ qreal m_bottomLeftRadius = -1; -+ qreal m_bottomRightRadius = -1; -+ -+ QList> m_exclude; -+ -+ static void excludeAppend(QQmlListProperty* prop, BlobRect* rect); -+ static qsizetype excludeCount(QQmlListProperty* prop); -+ static BlobRect* excludeAt(QQmlListProperty* prop, qsizetype index); -+ static void excludeClear(QQmlListProperty* prop); -+ static void excludeReplace(QQmlListProperty* prop, qsizetype index, BlobRect* rect); -+ static void excludeRemoveLast(QQmlListProperty* prop); -+}; -diff --git a/plugin/src/Caelestia/Blobs/blobshape.cpp b/plugin/src/Caelestia/Blobs/blobshape.cpp -new file mode 100644 -index 00000000..cfa11c44 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobshape.cpp -@@ -0,0 +1,392 @@ -+#include "blobshape.hpp" -+#include "blobgroup.hpp" -+#include "blobinvertedrect.hpp" -+ -+#include -+#include -+ -+#include -+#include -+ -+static float deformPadding(const QMatrix4x4& dm, float hw, float hh) { -+ // Bounding box of the deformed shape: |M * corners| -+ const float dm00 = dm(0, 0), dm01 = dm(0, 1); -+ const float dm10 = dm(1, 0), dm11 = dm(1, 1); -+ const float boundX = std::abs(dm00) * hw + std::abs(dm01) * hh; -+ const float boundY = std::abs(dm10) * hw + std::abs(dm11) * hh; -+ const float extraX = std::max(boundX - hw, 0.0f) + std::abs(dm(0, 3)); -+ const float extraY = std::max(boundY - hh, 0.0f) + std::abs(dm(1, 3)); -+ return std::max(extraX, extraY); -+} -+ -+static float cpuSdBox(float px, float py, float cx, float cy, float hw, float hh) { -+ const float dx = std::abs(px - cx) - hw; -+ const float dy = std::abs(py - cy) - hh; -+ const float mdx = std::max(dx, 0.0f); -+ const float mdy = std::max(dy, 0.0f); -+ return std::sqrt(mdx * mdx + mdy * mdy) + std::min(std::max(dx, dy), 0.0f); -+} -+ -+static float cpuSmoothstep(float edge0, float edge1, float x) { -+ const float t = std::clamp((x - edge0) / (edge1 - edge0), 0.0f, 1.0f); -+ return t * t * (3.0f - 2.0f * t); -+} -+ -+BlobShape::BlobShape(QQuickItem* parent) -+ : QQuickItem(parent) { -+ setFlag(ItemHasContents); -+} -+ -+void BlobShape::setGroup(BlobGroup* g) { -+ if (m_group == g) -+ return; -+ if (m_group && isComponentComplete()) -+ unregisterFromGroup(); -+ m_group = g; -+ if (m_group && isComponentComplete()) -+ registerWithGroup(); -+ emit groupChanged(); -+ if (m_group) -+ m_group->markDirty(); -+} -+ -+void BlobShape::setRadius(qreal r) { -+ if (qFuzzyCompare(m_radius, r)) -+ return; -+ m_radius = r; -+ emit radiusChanged(); -+ if (m_group) -+ m_group->markDirty(); -+} -+ -+void BlobShape::componentComplete() { -+ QQuickItem::componentComplete(); -+ if (m_group) -+ registerWithGroup(); -+} -+ -+void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { -+ QQuickItem::geometryChange(newGeometry, oldGeometry); -+ updateCenteredDeformMatrix(); -+ if (m_group) { -+ // Accumulate sub-pixel drift so slow movements don't desync the shader -+ m_pendingDx += static_cast(newGeometry.x() - oldGeometry.x()); -+ m_pendingDy += static_cast(newGeometry.y() - oldGeometry.y()); -+ const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); -+ const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); -+ if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) { -+ m_pendingDx = 0; -+ m_pendingDy = 0; -+ m_group->markShapeDirty(this); -+ } -+ } -+} -+ -+void BlobShape::updateCenteredDeformMatrix() { -+ const auto cx = static_cast(width()) * 0.5f; -+ const auto cy = static_cast(height()) * 0.5f; -+ QMatrix4x4 result; -+ result.translate(cx, cy); -+ result *= m_deformMatrix; -+ result.translate(-cx, -cy); -+ if (m_centeredDeformMatrix != result) { -+ m_centeredDeformMatrix = result; -+ emit deformMatrixChanged(); -+ } -+} -+ -+void BlobShape::cornerRadii(float out[4]) const { -+ const auto maxR = static_cast(std::min(width(), height())) * 0.5f; -+ const auto r = std::min(static_cast(m_radius), maxR); -+ out[0] = r; -+ out[1] = r; -+ out[2] = r; -+ out[3] = r; -+} -+ -+void BlobShape::registerWithGroup() { -+ if (m_group) -+ m_group->addShape(this); -+} -+ -+void BlobShape::unregisterFromGroup() { -+ if (m_group) -+ m_group->removeShape(this); -+} -+ -+void BlobShape::updatePolish() { -+ if (!m_group) -+ return; -+ -+ // Ensure all shapes have up-to-date physics (only once per frame) -+ m_group->ensurePhysicsUpdated(); -+ -+ const QPointF scenePos = mapToScene(QPointF(0, 0)); -+ const float pad = static_cast(m_group->smoothing()); -+ -+ if (isInvertedRect()) { -+ m_cachedPaddedX = static_cast(scenePos.x()); -+ m_cachedPaddedY = static_cast(scenePos.y()); -+ m_cachedPaddedW = static_cast(width()); -+ m_cachedPaddedH = static_cast(height()); -+ m_localPaddedRect = QRectF(0, 0, width(), height()); -+ } else { -+ const float hw = static_cast(width()) * 0.5f; -+ const float hh = static_cast(height()) * 0.5f; -+ const float totalPad = pad + deformPadding(m_deformMatrix, hw, hh); -+ -+ m_cachedPaddedX = static_cast(scenePos.x()) - totalPad; -+ m_cachedPaddedY = static_cast(scenePos.y()) - totalPad; -+ m_cachedPaddedW = static_cast(width()) + 2.0f * totalPad; -+ m_cachedPaddedH = static_cast(height()) + 2.0f * totalPad; -+ m_localPaddedRect = QRectF(static_cast(-totalPad), static_cast(-totalPad), -+ width() + 2.0 * static_cast(totalPad), height() + 2.0 * static_cast(totalPad)); -+ } -+ -+ // Filter nearby normal rects -+ m_cachedRects.clear(); -+ m_cachedMyIndex = -2; -+ const QRectF myPadded(static_cast(m_cachedPaddedX), static_cast(m_cachedPaddedY), -+ static_cast(m_cachedPaddedW), static_cast(m_cachedPaddedH)); -+ -+ // Track shape pointers parallel to m_cachedRects for pairwise exclusion lookups -+ QVector rectShapes; -+ rectShapes.reserve(m_group->shapes().size()); -+ -+ for (BlobShape* other : m_group->shapes()) { -+ if (other->isInvertedRect()) -+ continue; -+ -+ // Skip zero-size rects -+ if (other->width() <= 0 || other->height() <= 0) -+ continue; -+ -+ if (isExcluded(other)) -+ continue; -+ -+ const QPointF otherScene = other->mapToScene(QPointF(0, 0)); -+ -+ bool include = false; -+ if (isInvertedRect()) { -+ include = true; -+ } else { -+ const float otherHW = static_cast(other->width()) * 0.5f; -+ const float otherHH = static_cast(other->height()) * 0.5f; -+ const float otherPad = pad + deformPadding(other->m_deformMatrix, otherHW, otherHH); -+ const QRectF otherPadded(otherScene.x() - static_cast(otherPad), -+ otherScene.y() - static_cast(otherPad), other->width() + 2.0 * static_cast(otherPad), -+ other->height() + 2.0 * static_cast(otherPad)); -+ include = myPadded.intersects(otherPadded); -+ } -+ -+ if (include) { -+ if (other == this) -+ m_cachedMyIndex = static_cast(m_cachedRects.size()); -+ -+ const QMatrix4x4& dm = other->m_deformMatrix; -+ const float a = dm(0, 0), b = dm(1, 0); -+ const float c = dm(0, 1), d = dm(1, 1); -+ -+ BlobRectData r; -+ r.cx = static_cast(otherScene.x() + other->width() / 2.0); -+ r.cy = static_cast(otherScene.y() + other->height() / 2.0); -+ r.hw = static_cast(other->width() / 2.0); -+ r.hh = static_cast(other->height() / 2.0); -+ other->cornerRadii(r.radius); -+ r.offsetX = dm(0, 3); -+ r.offsetY = dm(1, 3); -+ -+ // Pre-compute inverse deformation matrix -+ const float det = a * d - c * b; -+ const float invDet = std::abs(det) > 1e-6f ? 1.0f / det : 1.0f; -+ r.invDeform[0] = d * invDet; -+ r.invDeform[1] = -b * invDet; -+ r.invDeform[2] = -c * invDet; -+ r.invDeform[3] = a * invDet; -+ -+ // Pre-compute minimum eigenvalue (avoids per-pixel sqrt) -+ const float halfTr = 0.5f * (a + d); -+ const float halfDiff = 0.5f * (a - d); -+ r.minEig = halfTr - std::sqrt(halfDiff * halfDiff + c * c); -+ -+ // Pre-compute screen-space AABB half-extents -+ r.screenHalfX = std::abs(a) * r.hw + std::abs(c) * r.hh; -+ r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; -+ -+ m_cachedRects.append(r); -+ rectShapes.append(other); -+ } -+ } -+ -+ if (isInvertedRect()) -+ m_cachedMyIndex = -1; -+ -+ // Compute pairwise exclude masks. Bit j in entry i is set iff rect i excludes rect j -+ // or rect j excludes rect i. The shader uses this to avoid smin between excluded pairs. -+ const auto cachedCount = m_cachedRects.size(); -+ for (qsizetype i = 0; i < cachedCount; ++i) { -+ int mask = 0; -+ BlobShape* si = rectShapes[i]; -+ for (qsizetype j = 0; j < cachedCount; ++j) { -+ if (j == i) -+ continue; -+ BlobShape* sj = rectShapes[j]; -+ if (si->isExcluded(sj) || sj->isExcluded(si)) -+ mask |= (1 << j); -+ } -+ m_cachedRects[i].excludeMask = mask; -+ } -+ -+ // Cache inverted rect data -+ m_cachedHasInverted = false; -+ m_cachedInvertedRadius = 0; -+ memset(m_cachedInvertedOuter, 0, sizeof(m_cachedInvertedOuter)); -+ memset(m_cachedInvertedInner, 0, sizeof(m_cachedInvertedInner)); -+ -+ auto* inv = m_group->invertedRect(); -+ if (inv) { -+ const QPointF invScene = inv->mapToScene(QPointF(0, 0)); -+ const float outerCX = static_cast(invScene.x() + inv->width() / 2.0); -+ const float outerCY = static_cast(invScene.y() + inv->height() / 2.0); -+ const float outerHW = static_cast(inv->width() / 2.0); -+ const float outerHH = static_cast(inv->height() / 2.0); -+ -+ const float innerCX = outerCX + static_cast((inv->borderLeft() - inv->borderRight()) / 2.0); -+ const float innerCY = outerCY + static_cast((inv->borderTop() - inv->borderBottom()) / 2.0); -+ const float innerHW = outerHW - static_cast((inv->borderLeft() + inv->borderRight()) / 2.0); -+ const float innerHH = outerHH - static_cast((inv->borderTop() + inv->borderBottom()) / 2.0); -+ -+ // Check if this rect is near the border (within 2x smoothing of inner edge) -+ bool nearBorder = isInvertedRect(); -+ if (!nearBorder) { -+ const float margin = pad * 2.0f; -+ const float myCX = m_cachedPaddedX + m_cachedPaddedW * 0.5f; -+ const float myCY = m_cachedPaddedY + m_cachedPaddedH * 0.5f; -+ const float myHW = m_cachedPaddedW * 0.5f; -+ const float myHH = m_cachedPaddedH * 0.5f; -+ // Near border if any edge of padded rect is within margin of inner edge -+ nearBorder = (myCX - myHW < innerCX - innerHW + margin) || (myCX + myHW > innerCX + innerHW - margin) || -+ (myCY - myHH < innerCY - innerHH + margin) || (myCY + myHH > innerCY + innerHH - margin); -+ } -+ -+ if (nearBorder) { -+ m_cachedHasInverted = true; -+ m_cachedInvertedRadius = static_cast(inv->radius()); -+ -+ m_cachedInvertedOuter[0] = outerCX; -+ m_cachedInvertedOuter[1] = outerCY; -+ m_cachedInvertedOuter[2] = outerHW; -+ m_cachedInvertedOuter[3] = outerHH; -+ -+ m_cachedInvertedInner[0] = innerCX; -+ m_cachedInvertedInner[1] = innerCY; -+ m_cachedInvertedInner[2] = innerHW; -+ m_cachedInvertedInner[3] = innerHH; -+ } -+ } -+ -+ // Pre-compute effective per-corner radii (moves O(N²) work from GPU to CPU) -+ const float smoothFactor = pad; -+ constexpr float minR = 2.0f; -+ const auto rectCount = m_cachedRects.size(); -+ for (qsizetype i = 0; i < rectCount; ++i) { -+ auto& ri = m_cachedRects[i]; -+ const int riExcludeMask = ri.excludeMask; -+ float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; -+ -+ const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; -+ const float cBrX = ri.cx + ri.hw, cBrY = ri.cy + ri.hh; -+ const float cBlX = ri.cx - ri.hw, cBlY = ri.cy + ri.hh; -+ const float cTlX = ri.cx - ri.hw, cTlY = ri.cy - ri.hh; -+ -+ for (qsizetype j = 0; j < rectCount; ++j) { -+ if (j == i) -+ continue; -+ if (riExcludeMask & (1 << j)) -+ continue; -+ const auto& rj = m_cachedRects[j]; -+ fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); -+ fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); -+ fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBlX, cBlY, rj.cx, rj.cy, rj.hw, rj.hh))); -+ fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTlX, cTlY, rj.cx, rj.cy, rj.hw, rj.hh))); -+ } -+ -+ if (m_cachedHasInverted) { -+ const float icx = m_cachedInvertedInner[0]; -+ const float icy = m_cachedInvertedInner[1]; -+ const float ihw = m_cachedInvertedInner[2]; -+ const float ihh = m_cachedInvertedInner[3]; -+ fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTrX, cTrY, icx, icy, ihw, ihh))); -+ fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBrX, cBrY, icx, icy, ihw, ihh))); -+ fBl = std::min(fBl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cBlX, cBlY, icx, icy, ihw, ihh))); -+ fTl = std::min(fTl, cpuSmoothstep(0.0f, smoothFactor, -cpuSdBox(cTlX, cTlY, icx, icy, ihw, ihh))); -+ } -+ -+ // Combine base radii with fill factors into effective per-corner radii -+ ri.radius[0] = std::max(ri.radius[0] * fTr, minR); -+ ri.radius[1] = std::max(ri.radius[1] * fBr, minR); -+ ri.radius[2] = std::max(ri.radius[2] * fBl, minR); -+ ri.radius[3] = std::max(ri.radius[3] * fTl, minR); -+ } -+} -+ -+QSGNode* BlobShape::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) { -+ if (!m_group) { -+ delete oldNode; -+ return nullptr; -+ } -+ -+ auto* node = static_cast(oldNode); -+ if (!node) { -+ node = new QSGGeometryNode; -+ -+ auto* geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), 4); -+ geometry->setDrawingMode(QSGGeometry::DrawTriangleStrip); -+ node->setGeometry(geometry); -+ node->setFlag(QSGNode::OwnsGeometry); -+ -+ auto* material = new BlobMaterial; -+ material->setFlag(QSGMaterial::Blending); -+ node->setMaterial(material); -+ node->setFlag(QSGNode::OwnsMaterial); -+ } -+ -+ // Update geometry -+ auto* geometry = node->geometry(); -+ auto* v = geometry->vertexDataAsTexturedPoint2D(); -+ -+ const float x0 = static_cast(m_localPaddedRect.x()); -+ const float y0 = static_cast(m_localPaddedRect.y()); -+ const float x1 = x0 + static_cast(m_localPaddedRect.width()); -+ const float y1 = y0 + static_cast(m_localPaddedRect.height()); -+ -+ v[0].set(x0, y0, 0.0f, 0.0f); -+ v[1].set(x1, y0, 1.0f, 0.0f); -+ v[2].set(x0, y1, 0.0f, 1.0f); -+ v[3].set(x1, y1, 1.0f, 1.0f); -+ -+ node->markDirty(QSGNode::DirtyGeometry); -+ -+ // Update material -+ auto* material = static_cast(node->material()); -+ material->m_paddedX = m_cachedPaddedX; -+ material->m_paddedY = m_cachedPaddedY; -+ material->m_paddedW = m_cachedPaddedW; -+ material->m_paddedH = m_cachedPaddedH; -+ material->m_smoothFactor = static_cast(m_group->smoothing()); -+ material->m_myIndex = m_cachedMyIndex; -+ material->m_color = m_group->color(); -+ material->m_hasInverted = m_cachedHasInverted ? 1 : 0; -+ material->m_invertedRadius = m_cachedInvertedRadius; -+ memcpy(material->m_invertedOuter, m_cachedInvertedOuter, sizeof(m_cachedInvertedOuter)); -+ memcpy(material->m_invertedInner, m_cachedInvertedInner, sizeof(m_cachedInvertedInner)); -+ -+ const int count = static_cast(qMin(m_cachedRects.size(), qsizetype(16))); -+ material->m_rectCount = count; -+ for (int i = 0; i < count; ++i) -+ material->m_rects[i] = m_cachedRects[i]; -+ -+ node->markDirty(QSGNode::DirtyMaterial); -+ -+ return node; -+} -diff --git a/plugin/src/Caelestia/Blobs/blobshape.hpp b/plugin/src/Caelestia/Blobs/blobshape.hpp -new file mode 100644 -index 00000000..c05a40d8 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/blobshape.hpp -@@ -0,0 +1,79 @@ -+#pragma once -+ -+#include "blobmaterial.hpp" -+ -+#include -+#include -+#include -+ -+class BlobGroup; -+ -+class BlobShape : public QQuickItem { -+ Q_OBJECT -+ Q_PROPERTY(BlobGroup* group READ group WRITE setGroup NOTIFY groupChanged) -+ Q_PROPERTY(qreal radius READ radius WRITE setRadius NOTIFY radiusChanged) -+ Q_PROPERTY(QMatrix4x4 deformMatrix READ deformMatrix NOTIFY deformMatrixChanged) -+ Q_PROPERTY(QMatrix4x4 rawDeformMatrix READ rawDeformMatrix NOTIFY rawDeformMatrixChanged) -+ -+ friend class BlobGroup; -+ -+public: -+ explicit BlobShape(QQuickItem* parent = nullptr); -+ ~BlobShape() override = default; -+ -+ BlobGroup* group() const { return m_group; } -+ -+ void setGroup(BlobGroup* g); -+ -+ qreal radius() const { return m_radius; } -+ -+ void setRadius(qreal r); -+ -+ QMatrix4x4 deformMatrix() const { return m_centeredDeformMatrix; } -+ -+ QMatrix4x4 rawDeformMatrix() const { return m_deformMatrix; } -+ -+signals: -+ void groupChanged(); -+ void radiusChanged(); -+ void deformMatrixChanged(); -+ void rawDeformMatrixChanged(); -+ -+protected: -+ void componentComplete() override; -+ void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; -+ void updatePolish() override; -+ QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData*) override; -+ -+ virtual bool isInvertedRect() const { return false; } -+ -+ virtual bool isExcluded(const BlobShape* /*other*/) const { return false; } -+ -+ virtual void cornerRadii(float out[4]) const; -+ -+ virtual void updatePhysics() {} -+ -+ virtual void registerWithGroup(); -+ virtual void unregisterFromGroup(); -+ void updateCenteredDeformMatrix(); -+ -+ BlobGroup* m_group = nullptr; -+ qreal m_radius = 0; -+ QMatrix4x4 m_deformMatrix; // identity by default -+ QMatrix4x4 m_centeredDeformMatrix; -+ -+ // Cached data from updatePolish -+ float m_cachedPaddedX = 0; -+ float m_cachedPaddedY = 0; -+ float m_cachedPaddedW = 0; -+ float m_cachedPaddedH = 0; -+ QRectF m_localPaddedRect; -+ QVector m_cachedRects; -+ int m_cachedMyIndex = -2; -+ float m_pendingDx = 0; -+ float m_pendingDy = 0; -+ bool m_cachedHasInverted = false; -+ float m_cachedInvertedRadius = 0; -+ float m_cachedInvertedOuter[4] = {}; -+ float m_cachedInvertedInner[4] = {}; -+}; -diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.frag b/plugin/src/Caelestia/Blobs/shaders/blob.frag -new file mode 100644 -index 00000000..c002fd25 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/shaders/blob.frag -@@ -0,0 +1,251 @@ -+#version 440 -+ -+layout(location = 0) in vec2 qt_TexCoord0; -+layout(location = 0) out vec4 fragColor; -+ -+layout(std140, binding = 0) uniform buf { -+ mat4 qt_Matrix; -+ float qt_Opacity; -+ float paddedX; -+ float paddedY; -+ float paddedW; -+ float paddedH; -+ float smoothFactor; -+ int rectCount; -+ int myIndex; -+ vec4 color; -+ int hasInverted; -+ float invertedRadius; -+ vec4 invertedOuter; -+ vec4 invertedInner; -+ vec4 rectData[80]; -+}; -+ -+float sdRoundedBox(vec2 p, vec2 center, vec2 halfSize, float radius) { -+ vec2 d = abs(p - center) - halfSize + vec2(radius); -+ return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0) - radius; -+} -+ -+float sdRoundedBox4(vec2 p, vec2 center, vec2 halfSize, vec4 r) { -+ // r = (topRight, bottomRight, bottomLeft, topLeft) -+ p -= center; -+ r.xy = (p.x > 0.0) ? r.xy : r.wz; -+ r.x = (p.y > 0.0) ? r.y : r.x; -+ vec2 q = abs(p) - halfSize + r.x; -+ return min(max(q.x, q.y), 0.0) + length(max(q, 0.0)) - r.x; -+} -+ -+float sdBox(vec2 p, vec2 center, vec2 halfSize) { -+ vec2 d = abs(p - center) - halfSize; -+ return length(max(d, vec2(0.0))) + min(max(d.x, d.y), 0.0); -+} -+ -+float smin(float a, float b, float k) { -+ // Cubic smooth min (C2 continuous — no curvature kinks at blend boundary) -+ float h = max(k - abs(a - b), 0.0) / k; -+ return min(a, b) - h * h * h * k * (1.0/6.0); -+} -+ -+float smax(float a, float b, float k) { -+ float h = max(k - abs(a - b), 0.0) / k; -+ return max(a, b) + h * h * h * k * (1.0/6.0); -+} -+ -+float smaxSharpA(float a, float b, float k) { -+ // smax variant that keeps a's boundary sharp (no inward rounding at a = 0). -+ // Used for the frame outer edge so it always fills to the edges. -+ float h = max(k - abs(a - b), 0.0) / k; -+ float blend = h * h * h * k * (1.0/6.0); -+ blend *= smoothstep(0.0, k * 0.5, -a); -+ return max(a, b) + blend; -+} -+ -+void main() { -+ vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); -+ -+ // Phase 1: compute per-rect SDF, track owner. We can't smin yet because excluded -+ // pairs need to skip the smooth blend, which requires pairwise pass below. -+ float dArr[16]; -+ int owner = -2; -+ float minDist = 1e10; -+ -+ for (int i = 0; i < rectCount; i++) { -+ vec4 rect = rectData[i * 5]; // cx, cy, hw, hh -+ vec4 props = rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig -+ vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix -+ vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 -+ vec4 radii = rectData[i * 5 + 4]; // effective per-corner radii (tr, br, bl, tl) -+ -+ // Offset center for asymmetric deformation -+ vec2 center = rect.xy + props.yz; -+ -+ // AABB early-out: skip rects far from this pixel -+ vec2 extent = sh.xy + vec2(smoothFactor * 1.5); -+ if (abs(pixel.x - center.x) > extent.x || abs(pixel.y - center.y) > extent.y) { -+ dArr[i] = 1e10; -+ continue; -+ } -+ -+ // Apply pre-computed inverse deformation to the evaluation point -+ mat2 invDeform = mat2(invDm.xy, invDm.zw); -+ vec2 transformedPixel = center + invDeform * (pixel - center); -+ -+ // Use pre-computed effective per-corner radii -+ float d = sdRoundedBox4(transformedPixel, center, rect.zw, radii); -+ -+ // Use pre-computed minimum eigenvalue for SDF correction -+ d *= max(props.w, 0.01); -+ -+ // Scale SDF on the axis facing a nearby border to narrow the smin blend zone -+ // in that direction only, without reducing k (which would cause sharp edges). -+ if (hasInverted != 0) { -+ vec2 screenHalf = sh.xy; -+ -+ float distY0 = (center.y + screenHalf.y) - (invertedInner.y - invertedInner.w); -+ float distY1 = (invertedInner.y + invertedInner.w) - (center.y - screenHalf.y); -+ float distX0 = (center.x + screenHalf.x) - (invertedInner.x - invertedInner.z); -+ float distX1 = (invertedInner.x + invertedInner.z) - (center.x - screenHalf.x); -+ -+ // 0 = far from border, 1 = at border (max compression) -+ float yProx = 1.0 - min( -+ smoothstep(0.0, smoothFactor, distY0), -+ smoothstep(0.0, smoothFactor, distY1) -+ ); -+ float xProx = 1.0 - min( -+ smoothstep(0.0, smoothFactor, distX0), -+ smoothstep(0.0, smoothFactor, distX1) -+ ); -+ -+ // Smooth axis weights: gradient-based at corners, face-based inside. -+ vec2 q = abs(pixel - center) - screenHalf; -+ vec2 qp = max(q, vec2(0.0)); -+ float cornerLen = length(qp); -+ -+ // Gradient direction in corner region (smooth 90-degree rotation) -+ float gradX = qp.x / max(cornerLen, 0.001); -+ float gradY = qp.y / max(cornerLen, 0.001); -+ -+ // Smooth face weights for inside/edge (no hard branch) -+ float faceY = smoothstep(-4.0, 4.0, q.y - q.x); -+ float faceX = 1.0 - faceY; -+ -+ // Blend: gradient in corner region, face-based inside -+ float t = smoothstep(0.0, 2.0, cornerLen); -+ float xWeight = mix(faceX, gradX, t); -+ float yWeight = mix(faceY, gradY, t); -+ -+ float boost = 3.0; -+ float scale = 1.0 + (xProx * xWeight + yProx * yWeight) * boost; -+ d *= scale; -+ } -+ -+ dArr[i] = d; -+ if (d < smoothFactor && d < minDist) { -+ minDist = d; -+ owner = i; -+ } -+ } -+ -+ // Phase 2: hard-min baseline over all rects. -+ float mergedSdf = 1e10; -+ for (int i = 0; i < rectCount; i++) { -+ mergedSdf = min(mergedSdf, dArr[i]); -+ } -+ -+ // Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin <= min, -+ // so taking the min over all non-excluded pair smins gives the smoothly-merged SDF. -+ for (int i = 0; i < rectCount; i++) { -+ if (dArr[i] >= 1e9) -+ continue; -+ int excludeMask = floatBitsToInt(rectData[i * 5 + 1].x); -+ for (int j = i + 1; j < rectCount; j++) { -+ if (dArr[j] >= 1e9) -+ continue; -+ if ((excludeMask & (1 << j)) != 0) -+ continue; -+ // smin only deviates from min within smoothFactor -+ if (abs(dArr[i] - dArr[j]) >= smoothFactor) -+ continue; -+ mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor)); -+ } -+ } -+ -+ if (hasInverted != 0) { -+ float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; -+ float dInner = sdRoundedBox(pixel, invertedInner.xy, invertedInner.zw, invertedRadius); -+ -+ // Border sinks: track the opposite rect edge, clamped to border thickness -+ float innerTop = invertedInner.y - invertedInner.w; -+ float innerBot = invertedInner.y + invertedInner.w; -+ float innerLeft = invertedInner.x - invertedInner.z; -+ float innerRight = invertedInner.x + invertedInner.z; -+ float outerTop = invertedOuter.y - invertedOuter.w; -+ float outerBot = invertedOuter.y + invertedOuter.w; -+ float outerLeft = invertedOuter.x - invertedOuter.z; -+ float outerRight = invertedOuter.x + invertedOuter.z; -+ -+ float sinkValue = 0.0; -+ for (int i = 0; i < rectCount; i++) { -+ vec4 rect = rectData[i * 5]; -+ vec4 sinkProps = rectData[i * 5 + 1]; -+ vec2 sinkSh = rectData[i * 5 + 3].xy; -+ -+ // Screen-space center (with offset) and pre-computed AABB half-extents -+ vec2 ctr = rect.xy + sinkProps.yz; -+ -+ // Delay sink to absorb smin blend depth (cubic smin max = k/6) -+ float preOff = smoothFactor * (1.0/6.0); -+ -+ // Top border: track rect's BOTTOM edge, only within border thickness -+ float topPen = clamp(innerTop - (ctr.y + sinkSh.y) - preOff, 0.0, innerTop - outerTop); -+ -+ // Bottom border: track rect's TOP edge -+ float botPen = clamp((ctr.y - sinkSh.y) - innerBot - preOff, 0.0, outerBot - innerBot); -+ -+ // Left border: track rect's RIGHT edge -+ float leftPen = clamp(innerLeft - (ctr.x + sinkSh.x) - preOff, 0.0, innerLeft - outerLeft); -+ -+ // Right border: track rect's LEFT edge -+ float rightPen = clamp((ctr.x - sinkSh.x) - innerRight - preOff, 0.0, outerRight - innerRight); -+ -+ // Lateral distance from pixel to rect's extent along each edge -+ float hLat = max(abs(pixel.x - ctr.x) - sinkSh.x, 0.0); -+ float vLat = max(abs(pixel.y - ctr.y) - sinkSh.y, 0.0); -+ -+ // Perpendicular proximity: full strength in border, fade inside inner area -+ float topZone = 1.0 - smoothstep(innerTop, innerTop + smoothFactor, pixel.y); -+ float botZone = smoothstep(innerBot - smoothFactor, innerBot, pixel.y); -+ float leftZone = 1.0 - smoothstep(innerLeft, innerLeft + smoothFactor, pixel.x); -+ float rightZone = smoothstep(innerRight - smoothFactor, innerRight, pixel.x); -+ -+ float s = smoothFactor * 2.0; -+ float sink = max( -+ max(topPen * smoothstep(s, 0.0, hLat) * topZone, -+ botPen * smoothstep(s, 0.0, hLat) * botZone), -+ max(leftPen * smoothstep(s, 0.0, vLat) * leftZone, -+ rightPen * smoothstep(s, 0.0, vLat) * rightZone) -+ ); -+ sinkValue = max(sinkValue, sink); -+ } -+ -+ dInner -= sinkValue; -+ -+ float dFrame = smaxSharpA(dOuter, -dInner, smoothFactor); -+ -+ mergedSdf = smin(mergedSdf, dFrame, smoothFactor); -+ if (dFrame < minDist) { -+ owner = -1; -+ } -+ } -+ -+ // Each renderer only outputs pixels it owns, but allow rendering -+ // blend zones to prevent gaps (mergedSdf < smoothFactor means in blend) -+ // myIndex == -1: inverted rect renders border-owned pixels -+ // myIndex >= 0: individual rect renders its owned pixels -+ if (owner != myIndex && mergedSdf > smoothFactor) -+ discard; -+ -+ float fw = fwidth(mergedSdf); -+ float alpha = 1.0 - smoothstep(-fw, fw, mergedSdf); -+ fragColor = vec4(color.rgb * alpha, alpha) * qt_Opacity; -+} -diff --git a/plugin/src/Caelestia/Blobs/shaders/blob.vert b/plugin/src/Caelestia/Blobs/shaders/blob.vert -new file mode 100644 -index 00000000..e71d8104 ---- /dev/null -+++ b/plugin/src/Caelestia/Blobs/shaders/blob.vert -@@ -0,0 +1,29 @@ -+#version 440 -+ -+layout(location = 0) in vec4 qt_VertexPosition; -+layout(location = 1) in vec2 qt_VertexTexCoord; -+ -+layout(location = 0) out vec2 qt_TexCoord0; -+ -+layout(std140, binding = 0) uniform buf { -+ mat4 qt_Matrix; -+ float qt_Opacity; -+ float paddedX; -+ float paddedY; -+ float paddedW; -+ float paddedH; -+ float smoothFactor; -+ int rectCount; -+ int myIndex; -+ vec4 color; -+ int hasInverted; -+ float invertedRadius; -+ vec4 invertedOuter; -+ vec4 invertedInner; -+ vec4 rectData[80]; -+}; -+ -+void main() { -+ gl_Position = qt_Matrix * qt_VertexPosition; -+ qt_TexCoord0 = qt_VertexTexCoord; -+} -diff --git a/plugin/src/Caelestia/CMakeLists.txt b/plugin/src/Caelestia/CMakeLists.txt -index e4a02012..6e73f4fe 100644 ---- a/plugin/src/Caelestia/CMakeLists.txt -+++ b/plugin/src/Caelestia/CMakeLists.txt -@@ -1,20 +1,24 @@ --find_package(Qt6 REQUIRED COMPONENTS Core Qml Gui Quick Concurrent Sql Network DBus) -+find_package(Qt6 REQUIRED COMPONENTS ShaderTools Core Qml Gui Quick QuickControls2 Concurrent Sql Network DBus) - find_package(PkgConfig REQUIRED) - pkg_check_modules(Qalculate IMPORTED_TARGET libqalculate REQUIRED) - pkg_check_modules(Pipewire IMPORTED_TARGET libpipewire-0.3 REQUIRED) - pkg_check_modules(Aubio IMPORTED_TARGET aubio REQUIRED) --pkg_check_modules(Cava IMPORTED_TARGET libcava REQUIRED) -+pkg_check_modules(Cava IMPORTED_TARGET libcava QUIET) -+if(NOT Cava_FOUND) -+ pkg_check_modules(Cava IMPORTED_TARGET cava REQUIRED) -+endif() - - set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml") - qt_standard_project_setup(REQUIRES 6.9) - - function(qml_module arg_TARGET) -- cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;LIBRARIES") -+ cmake_parse_arguments(PARSE_ARGV 1 arg "" "URI" "SOURCES;QML_FILES;LIBRARIES") - - qt_add_qml_module(${arg_TARGET} - URI ${arg_URI} - VERSION ${VERSION} - SOURCES ${arg_SOURCES} -+ QML_FILES ${arg_QML_FILES} - ) - - qt_query_qml_module(${arg_TARGET} -@@ -34,9 +38,24 @@ function(qml_module arg_TARGET) - install(FILES "${module_qmldir}" DESTINATION "${module_dir}") - install(FILES "${module_typeinfo}" DESTINATION "${module_dir}") - -- target_link_libraries(${arg_TARGET} PRIVATE Qt::Core Qt::Qml ${arg_LIBRARIES}) -+ target_link_libraries(${arg_TARGET} PRIVATE caelestia-pch Qt::Core Qt::Qml ${arg_LIBRARIES}) - endfunction() - -+add_library(caelestia-pch INTERFACE) -+target_precompile_headers(caelestia-pch INTERFACE -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+ -+) -+ - qml_module(caelestia - URI Caelestia - SOURCES -@@ -54,6 +73,10 @@ qml_module(caelestia - PkgConfig::Qalculate - ) - -+add_subdirectory(Components) -+add_subdirectory(Config) - add_subdirectory(Internal) - add_subdirectory(Models) - add_subdirectory(Services) -+add_subdirectory(Blobs) -+add_subdirectory(Images) -diff --git a/plugin/src/Caelestia/Components/CMakeLists.txt b/plugin/src/Caelestia/Components/CMakeLists.txt -new file mode 100644 -index 00000000..f880d318 ---- /dev/null -+++ b/plugin/src/Caelestia/Components/CMakeLists.txt -@@ -0,0 +1,7 @@ -+qml_module(caelestia-components -+ URI Caelestia.Components -+ SOURCES -+ lazylistview.hpp lazylistview.cpp -+ LIBRARIES -+ Qt::Quick -+) -diff --git a/plugin/src/Caelestia/Components/lazylistview.cpp b/plugin/src/Caelestia/Components/lazylistview.cpp -new file mode 100644 -index 00000000..36b49301 ---- /dev/null -+++ b/plugin/src/Caelestia/Components/lazylistview.cpp -@@ -0,0 +1,1108 @@ -+#include "lazylistview.hpp" -+ -+#include -+#include -+#include -+ -+namespace { -+ -+constexpr int ASYNC_BATCH_CREATE = 2; -+constexpr int ASYNC_BATCH_DESTROY = 4; -+ -+} // namespace -+ -+namespace caelestia::components { -+ -+// --- LazyListViewAttached --- -+ -+LazyListViewAttached::LazyListViewAttached(QObject* parent) -+ : QObject(parent) {} -+ -+qreal LazyListViewAttached::preferredHeight() const { -+ return m_preferredHeight; -+} -+ -+void LazyListViewAttached::setPreferredHeight(qreal height) { -+ if (qFuzzyCompare(m_preferredHeight + 1.0, height + 1.0)) -+ return; -+ m_preferredHeight = height; -+ emit preferredHeightChanged(); -+} -+ -+qreal LazyListViewAttached::visibleHeight() const { -+ return m_visibleHeight; -+} -+ -+void LazyListViewAttached::setVisibleHeight(qreal height) { -+ if (qFuzzyCompare(m_visibleHeight + 1.0, height + 1.0)) -+ return; -+ m_visibleHeight = height; -+ emit visibleHeightChanged(); -+} -+ -+bool LazyListViewAttached::ready() const { -+ return m_ready; -+} -+ -+void LazyListViewAttached::setReady(bool ready) { -+ if (m_ready == ready) -+ return; -+ m_ready = ready; -+ emit readyChanged(); -+} -+ -+bool LazyListViewAttached::adding() const { -+ return m_adding; -+} -+ -+void LazyListViewAttached::setAdding(bool adding) { -+ if (m_adding == adding) -+ return; -+ m_adding = adding; -+ emit addingChanged(); -+} -+ -+bool LazyListViewAttached::removing() const { -+ return m_removing; -+} -+ -+void LazyListViewAttached::setRemoving(bool removing) { -+ if (m_removing == removing) -+ return; -+ m_removing = removing; -+ emit removingChanged(); -+} -+ -+bool LazyListViewAttached::trackViewport() const { -+ return m_trackViewport; -+} -+ -+void LazyListViewAttached::setTrackViewport(bool track) { -+ if (m_trackViewport == track) -+ return; -+ m_trackViewport = track; -+ emit trackViewportChanged(); -+} -+ -+// --- LazyListView --- -+ -+LazyListView::LazyListView(QQuickItem* parent) -+ : QQuickItem(parent) { -+ setFlag(ItemHasContents, false); -+} -+ -+LazyListViewAttached* LazyListView::qmlAttachedProperties(QObject* object) { -+ return new LazyListViewAttached(object); -+} -+ -+LazyListView::~LazyListView() { -+ for (auto& entry : m_delegates) -+ destroyDelegate(entry); -+ for (auto& entry : m_dyingDelegates) -+ destroyDelegate(entry); -+} -+ -+// --- Model & Delegate --- -+ -+QAbstractItemModel* LazyListView::model() const { -+ return m_model; -+} -+ -+void LazyListView::setModel(QAbstractItemModel* model) { -+ if (m_model == model) -+ return; -+ -+ if (m_model) -+ disconnectModel(); -+ -+ m_model = model; -+ -+ if (m_model) -+ connectModel(); -+ -+ resetContent(); -+ emit modelChanged(); -+} -+ -+QQmlComponent* LazyListView::delegate() const { -+ return m_delegate; -+} -+ -+void LazyListView::setDelegate(QQmlComponent* delegate) { -+ if (m_delegate == delegate) -+ return; -+ -+ m_delegate = delegate; -+ resetContent(); -+ emit delegateChanged(); -+} -+ -+// --- Layout --- -+ -+qreal LazyListView::spacing() const { -+ return m_spacing; -+} -+ -+void LazyListView::setSpacing(qreal spacing) { -+ if (qFuzzyCompare(m_spacing, spacing)) -+ return; -+ m_spacing = spacing; -+ emit spacingChanged(); -+ polish(); -+} -+ -+qreal LazyListView::contentHeight() const { -+ return m_contentHeight; -+} -+ -+qreal LazyListView::layoutHeight() const { -+ return m_layoutHeight; -+} -+ -+qreal LazyListView::contentY() const { -+ return m_contentY; -+} -+ -+void LazyListView::setContentY(qreal contentY) { -+ if (qFuzzyCompare(m_contentY, contentY)) -+ return; -+ m_contentY = contentY; -+ emit contentYChanged(); -+ polish(); -+} -+ -+// --- Viewport --- -+ -+QRectF LazyListView::viewport() const { -+ return m_viewport; -+} -+ -+void LazyListView::setViewport(const QRectF& viewport) { -+ if (m_viewport == viewport) -+ return; -+ m_viewport = viewport; -+ emit viewportChanged(); -+ if (m_useCustomViewport) -+ polish(); -+} -+ -+bool LazyListView::useCustomViewport() const { -+ return m_useCustomViewport; -+} -+ -+void LazyListView::setUseCustomViewport(bool use) { -+ if (m_useCustomViewport == use) -+ return; -+ m_useCustomViewport = use; -+ emit useCustomViewportChanged(); -+ polish(); -+} -+ -+qreal LazyListView::cacheBuffer() const { -+ return m_cacheBuffer; -+} -+ -+void LazyListView::setCacheBuffer(qreal buffer) { -+ if (qFuzzyCompare(m_cacheBuffer, buffer)) -+ return; -+ m_cacheBuffer = buffer; -+ emit cacheBufferChanged(); -+ polish(); -+} -+ -+// --- Sizing --- -+ -+qreal LazyListView::estimatedHeight() const { -+ return m_estimatedHeight; -+} -+ -+void LazyListView::setEstimatedHeight(qreal height) { -+ if (qFuzzyCompare(m_estimatedHeight, height)) -+ return; -+ m_estimatedHeight = height; -+ emit estimatedHeightChanged(); -+ polish(); -+} -+ -+bool LazyListView::asynchronous() const { -+ return m_asynchronous; -+} -+ -+void LazyListView::setAsynchronous(bool async) { -+ if (m_asynchronous == async) -+ return; -+ m_asynchronous = async; -+ emit asynchronousChanged(); -+} -+ -+qreal LazyListView::effectiveEstimatedHeight() const { -+ if (m_estimatedHeight >= 0) -+ return m_estimatedHeight; -+ if (m_knownHeightCount > 0) -+ return m_knownHeightSum / m_knownHeightCount; -+ return 40; -+} -+ -+void LazyListView::trackHeight(qreal height) { -+ m_knownHeightSum += height; -+ ++m_knownHeightCount; -+} -+ -+void LazyListView::untrackHeight(qreal height) { -+ m_knownHeightSum -= height; -+ --m_knownHeightCount; -+} -+ -+qreal LazyListView::delegateHeight(QQuickItem* item) { -+ if (!item) -+ return 0; -+ -+ auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); -+ if (attached && attached->preferredHeight() >= 0) -+ return attached->preferredHeight(); -+ -+ return item->implicitHeight(); -+} -+ -+qreal LazyListView::delegateVisibleHeight(QQuickItem* item) { -+ if (!item) -+ return 0; -+ -+ auto* attached = qobject_cast(qmlAttachedPropertiesObject(item, false)); -+ if (attached) { -+ if (attached->visibleHeight() >= 0) -+ return attached->visibleHeight(); -+ if (attached->preferredHeight() >= 0) -+ return attached->preferredHeight(); -+ } -+ -+ return item->implicitHeight(); -+} -+ -+bool LazyListView::isDelegateReady(QQuickItem* item) { -+ if (!item) -+ return false; -+ auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); -+ return !att || att->ready(); -+} -+ -+// --- Animation Durations --- -+ -+int LazyListView::removeDuration() const { -+ return m_removeDuration; -+} -+ -+void LazyListView::setRemoveDuration(int duration) { -+ if (m_removeDuration == duration) -+ return; -+ m_removeDuration = duration; -+ emit removeDurationChanged(); -+} -+ -+int LazyListView::readyDelay() const { -+ return m_readyDelay; -+} -+ -+void LazyListView::setReadyDelay(int delay) { -+ if (m_readyDelay == delay) -+ return; -+ m_readyDelay = delay; -+ emit readyDelayChanged(); -+} -+ -+// --- State --- -+ -+int LazyListView::count() const { -+ return m_model ? m_model->rowCount() : 0; -+} -+ -+// --- QQuickItem Overrides --- -+ -+void LazyListView::componentComplete() { -+ QQuickItem::componentComplete(); -+ m_componentComplete = true; -+ resetContent(); -+} -+ -+void LazyListView::geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) { -+ QQuickItem::geometryChange(newGeometry, oldGeometry); -+ -+ if (!m_componentComplete) -+ return; -+ -+ if (!qFuzzyCompare(newGeometry.width(), oldGeometry.width())) { -+ for (auto& entry : m_delegates) { -+ if (entry.item) -+ entry.item->setWidth(newGeometry.width()); -+ } -+ } -+ -+ polish(); -+} -+ -+void LazyListView::updatePolish() { -+ if (!m_componentComplete || !m_model || !m_delegate) -+ return; -+ -+ // Flush pending inserts — make items visible and clear the adding flag -+ // so enter animations begin. When readyDelay > 0 the entire insert is -+ // deferred so delegates have time to lay out before appearing. -+ for (auto& entry : m_delegates) { -+ if (!entry.pendingInsert || !entry.item) -+ continue; -+ -+ if (m_readyDelay > 0) { -+ if (!entry.readyDelayStarted) { -+ entry.readyDelayStarted = true; -+ auto* item = entry.item; -+ QTimer::singleShot(m_readyDelay, this, [this, item] { -+ auto indexIt = m_itemToIndex.find(item); -+ if (indexIt == m_itemToIndex.end()) -+ return; -+ const int idx = indexIt.value(); -+ auto it = m_delegates.find(idx); -+ if (it == m_delegates.end() || it->item != item || !it->pendingInsert) -+ return; -+ -+ it->pendingInsert = false; -+ it->readyDelayStarted = false; -+ -+ // Set initial y to visual position (based on current visible heights) -+ if (idx >= 0 && idx < static_cast(m_layout.size())) { -+ qreal visualY = 0; -+ bool hasVisItem = false; -+ for (int i = 0; i < static_cast(m_layout.size()); ++i) { -+ qreal h; -+ auto dit = m_delegates.find(i); -+ if (dit != m_delegates.end() && dit->item) -+ h = delegateVisibleHeight(dit->item); -+ else -+ h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); -+ if (h > 0) { -+ if (hasVisItem) -+ visualY += m_spacing; -+ hasVisItem = true; -+ } -+ if (i == idx) -+ break; -+ if (h > 0) -+ visualY += h; -+ } -+ item->setY(visualY - m_contentY); -+ } -+ -+ item->setVisible(true); -+ auto* att = -+ qobject_cast(qmlAttachedPropertiesObject(item, false)); -+ if (att) { -+ att->setAdding(false); -+ att->setReady(true); -+ } -+ -+ // Animate from visual position to layout position -+ if (idx >= 0 && idx < static_cast(m_layout.size())) -+ item->setProperty("y", m_layout[idx].targetY - m_contentY); -+ -+ polish(); -+ }); -+ } -+ continue; -+ } -+ -+ entry.pendingInsert = false; -+ entry.item->setVisible(true); -+ auto* att = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); -+ if (att) { -+ att->setAdding(false); -+ att->setReady(true); -+ } -+ } -+ -+ relayout(); -+ syncDelegates(); -+ -+ // Clear isNew flags — the add animation only plays for items created -+ // during the same polish cycle as their model insertion, not for -+ // delegates created later when scrolling items into the viewport. -+ for (auto& record : m_layout) -+ record.isNew = false; -+ -+ // Position delegates — QML Behavior on y handles the animation -+ for (auto& entry : m_delegates) { -+ if (!entry.item || entry.pendingRemoval || entry.pendingInsert) -+ continue; -+ -+ const int idx = entry.modelIndex; -+ if (idx < 0 || idx >= static_cast(m_layout.size())) -+ continue; -+ -+ if (m_layout[idx].heightKnown && qFuzzyIsNull(m_layout[idx].height)) -+ continue; -+ -+ // Use setProperty to go through the QML property system, -+ // which triggers Behaviors (setY bypasses them). -+ entry.item->setProperty("y", m_layout[idx].targetY - m_contentY); -+ } -+} -+ -+// --- Layout Engine --- -+ -+void LazyListView::relayout() { -+ // Layout positioning uses preferredHeight (final/non-animated). -+ // Only add spacing between items with non-zero height. -+ qreal y = 0; -+ bool hasLayoutItem = false; -+ for (auto& record : m_layout) { -+ const qreal layoutH = record.heightKnown ? record.height : effectiveEstimatedHeight(); -+ if (layoutH > 0) { -+ if (hasLayoutItem) -+ y += m_spacing; -+ hasLayoutItem = true; -+ record.targetY = y; -+ y += layoutH; -+ } else { -+ record.targetY = y; -+ } -+ } -+ -+ if (!qFuzzyCompare(m_layoutHeight + 1.0, y + 1.0)) { -+ m_layoutHeight = y; -+ emit layoutHeightChanged(); -+ } -+ -+ // Content height tracks actual visible heights so scrolling follows animations. -+ // Only add spacing between items with non-zero visible height. -+ qreal visY = 0; -+ bool hasVisItem = false; -+ for (int i = 0; i < static_cast(m_layout.size()); ++i) { -+ qreal h; -+ auto dit = m_delegates.find(i); -+ if (dit != m_delegates.end() && dit->item) -+ h = delegateVisibleHeight(dit->item); -+ else -+ h = m_layout[i].heightKnown ? m_layout[i].height : effectiveEstimatedHeight(); -+ if (h > 0) { -+ if (hasVisItem) -+ visY += m_spacing; -+ hasVisItem = true; -+ visY += h; -+ } -+ } -+ -+ // Account for dying delegates still visually present -+ for (const auto& dying : std::as_const(m_dyingDelegates)) { -+ if (!dying.item) -+ continue; -+ const qreal dyingH = delegateVisibleHeight(dying.item); -+ if (dyingH > 0) -+ visY = std::max(visY, dying.item->y() + dyingH); -+ } -+ -+ if (!qFuzzyCompare(m_contentHeight + 1.0, visY + 1.0)) { -+ m_contentHeight = visY; -+ emit contentHeightChanged(); -+ } -+} -+ -+QRectF LazyListView::effectiveViewport() const { -+ QRectF vp; -+ if (m_useCustomViewport) -+ vp = m_viewport; -+ else -+ vp = QRectF(0, m_contentY, width(), height()); -+ -+ // During Flickable overshoot the viewport can extend entirely beyond content bounds, -+ // causing all delegates to be culled. Clamp so it always overlaps [0, layoutHeight]. -+ // Only needed for the built-in viewport — custom viewports represent the actual -+ // visible area and may legitimately lie entirely outside the content. -+ if (!m_useCustomViewport && m_layoutHeight > 0) { -+ const qreal top = std::min(vp.y(), m_layoutHeight); -+ const qreal bottom = std::max(vp.y() + vp.height(), 0.0); -+ if (bottom > top) -+ vp = QRectF(vp.x(), top, vp.width(), bottom - top); -+ } -+ -+ vp.adjust(0, -m_cacheBuffer, 0, m_cacheBuffer); -+ -+ // Trim the cache-buffered viewport to [0, layoutHeight]. No items exist outside -+ // those bounds, so extending past them wastes budget and can cause edge thrashing -+ // when a large cache buffer reaches the opposite end of the content. -+ if (m_layoutHeight > 0) { -+ const qreal top = std::max(vp.y(), 0.0); -+ const qreal bottom = std::min(vp.y() + vp.height(), m_layoutHeight); -+ if (top < bottom) -+ vp = QRectF(vp.x(), top, vp.width(), bottom - top); -+ else -+ return {}; -+ } -+ -+ return vp; -+} -+ -+std::pair LazyListView::computeVisibleRange() const { -+ if (m_layout.isEmpty()) -+ return { -1, -1 }; -+ -+ const auto vp = effectiveViewport(); -+ if (vp.isEmpty()) -+ return { -1, -1 }; -+ -+ const qreal vpTop = vp.y(); -+ const qreal vpBottom = vp.y() + vp.height(); -+ -+ // Binary search for first visible item -+ int lo = 0; -+ int hi = static_cast(m_layout.size()) - 1; -+ int first = static_cast(m_layout.size()); -+ -+ while (lo <= hi) { -+ const int mid = lo + (hi - lo) / 2; -+ const auto& record = m_layout[mid]; -+ const qreal itemBottom = record.targetY + (record.heightKnown ? record.height : effectiveEstimatedHeight()); -+ -+ if (itemBottom >= vpTop) { -+ first = mid; -+ hi = mid - 1; -+ } else { -+ lo = mid + 1; -+ } -+ } -+ -+ if (first >= static_cast(m_layout.size())) -+ return { -1, -1 }; -+ -+ // Linear scan for last visible item -+ int last = first; -+ for (int i = first; i < static_cast(m_layout.size()); ++i) { -+ if (m_layout[i].targetY > vpBottom) -+ break; -+ last = i; -+ } -+ -+ return { first, last }; -+} -+ -+// --- Delegate Lifecycle --- -+ -+void LazyListView::syncDelegates() { -+ const auto [first, last] = computeVisibleRange(); -+ -+ // Collect indices that should be alive -+ QSet visibleIndices; -+ if (first >= 0) { -+ for (int i = first; i <= last; ++i) -+ visibleIndices.insert(i); -+ } -+ -+ // Collect delegates to destroy — only if visually outside the viewport -+ const auto vp = effectiveViewport(); -+ QList toRemove; -+ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { -+ if (visibleIndices.contains(it.key())) -+ continue; -+ if (!it->item || vp.isEmpty()) { -+ toRemove.append(it.key()); -+ continue; -+ } -+ const qreal itemTop = it->item->y(); -+ const qreal itemBottom = itemTop + delegateVisibleHeight(it->item); -+ if (itemBottom < vp.top() || itemTop > vp.bottom()) -+ toRemove.append(it.key()); -+ } -+ -+ // Batch destroy -+ const int destroyBudget = m_asynchronous ? ASYNC_BATCH_DESTROY : static_cast(toRemove.size()); -+ QVector removedEntries; -+ removedEntries.reserve(std::min(destroyBudget, static_cast(toRemove.size()))); -+ int destroyed = 0; -+ for (int idx : toRemove) { -+ if (destroyed >= destroyBudget) -+ break; -+ auto entry = m_delegates.take(idx); -+ if (entry.item) -+ m_itemToIndex.remove(entry.item); -+ removedEntries.append(std::move(entry)); -+ ++destroyed; -+ } -+ for (auto& entry : removedEntries) -+ destroyDelegate(entry); -+ -+ // Collect indices to create -+ QList toCreate; -+ if (first >= 0) { -+ for (int i = first; i <= last; ++i) { -+ if (!m_delegates.contains(i)) -+ toCreate.append(i); -+ } -+ } -+ -+ // Batch create -+ const int createBudget = m_asynchronous ? ASYNC_BATCH_CREATE : static_cast(toCreate.size()); -+ int created = 0; -+ for (int i : toCreate) { -+ if (created >= createBudget) -+ break; -+ -+ auto entry = createDelegate(i); -+ if (entry.item) { -+ // Height tracking and viewport compensation are deferred -+ // until the delegate signals ready via readyChanged. -+ entry.pendingInsert = true; -+ entry.item->setY(m_layout[i].targetY - m_contentY); -+ m_itemToIndex.insert(entry.item, i); -+ m_delegates.insert(i, std::move(entry)); -+ ++created; -+ } -+ } -+ -+ // Pending inserts need to become visible on the next frame, and -+ // async mode may have remaining create/destroy work. -+ if (created > 0 || (m_asynchronous && (destroyed < static_cast(toRemove.size()) || -+ created < static_cast(toCreate.size())))) -+ polish(); -+} -+ -+LazyListView::DelegateEntry LazyListView::createDelegate(int modelIndex) { -+ DelegateEntry entry; -+ entry.modelIndex = modelIndex; -+ -+ if (!m_delegate || !m_model) -+ return entry; -+ -+ const auto roleNames = m_model->roleNames(); -+ -+ // Use the delegate component's creation context for beginCreate -+ // so bound components (pragma ComponentBehavior: Bound) are accepted. -+ auto* compContext = m_delegate->creationContext(); -+ if (!compContext) -+ compContext = qmlContext(this); -+ if (!compContext) -+ return entry; -+ -+ auto* obj = m_delegate->beginCreate(compContext); -+ entry.item = qobject_cast(obj); -+ -+ if (!entry.item) { -+ if (obj) -+ m_delegate->completeCreate(); -+ delete obj; -+ return entry; -+ } -+ -+ // Build initial properties from model data -+ const auto index = m_model->index(modelIndex, 0); -+ QVariantMap initialProps; -+ bool hasModelData = false; -+ -+ for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { -+ const auto name = QString::fromUtf8(it.value()); -+ initialProps.insert(name, m_model->data(index, it.key())); -+ if (name == QStringLiteral("modelData")) -+ hasModelData = true; -+ } -+ initialProps.insert(QStringLiteral("index"), modelIndex); -+ -+ if (!hasModelData) { -+ const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); -+ initialProps.insert(QStringLiteral("modelData"), m_model->data(index, role)); -+ } -+ -+ m_delegate->setInitialProperties(entry.item, initialProps); -+ -+ entry.item->setParentItem(this); -+ entry.item->setWidth(width()); -+ -+ // Only set adding = true for genuinely new model items (not viewport entries). -+ // Cleared on the next frame in updatePolish when the item becomes visible. -+ if (modelIndex < static_cast(m_layout.size()) && m_layout[modelIndex].isNew) { -+ auto* addingAttached = -+ qobject_cast(qmlAttachedPropertiesObject(entry.item, true)); -+ if (addingAttached) -+ addingAttached->setAdding(true); -+ } -+ -+ m_delegate->completeCreate(); -+ -+ // Keep adding=true and hide — flushed on the next frame in updatePolish -+ entry.item->setVisible(false); -+ -+ // Height-change handler — uses m_itemToIndex for O(1) lookup. -+ // Ignored while the delegate is not yet ready. -+ auto onHeightChanged = [this, item = entry.item] { -+ if (!isDelegateReady(item)) -+ return; -+ auto indexIt = m_itemToIndex.find(item); -+ if (indexIt == m_itemToIndex.end()) -+ return; -+ const int idx = indexIt.value(); -+ auto delegateIt = m_delegates.find(idx); -+ if (delegateIt == m_delegates.end() || delegateIt->item != item) -+ return; -+ const qreal h = delegateHeight(item); -+ if (idx < static_cast(m_layout.size()) && !qFuzzyCompare(m_layout[idx].height + 1.0, h + 1.0)) { -+ const qreal oldH = m_layout[idx].height; -+ const bool wasKnown = m_layout[idx].heightKnown; -+ m_layout[idx].height = h; -+ m_layout[idx].heightKnown = true; -+ if (wasKnown) -+ untrackHeight(oldH); -+ trackHeight(h); -+ -+ // If this tracked item is above the viewport, emit a -+ // compensation delta so the consumer can adjust scroll. -+ if (wasKnown) { -+ auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); -+ if (att && att->trackViewport()) { -+ const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; -+ if (m_layout[idx].targetY < vpTop) -+ emit viewportAdjustNeeded(h - oldH); -+ } -+ } -+ -+ if (!m_relayoutPending) { -+ m_relayoutPending = true; -+ QTimer::singleShot(0, this, [this] { -+ m_relayoutPending = false; -+ relayout(); -+ polish(); -+ }); -+ } -+ } -+ }; -+ -+ // Watch implicitHeight as fallback -+ connect(entry.item, &QQuickItem::implicitHeightChanged, this, onHeightChanged); -+ -+ // Watch attached properties if the delegate uses them -+ auto* attached = qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); -+ if (attached) { -+ connect(attached, &LazyListViewAttached::preferredHeightChanged, this, onHeightChanged); -+ connect(attached, &LazyListViewAttached::visibleHeightChanged, this, [this] { -+ polish(); -+ }); -+ connect(attached, &LazyListViewAttached::readyChanged, this, [this, item = entry.item] { -+ auto indexIt = m_itemToIndex.find(item); -+ if (indexIt == m_itemToIndex.end()) -+ return; -+ const int idx = indexIt.value(); -+ if (idx >= static_cast(m_layout.size())) -+ return; -+ auto* att = qobject_cast(qmlAttachedPropertiesObject(item, false)); -+ if (!att || !att->ready()) -+ return; -+ -+ const qreal h = delegateHeight(item); -+ const qreal oldLayoutH = m_layout[idx].heightKnown ? m_layout[idx].height : effectiveEstimatedHeight(); -+ if (m_layout[idx].heightKnown) -+ untrackHeight(m_layout[idx].height); -+ m_layout[idx].height = h; -+ m_layout[idx].heightKnown = true; -+ trackHeight(h); -+ -+ if (att->trackViewport() && !qFuzzyCompare(h + 1.0, oldLayoutH + 1.0)) { -+ const qreal vpTop = m_useCustomViewport ? m_viewport.y() : m_contentY; -+ if (m_layout[idx].targetY < vpTop) -+ emit viewportAdjustNeeded(h - oldLayoutH); -+ } -+ -+ polish(); -+ }); -+ } -+ -+ return entry; -+} -+ -+void LazyListView::destroyDelegate(DelegateEntry& entry) { -+ if (entry.item) { -+ entry.item->setParentItem(nullptr); -+ entry.item->setVisible(false); -+ entry.item->deleteLater(); -+ entry.item = nullptr; -+ } -+} -+ -+void LazyListView::updateDelegateData(DelegateEntry& entry) { -+ if (!m_model || !entry.item) -+ return; -+ -+ const auto roleNames = m_model->roleNames(); -+ const auto index = m_model->index(entry.modelIndex, 0); -+ bool hasModelData = false; -+ -+ for (auto it = roleNames.constBegin(); it != roleNames.constEnd(); ++it) { -+ const auto name = QString::fromUtf8(it.value()); -+ entry.item->setProperty(name.toUtf8().constData(), m_model->data(index, it.key())); -+ if (name == QStringLiteral("modelData")) -+ hasModelData = true; -+ } -+ -+ entry.item->setProperty("index", entry.modelIndex); -+ -+ if (!hasModelData) { -+ const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); -+ entry.item->setProperty("modelData", m_model->data(index, role)); -+ } -+} -+ -+// --- Model Connection --- -+ -+void LazyListView::connectModel() { -+ if (!m_model) -+ return; -+ -+ m_modelConnections = { -+ connect(m_model, &QAbstractItemModel::rowsInserted, this, &LazyListView::onRowsInserted), -+ connect(m_model, &QAbstractItemModel::rowsAboutToBeRemoved, this, &LazyListView::onRowsAboutToBeRemoved), -+ connect(m_model, &QAbstractItemModel::rowsRemoved, this, &LazyListView::onRowsRemoved), -+ connect(m_model, &QAbstractItemModel::rowsMoved, this, &LazyListView::onRowsMoved), -+ connect(m_model, &QAbstractItemModel::dataChanged, this, &LazyListView::onDataChanged), -+ connect(m_model, &QAbstractItemModel::modelReset, this, &LazyListView::onModelReset), -+ connect(m_model, &QAbstractItemModel::layoutChanged, this, -+ [this] { -+ for (auto& entry : m_delegates) -+ updateDelegateData(entry); -+ polish(); -+ }), -+ connect(m_model, &QObject::destroyed, this, -+ [this] { -+ m_model = nullptr; -+ resetContent(); -+ emit modelChanged(); -+ }), -+ }; -+} -+ -+void LazyListView::disconnectModel() { -+ for (auto& conn : m_modelConnections) -+ disconnect(conn); -+ m_modelConnections.clear(); -+} -+ -+void LazyListView::resetContent() { -+ // Stop all animations and destroy all delegates -+ for (auto& entry : m_delegates) -+ destroyDelegate(entry); -+ m_delegates.clear(); -+ m_itemToIndex.clear(); -+ -+ for (auto& entry : m_dyingDelegates) -+ destroyDelegate(entry); -+ m_dyingDelegates.clear(); -+ -+ // Reset pending state -+ m_knownHeightSum = 0; -+ m_knownHeightCount = 0; -+ -+ // Rebuild layout from model -+ m_layout.clear(); -+ if (m_model && m_componentComplete) { -+ const int rows = m_model->rowCount(); -+ m_layout.resize(rows); -+ for (int i = 0; i < rows; ++i) { -+ m_layout[i].height = 0; -+ m_layout[i].heightKnown = false; -+ } -+ emit countChanged(); -+ } -+ -+ polish(); -+} -+ -+void LazyListView::onRowsInserted(const QModelIndex& parent, int first, int last) { -+ if (parent.isValid()) -+ return; -+ -+ const int insertCount = last - first + 1; -+ // Insert new layout records -+ m_layout.insert(first, insertCount, ItemRecord{ 0, 0, false, true }); -+ -+ // Shift existing delegate indices -+ QHash shifted; -+ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { -+ int newIdx = it.key() >= first ? it.key() + insertCount : it.key(); -+ auto entry = std::move(it.value()); -+ entry.modelIndex = newIdx; -+ if (entry.item) { -+ entry.item->setProperty("index", newIdx); -+ m_itemToIndex[entry.item] = newIdx; -+ } -+ shifted.insert(newIdx, std::move(entry)); -+ } -+ m_delegates = std::move(shifted); -+ -+ emit countChanged(); -+ polish(); -+} -+ -+void LazyListView::onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last) { -+ if (parent.isValid()) -+ return; -+ -+ for (int i = first; i <= last; ++i) { -+ if (!m_delegates.contains(i)) -+ continue; -+ -+ auto entry = m_delegates.take(i); -+ if (entry.item) -+ m_itemToIndex.remove(entry.item); -+ entry.pendingRemoval = true; -+ -+ // Never made visible — skip remove animation -+ if (entry.pendingInsert) { -+ destroyDelegate(entry); -+ continue; -+ } -+ -+ if (m_removeDuration > 0 && entry.item) { -+ auto* attached = -+ qobject_cast(qmlAttachedPropertiesObject(entry.item, false)); -+ if (attached) -+ attached->setRemoving(true); -+ -+ // Schedule destruction after the remove animation duration -+ auto* item = entry.item; -+ QTimer::singleShot(m_removeDuration, this, [this, item] { -+ for (auto it = m_dyingDelegates.begin(); it != m_dyingDelegates.end(); ++it) { -+ if (it->item == item) { -+ destroyDelegate(*it); -+ m_dyingDelegates.erase(it); -+ return; -+ } -+ } -+ }); -+ m_dyingDelegates.append(std::move(entry)); -+ } else { -+ destroyDelegate(entry); -+ } -+ } -+} -+ -+void LazyListView::onRowsRemoved(const QModelIndex& parent, int first, int last) { -+ if (parent.isValid()) -+ return; -+ -+ const int removeCount = last - first + 1; -+ -+ // Untrack known heights being removed -+ for (int i = first; i <= last; ++i) { -+ if (m_layout[i].heightKnown) -+ untrackHeight(m_layout[i].height); -+ } -+ -+ // Remove layout records -+ m_layout.remove(first, removeCount); -+ -+ // Shift remaining delegate indices down -+ QHash shifted; -+ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { -+ int newIdx = it.key() > last ? it.key() - removeCount : it.key(); -+ auto entry = std::move(it.value()); -+ entry.modelIndex = newIdx; -+ if (entry.item) { -+ entry.item->setProperty("index", newIdx); -+ m_itemToIndex[entry.item] = newIdx; -+ } -+ shifted.insert(newIdx, std::move(entry)); -+ } -+ m_delegates = std::move(shifted); -+ -+ emit countChanged(); -+ polish(); -+} -+ -+void LazyListView::onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row) { -+ if (parent.isValid() || destination.isValid()) -+ return; -+ -+ const int count = end - start + 1; -+ const int dest = row > start ? row - count : row; -+ -+ // Reorder layout records -+ QVector moved; -+ moved.reserve(count); -+ for (int i = start; i <= end; ++i) -+ moved.append(m_layout[i]); -+ m_layout.remove(start, count); -+ for (int i = 0; i < count; ++i) -+ m_layout.insert(dest + i, moved[i]); -+ -+ // Remap delegate indices to match new model order -+ QHash remapped; -+ for (auto it = m_delegates.begin(); it != m_delegates.end(); ++it) { -+ int oldIdx = it.key(); -+ int newIdx = oldIdx; -+ -+ if (oldIdx >= start && oldIdx <= end) { -+ newIdx = dest + (oldIdx - start); -+ } else { -+ if (oldIdx > end) -+ newIdx -= count; -+ if (newIdx >= dest) -+ newIdx += count; -+ } -+ -+ auto entry = std::move(it.value()); -+ entry.modelIndex = newIdx; -+ if (entry.item) { -+ entry.item->setProperty("index", newIdx); -+ m_itemToIndex[entry.item] = newIdx; -+ } -+ remapped.insert(newIdx, std::move(entry)); -+ } -+ m_delegates = std::move(remapped); -+ -+ polish(); -+} -+ -+void LazyListView::onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles) { -+ Q_UNUSED(roles) -+ -+ if (topLeft.parent().isValid()) -+ return; -+ -+ for (int i = topLeft.row(); i <= bottomRight.row(); ++i) { -+ if (m_delegates.contains(i)) -+ updateDelegateData(m_delegates[i]); -+ } -+} -+ -+void LazyListView::onModelReset() { -+ if (!m_model) { -+ resetContent(); -+ return; -+ } -+ -+ const int newRows = m_model->rowCount(); -+ const int oldRows = static_cast(m_layout.size()); -+ -+ // Check if the model data actually changed -+ if (newRows == oldRows) { -+ const auto roleNames = m_model->roleNames(); -+ const auto role = roleNames.isEmpty() ? Qt::DisplayRole : roleNames.constBegin().key(); -+ bool changed = false; -+ -+ for (auto it = m_delegates.constBegin(); it != m_delegates.constEnd(); ++it) { -+ if (!it->item || it.key() >= newRows) { -+ changed = true; -+ break; -+ } -+ const auto newData = m_model->data(m_model->index(it.key(), 0), role); -+ const auto oldData = it->item->property("modelData"); -+ if (newData != oldData) { -+ changed = true; -+ break; -+ } -+ } -+ -+ if (!changed) { -+ // Model content unchanged, just refresh delegate data -+ for (auto& entry : m_delegates) -+ updateDelegateData(entry); -+ return; -+ } -+ } -+ -+ resetContent(); -+} -+ -+} // namespace caelestia::components -diff --git a/plugin/src/Caelestia/Components/lazylistview.hpp b/plugin/src/Caelestia/Components/lazylistview.hpp -new file mode 100644 -index 00000000..e0746db2 ---- /dev/null -+++ b/plugin/src/Caelestia/Components/lazylistview.hpp -@@ -0,0 +1,243 @@ -+#pragma once -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+namespace caelestia::components { -+ -+class LazyListViewAttached : public QObject { -+ Q_OBJECT -+ -+ Q_PROPERTY(qreal preferredHeight READ preferredHeight WRITE setPreferredHeight NOTIFY preferredHeightChanged) -+ Q_PROPERTY(qreal visibleHeight READ visibleHeight WRITE setVisibleHeight NOTIFY visibleHeightChanged) -+ Q_PROPERTY(bool ready READ ready NOTIFY readyChanged) -+ Q_PROPERTY(bool adding READ adding NOTIFY addingChanged) -+ Q_PROPERTY(bool removing READ removing NOTIFY removingChanged) -+ Q_PROPERTY(bool trackViewport READ trackViewport WRITE setTrackViewport NOTIFY trackViewportChanged) -+ -+public: -+ explicit LazyListViewAttached(QObject* parent = nullptr); -+ -+ [[nodiscard]] qreal preferredHeight() const; -+ void setPreferredHeight(qreal height); -+ -+ [[nodiscard]] qreal visibleHeight() const; -+ void setVisibleHeight(qreal height); -+ -+ [[nodiscard]] bool ready() const; -+ void setReady(bool ready); -+ -+ [[nodiscard]] bool adding() const; -+ void setAdding(bool adding); -+ -+ [[nodiscard]] bool removing() const; -+ void setRemoving(bool removing); -+ -+ [[nodiscard]] bool trackViewport() const; -+ void setTrackViewport(bool track); -+ -+signals: -+ void preferredHeightChanged(); -+ void visibleHeightChanged(); -+ void readyChanged(); -+ void addingChanged(); -+ void removingChanged(); -+ void trackViewportChanged(); -+ -+private: -+ qreal m_preferredHeight = -1; -+ qreal m_visibleHeight = -1; -+ bool m_ready = false; -+ bool m_adding = false; -+ bool m_removing = false; -+ bool m_trackViewport = false; -+}; -+ -+class LazyListView : public QQuickItem { -+ Q_OBJECT -+ QML_ELEMENT -+ QML_ATTACHED(LazyListViewAttached) -+ -+ // Model & Delegate -+ Q_PROPERTY(QAbstractItemModel* model READ model WRITE setModel NOTIFY modelChanged) -+ Q_PROPERTY(QQmlComponent* delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) -+ -+ // Layout -+ Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) -+ Q_PROPERTY(qreal contentHeight READ contentHeight NOTIFY contentHeightChanged) -+ Q_PROPERTY(qreal layoutHeight READ layoutHeight NOTIFY layoutHeightChanged) -+ Q_PROPERTY(qreal contentY READ contentY WRITE setContentY NOTIFY contentYChanged) -+ -+ // Viewport & Lazy Loading -+ Q_PROPERTY(QRectF viewport READ viewport WRITE setViewport NOTIFY viewportChanged) -+ Q_PROPERTY(bool useCustomViewport READ useCustomViewport WRITE setUseCustomViewport NOTIFY useCustomViewportChanged) -+ Q_PROPERTY(qreal cacheBuffer READ cacheBuffer WRITE setCacheBuffer NOTIFY cacheBufferChanged) -+ -+ // Sizing -+ Q_PROPERTY(qreal estimatedHeight READ estimatedHeight WRITE setEstimatedHeight NOTIFY estimatedHeightChanged) -+ -+ // Async -+ Q_PROPERTY(bool asynchronous READ asynchronous WRITE setAsynchronous NOTIFY asynchronousChanged) -+ -+ // Animation Durations -+ Q_PROPERTY(int removeDuration READ removeDuration WRITE setRemoveDuration NOTIFY removeDurationChanged) -+ Q_PROPERTY(int readyDelay READ readyDelay WRITE setReadyDelay NOTIFY readyDelayChanged) -+ -+ // State -+ Q_PROPERTY(int count READ count NOTIFY countChanged) -+ -+public: -+ explicit LazyListView(QQuickItem* parent = nullptr); -+ ~LazyListView() override; -+ -+ static LazyListViewAttached* qmlAttachedProperties(QObject* object); -+ -+ // Model & Delegate -+ [[nodiscard]] QAbstractItemModel* model() const; -+ void setModel(QAbstractItemModel* model); -+ -+ [[nodiscard]] QQmlComponent* delegate() const; -+ void setDelegate(QQmlComponent* delegate); -+ -+ // Layout -+ [[nodiscard]] qreal spacing() const; -+ void setSpacing(qreal spacing); -+ -+ [[nodiscard]] qreal contentHeight() const; -+ [[nodiscard]] qreal layoutHeight() const; -+ -+ [[nodiscard]] qreal contentY() const; -+ void setContentY(qreal contentY); -+ -+ // Viewport -+ [[nodiscard]] QRectF viewport() const; -+ void setViewport(const QRectF& viewport); -+ -+ [[nodiscard]] bool useCustomViewport() const; -+ void setUseCustomViewport(bool use); -+ -+ [[nodiscard]] qreal cacheBuffer() const; -+ void setCacheBuffer(qreal buffer); -+ -+ // Sizing -+ [[nodiscard]] qreal estimatedHeight() const; -+ void setEstimatedHeight(qreal height); -+ -+ // Async -+ [[nodiscard]] bool asynchronous() const; -+ void setAsynchronous(bool async); -+ -+ // Animation Durations -+ [[nodiscard]] int removeDuration() const; -+ void setRemoveDuration(int duration); -+ -+ [[nodiscard]] int readyDelay() const; -+ void setReadyDelay(int delay); -+ -+ // State -+ [[nodiscard]] int count() const; -+signals: -+ void modelChanged(); -+ void delegateChanged(); -+ void spacingChanged(); -+ void contentHeightChanged(); -+ void layoutHeightChanged(); -+ void contentYChanged(); -+ void viewportChanged(); -+ void useCustomViewportChanged(); -+ void cacheBufferChanged(); -+ void estimatedHeightChanged(); -+ void asynchronousChanged(); -+ void removeDurationChanged(); -+ void readyDelayChanged(); -+ void countChanged(); -+ void viewportAdjustNeeded(qreal delta); -+ -+protected: -+ void componentComplete() override; -+ void geometryChange(const QRectF& newGeometry, const QRectF& oldGeometry) override; -+ void updatePolish() override; -+ -+private: -+ struct ItemRecord { -+ qreal targetY = 0; -+ qreal height = 0; -+ bool heightKnown = false; -+ bool isNew = false; -+ }; -+ -+ struct DelegateEntry { -+ int modelIndex = -1; -+ QQuickItem* item = nullptr; -+ bool pendingRemoval = false; -+ bool pendingInsert = false; -+ bool readyDelayStarted = false; -+ }; -+ -+ // Layout -+ void relayout(); -+ [[nodiscard]] std::pair computeVisibleRange() const; -+ [[nodiscard]] QRectF effectiveViewport() const; -+ [[nodiscard]] qreal effectiveEstimatedHeight() const; -+ [[nodiscard]] static qreal delegateHeight(QQuickItem* item); -+ [[nodiscard]] static qreal delegateVisibleHeight(QQuickItem* item); -+ [[nodiscard]] static bool isDelegateReady(QQuickItem* item); -+ void trackHeight(qreal height); -+ void untrackHeight(qreal height); -+ -+ // Delegate lifecycle -+ void syncDelegates(); -+ DelegateEntry createDelegate(int modelIndex); -+ void destroyDelegate(DelegateEntry& entry); -+ void updateDelegateData(DelegateEntry& entry); -+ -+ // Model connection -+ void connectModel(); -+ void disconnectModel(); -+ void resetContent(); -+ void onRowsInserted(const QModelIndex& parent, int first, int last); -+ void onRowsAboutToBeRemoved(const QModelIndex& parent, int first, int last); -+ void onRowsRemoved(const QModelIndex& parent, int first, int last); -+ void onRowsMoved(const QModelIndex& parent, int start, int end, const QModelIndex& destination, int row); -+ void onDataChanged(const QModelIndex& topLeft, const QModelIndex& bottomRight, const QList& roles); -+ void onModelReset(); -+ -+ // Members -+ QAbstractItemModel* m_model = nullptr; -+ QQmlComponent* m_delegate = nullptr; -+ -+ qreal m_spacing = 0; -+ qreal m_contentHeight = 0; -+ qreal m_layoutHeight = 0; -+ qreal m_contentY = 0; -+ -+ QRectF m_viewport; -+ bool m_useCustomViewport = false; -+ qreal m_cacheBuffer = 0; -+ -+ qreal m_estimatedHeight = -1; -+ qreal m_knownHeightSum = 0; -+ int m_knownHeightCount = 0; -+ bool m_asynchronous = false; -+ -+ int m_removeDuration = 300; -+ int m_readyDelay = 0; -+ -+ QVector m_layout; -+ QHash m_delegates; -+ QHash m_itemToIndex; -+ QVector m_dyingDelegates; -+ -+ bool m_componentComplete = false; -+ bool m_relayoutPending = false; -+ -+ QList m_modelConnections; -+}; -+ -+} // namespace caelestia::components -diff --git a/plugin/src/Caelestia/Config/CMakeLists.txt b/plugin/src/Caelestia/Config/CMakeLists.txt -new file mode 100644 -index 00000000..4b26ea02 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/CMakeLists.txt -@@ -0,0 +1,32 @@ -+qml_module(caelestia-config -+ URI Caelestia.Config -+ SOURCES -+ config.cpp -+ configattached.cpp -+ configobject.cpp -+ rootconfig.cpp -+ appearanceconfig.cpp -+ tokens.cpp -+ tokensattached.cpp -+ anim.cpp -+ monitorconfigmanager.cpp -+ backgroundconfig.hpp -+ barconfig.hpp -+ borderconfig.hpp -+ controlcenterconfig.hpp -+ dashboardconfig.hpp -+ generalconfig.hpp -+ launcherconfig.hpp -+ lockconfig.hpp -+ notifsconfig.hpp -+ osdconfig.hpp -+ serviceconfig.hpp -+ sessionconfig.hpp -+ sidebarconfig.hpp -+ userpaths.hpp -+ utilitiesconfig.hpp -+ winfoconfig.hpp -+ LIBRARIES -+ Qt::Quick -+ Qt::QuickControls2 -+) -diff --git a/plugin/src/Caelestia/Config/anim.cpp b/plugin/src/Caelestia/Config/anim.cpp -new file mode 100644 -index 00000000..e307e2f9 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/anim.cpp -@@ -0,0 +1,117 @@ -+#include "anim.hpp" -+#include "appearanceconfig.hpp" -+#include "tokens.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+AnimTokens::AnimTokens(QObject* parent) -+ : QObject(parent) {} -+ -+QEasingCurve AnimTokens::emphasized() const { -+ return m_emphasized; -+} -+ -+QEasingCurve AnimTokens::emphasizedAccel() const { -+ return m_emphasizedAccel; -+} -+ -+QEasingCurve AnimTokens::emphasizedDecel() const { -+ return m_emphasizedDecel; -+} -+ -+QEasingCurve AnimTokens::standard() const { -+ return m_standard; -+} -+ -+QEasingCurve AnimTokens::standardAccel() const { -+ return m_standardAccel; -+} -+ -+QEasingCurve AnimTokens::standardDecel() const { -+ return m_standardDecel; -+} -+ -+QEasingCurve AnimTokens::expressiveFastSpatial() const { -+ return m_expressiveFastSpatial; -+} -+ -+QEasingCurve AnimTokens::expressiveDefaultSpatial() const { -+ return m_expressiveDefaultSpatial; -+} -+ -+QEasingCurve AnimTokens::expressiveSlowSpatial() const { -+ return m_expressiveSlowSpatial; -+} -+ -+QEasingCurve AnimTokens::expressiveFastEffects() const { -+ return m_expressiveFastEffects; -+} -+ -+QEasingCurve AnimTokens::expressiveDefaultEffects() const { -+ return m_expressiveDefaultEffects; -+} -+ -+QEasingCurve AnimTokens::expressiveSlowEffects() const { -+ return m_expressiveSlowEffects; -+} -+ -+AnimDurations* AnimTokens::durations() const { -+ return m_durations; -+} -+ -+QEasingCurve AnimTokens::buildCurve(const QList& points) { -+ QEasingCurve curve(QEasingCurve::BezierSpline); -+ -+ // Points come in pairs of (x, y) forming cubic bezier segments. -+ // Each segment needs 3 control points: c1, c2, endPoint. -+ // So 6 values per segment: c1x, c1y, c2x, c2y, endX, endY. -+ for (int i = 0; i + 5 < points.size(); i += 6) { -+ QPointF c1(points[i], points[i + 1]); -+ QPointF c2(points[i + 2], points[i + 3]); -+ QPointF end(points[i + 4], points[i + 5]); -+ curve.addCubicBezierSegment(c1, c2, end); -+ } -+ -+ return curve; -+} -+ -+void AnimTokens::rebuildCurves() { -+ if (!m_curves) -+ return; -+ -+ m_emphasized = buildCurve(m_curves->emphasized()); -+ m_emphasizedAccel = buildCurve(m_curves->emphasizedAccel()); -+ m_emphasizedDecel = buildCurve(m_curves->emphasizedDecel()); -+ m_standard = buildCurve(m_curves->standard()); -+ m_standardAccel = buildCurve(m_curves->standardAccel()); -+ m_standardDecel = buildCurve(m_curves->standardDecel()); -+ m_expressiveFastSpatial = buildCurve(m_curves->expressiveFastSpatial()); -+ m_expressiveDefaultSpatial = buildCurve(m_curves->expressiveDefaultSpatial()); -+ m_expressiveSlowSpatial = buildCurve(m_curves->expressiveSlowSpatial()); -+ m_expressiveFastEffects = buildCurve(m_curves->expressiveFastEffects()); -+ m_expressiveDefaultEffects = buildCurve(m_curves->expressiveDefaultEffects()); -+ m_expressiveSlowEffects = buildCurve(m_curves->expressiveSlowEffects()); -+ -+ emit curvesChanged(); -+} -+ -+void AnimTokens::bindCurves(AnimCurves* curves) { -+ m_curves = curves; -+ -+ // Rebuild when any curve control points change -+ connect(curves, &AnimCurves::propertiesChanged, this, &AnimTokens::rebuildCurves); -+ -+ rebuildCurves(); -+} -+ -+void AnimTokens::bindDurations(AnimDurations* durations) { -+ if (m_durations == durations) -+ return; -+ -+ m_durations = durations; -+ emit durationsChanged(); -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/anim.hpp b/plugin/src/Caelestia/Config/anim.hpp -new file mode 100644 -index 00000000..895b8e06 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/anim.hpp -@@ -0,0 +1,78 @@ -+#pragma once -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+class AnimCurves; -+class AnimDurations; -+ -+class AnimTokens : public QObject { -+ Q_OBJECT -+ Q_MOC_INCLUDE("tokens.hpp") // AnimCurves -+ Q_MOC_INCLUDE("appearanceconfig.hpp") // AnimDurations -+ QML_ANONYMOUS -+ -+ Q_PROPERTY(QEasingCurve emphasized READ emphasized NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve emphasizedAccel READ emphasizedAccel NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve emphasizedDecel READ emphasizedDecel NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve standard READ standard NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve standardAccel READ standardAccel NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve standardDecel READ standardDecel NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve expressiveFastSpatial READ expressiveFastSpatial NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve expressiveFastEffects READ expressiveFastEffects NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY curvesChanged) -+ Q_PROPERTY(QEasingCurve expressiveSlowEffects READ expressiveSlowEffects NOTIFY curvesChanged) -+ -+ Q_PROPERTY(caelestia::config::AnimDurations* durations READ durations NOTIFY durationsChanged) -+ -+public: -+ explicit AnimTokens(QObject* parent = nullptr); -+ -+ void bindCurves(AnimCurves* curves); -+ void bindDurations(AnimDurations* durations); -+ -+ [[nodiscard]] QEasingCurve emphasized() const; -+ [[nodiscard]] QEasingCurve emphasizedAccel() const; -+ [[nodiscard]] QEasingCurve emphasizedDecel() const; -+ [[nodiscard]] QEasingCurve standard() const; -+ [[nodiscard]] QEasingCurve standardAccel() const; -+ [[nodiscard]] QEasingCurve standardDecel() const; -+ [[nodiscard]] QEasingCurve expressiveFastSpatial() const; -+ [[nodiscard]] QEasingCurve expressiveDefaultSpatial() const; -+ [[nodiscard]] QEasingCurve expressiveSlowSpatial() const; -+ [[nodiscard]] QEasingCurve expressiveFastEffects() const; -+ [[nodiscard]] QEasingCurve expressiveDefaultEffects() const; -+ [[nodiscard]] QEasingCurve expressiveSlowEffects() const; -+ [[nodiscard]] AnimDurations* durations() const; -+ -+signals: -+ void curvesChanged(); -+ void durationsChanged(); -+ -+private: -+ void rebuildCurves(); -+ static QEasingCurve buildCurve(const QList& points); -+ -+ AnimCurves* m_curves = nullptr; -+ AnimDurations* m_durations = nullptr; -+ -+ QEasingCurve m_emphasized; -+ QEasingCurve m_emphasizedAccel; -+ QEasingCurve m_emphasizedDecel; -+ QEasingCurve m_standard; -+ QEasingCurve m_standardAccel; -+ QEasingCurve m_standardDecel; -+ QEasingCurve m_expressiveFastSpatial; -+ QEasingCurve m_expressiveDefaultSpatial; -+ QEasingCurve m_expressiveSlowSpatial; -+ QEasingCurve m_expressiveFastEffects; -+ QEasingCurve m_expressiveDefaultEffects; -+ QEasingCurve m_expressiveSlowEffects; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/appearanceconfig.cpp b/plugin/src/Caelestia/Config/appearanceconfig.cpp -new file mode 100644 -index 00000000..59228c67 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/appearanceconfig.cpp -@@ -0,0 +1,183 @@ -+#include "appearanceconfig.hpp" -+#include "tokens.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+// Helper: connect all changed signals from a token object to a single valuesChanged signal, -+// plus connect the local scaleChanged signal. -+template static void connectTokenSignals(Source* source, Target* target) { -+ const auto* meta = source->metaObject(); -+ -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ auto prop = meta->property(i); -+ -+ if (prop.hasNotifySignal()) -+ QObject::connect(source, prop.notifySignal(), target, -+ target->metaObject()->method(target->metaObject()->indexOfSignal("valuesChanged()"))); -+ } -+ -+ QObject::connect(target, &Target::scaleChanged, target, &Target::valuesChanged); -+} -+ -+// AppearanceRounding -+ -+void AppearanceRounding::bindTokens(RoundingTokens* tokens) { -+ m_tokens = tokens; -+ connectTokenSignals(tokens, this); -+} -+ -+int AppearanceRounding::extraSmall() const { -+ return m_tokens ? static_cast(m_tokens->extraSmall() * m_scale) : 0; -+} -+ -+int AppearanceRounding::small() const { -+ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; -+} -+ -+int AppearanceRounding::normal() const { -+ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -+} -+ -+int AppearanceRounding::large() const { -+ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; -+} -+ -+int AppearanceRounding::full() const { -+ return m_tokens ? static_cast(m_tokens->full() * m_scale) : 0; -+} -+ -+// AppearanceSpacing -+ -+void AppearanceSpacing::bindTokens(SpacingTokens* tokens) { -+ m_tokens = tokens; -+ connectTokenSignals(tokens, this); -+} -+ -+int AppearanceSpacing::small() const { -+ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; -+} -+ -+int AppearanceSpacing::smaller() const { -+ return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -+} -+ -+int AppearanceSpacing::normal() const { -+ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -+} -+ -+int AppearanceSpacing::larger() const { -+ return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; -+} -+ -+int AppearanceSpacing::large() const { -+ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; -+} -+ -+// AppearancePadding -+ -+void AppearancePadding::bindTokens(PaddingTokens* tokens) { -+ m_tokens = tokens; -+ connectTokenSignals(tokens, this); -+} -+ -+int AppearancePadding::small() const { -+ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; -+} -+ -+int AppearancePadding::smaller() const { -+ return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -+} -+ -+int AppearancePadding::normal() const { -+ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -+} -+ -+int AppearancePadding::larger() const { -+ return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; -+} -+ -+int AppearancePadding::large() const { -+ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; -+} -+ -+// FontSize -+ -+void FontSize::bindTokens(FontSizeTokens* tokens) { -+ m_tokens = tokens; -+ connectTokenSignals(tokens, this); -+} -+ -+int FontSize::small() const { -+ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; -+} -+ -+int FontSize::smaller() const { -+ return m_tokens ? static_cast(m_tokens->smaller() * m_scale) : 0; -+} -+ -+int FontSize::normal() const { -+ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -+} -+ -+int FontSize::larger() const { -+ return m_tokens ? static_cast(m_tokens->larger() * m_scale) : 0; -+} -+ -+int FontSize::large() const { -+ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; -+} -+ -+int FontSize::extraLarge() const { -+ return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; -+} -+ -+// AnimDurations -+ -+void AnimDurations::bindTokens(AnimDurationTokens* tokens) { -+ m_tokens = tokens; -+ connectTokenSignals(tokens, this); -+} -+ -+int AnimDurations::small() const { -+ return m_tokens ? static_cast(m_tokens->small() * m_scale) : 0; -+} -+ -+int AnimDurations::normal() const { -+ return m_tokens ? static_cast(m_tokens->normal() * m_scale) : 0; -+} -+ -+int AnimDurations::large() const { -+ return m_tokens ? static_cast(m_tokens->large() * m_scale) : 0; -+} -+ -+int AnimDurations::extraLarge() const { -+ return m_tokens ? static_cast(m_tokens->extraLarge() * m_scale) : 0; -+} -+ -+int AnimDurations::expressiveFastSpatial() const { -+ return m_tokens ? static_cast(m_tokens->expressiveFastSpatial() * m_scale) : 0; -+} -+ -+int AnimDurations::expressiveDefaultSpatial() const { -+ return m_tokens ? static_cast(m_tokens->expressiveDefaultSpatial() * m_scale) : 0; -+} -+ -+int AnimDurations::expressiveSlowSpatial() const { -+ return m_tokens ? static_cast(m_tokens->expressiveSlowSpatial() * m_scale) : 0; -+} -+ -+int AnimDurations::expressiveFastEffects() const { -+ return m_tokens ? static_cast(m_tokens->expressiveFastEffects() * m_scale) : 0; -+} -+ -+int AnimDurations::expressiveDefaultEffects() const { -+ return m_tokens ? static_cast(m_tokens->expressiveDefaultEffects() * m_scale) : 0; -+} -+ -+int AnimDurations::expressiveSlowEffects() const { -+ return m_tokens ? static_cast(m_tokens->expressiveSlowEffects() * m_scale) : 0; -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/appearanceconfig.hpp b/plugin/src/Caelestia/Config/appearanceconfig.hpp -new file mode 100644 -index 00000000..3446ea72 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/appearanceconfig.hpp -@@ -0,0 +1,259 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+// Forward declare token types from advancedconfig.hpp -+class RoundingTokens; -+class SpacingTokens; -+class PaddingTokens; -+class FontSizeTokens; -+class AnimDurationTokens; -+ -+class AppearanceRounding : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, scale, 1) -+ -+ Q_PROPERTY(int extraSmall READ extraSmall NOTIFY valuesChanged) -+ Q_PROPERTY(int small READ small NOTIFY valuesChanged) -+ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) -+ Q_PROPERTY(int large READ large NOTIFY valuesChanged) -+ Q_PROPERTY(int full READ full NOTIFY valuesChanged) -+ -+public: -+ explicit AppearanceRounding(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+ -+ void bindTokens(RoundingTokens* tokens); -+ -+ [[nodiscard]] int extraSmall() const; -+ [[nodiscard]] int small() const; -+ [[nodiscard]] int normal() const; -+ [[nodiscard]] int large() const; -+ [[nodiscard]] int full() const; -+ -+signals: -+ void valuesChanged(); -+ -+private: -+ RoundingTokens* m_tokens = nullptr; -+}; -+ -+class AppearanceSpacing : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, scale, 1) -+ -+ Q_PROPERTY(int small READ small NOTIFY valuesChanged) -+ Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) -+ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) -+ Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) -+ Q_PROPERTY(int large READ large NOTIFY valuesChanged) -+ -+public: -+ explicit AppearanceSpacing(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+ -+ void bindTokens(SpacingTokens* tokens); -+ -+ [[nodiscard]] int small() const; -+ [[nodiscard]] int smaller() const; -+ [[nodiscard]] int normal() const; -+ [[nodiscard]] int larger() const; -+ [[nodiscard]] int large() const; -+ -+signals: -+ void valuesChanged(); -+ -+private: -+ SpacingTokens* m_tokens = nullptr; -+}; -+ -+class AppearancePadding : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, scale, 1) -+ -+ Q_PROPERTY(int small READ small NOTIFY valuesChanged) -+ Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) -+ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) -+ Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) -+ Q_PROPERTY(int large READ large NOTIFY valuesChanged) -+ -+public: -+ explicit AppearancePadding(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+ -+ void bindTokens(PaddingTokens* tokens); -+ -+ [[nodiscard]] int small() const; -+ [[nodiscard]] int smaller() const; -+ [[nodiscard]] int normal() const; -+ [[nodiscard]] int larger() const; -+ [[nodiscard]] int large() const; -+ -+signals: -+ void valuesChanged(); -+ -+private: -+ PaddingTokens* m_tokens = nullptr; -+}; -+ -+class FontFamily : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(QString, sans, QStringLiteral("Rubik")) -+ CONFIG_PROPERTY(QString, mono, QStringLiteral("CaskaydiaCove NF")) -+ CONFIG_PROPERTY(QString, material, QStringLiteral("Material Symbols Rounded")) -+ CONFIG_PROPERTY(QString, clock, QStringLiteral("Rubik")) -+ -+public: -+ explicit FontFamily(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class FontSize : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, scale, 1) -+ -+ Q_PROPERTY(int small READ small NOTIFY valuesChanged) -+ Q_PROPERTY(int smaller READ smaller NOTIFY valuesChanged) -+ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) -+ Q_PROPERTY(int larger READ larger NOTIFY valuesChanged) -+ Q_PROPERTY(int large READ large NOTIFY valuesChanged) -+ Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) -+ -+public: -+ explicit FontSize(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+ -+ void bindTokens(FontSizeTokens* tokens); -+ -+ [[nodiscard]] int small() const; -+ [[nodiscard]] int smaller() const; -+ [[nodiscard]] int normal() const; -+ [[nodiscard]] int larger() const; -+ [[nodiscard]] int large() const; -+ [[nodiscard]] int extraLarge() const; -+ -+signals: -+ void valuesChanged(); -+ -+private: -+ FontSizeTokens* m_tokens = nullptr; -+}; -+ -+class AppearanceFont : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_SUBOBJECT(FontFamily, family) -+ CONFIG_SUBOBJECT(FontSize, size) -+ -+public: -+ explicit AppearanceFont(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_family(new FontFamily(this)) -+ , m_size(new FontSize(this)) {} -+}; -+ -+class AnimDurations : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(qreal, scale, 1) -+ -+ Q_PROPERTY(int small READ small NOTIFY valuesChanged) -+ Q_PROPERTY(int normal READ normal NOTIFY valuesChanged) -+ Q_PROPERTY(int large READ large NOTIFY valuesChanged) -+ Q_PROPERTY(int extraLarge READ extraLarge NOTIFY valuesChanged) -+ Q_PROPERTY(int expressiveFastSpatial READ expressiveFastSpatial NOTIFY valuesChanged) -+ Q_PROPERTY(int expressiveDefaultSpatial READ expressiveDefaultSpatial NOTIFY valuesChanged) -+ Q_PROPERTY(int expressiveSlowSpatial READ expressiveSlowSpatial NOTIFY valuesChanged) -+ Q_PROPERTY(int expressiveFastEffects READ expressiveFastEffects NOTIFY valuesChanged) -+ Q_PROPERTY(int expressiveDefaultEffects READ expressiveDefaultEffects NOTIFY valuesChanged) -+ Q_PROPERTY(int expressiveSlowEffects READ expressiveSlowEffects NOTIFY valuesChanged) -+ -+public: -+ explicit AnimDurations(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+ -+ void bindTokens(AnimDurationTokens* tokens); -+ -+ [[nodiscard]] int small() const; -+ [[nodiscard]] int normal() const; -+ [[nodiscard]] int large() const; -+ [[nodiscard]] int extraLarge() const; -+ [[nodiscard]] int expressiveFastSpatial() const; -+ [[nodiscard]] int expressiveDefaultSpatial() const; -+ [[nodiscard]] int expressiveSlowSpatial() const; -+ [[nodiscard]] int expressiveFastEffects() const; -+ [[nodiscard]] int expressiveDefaultEffects() const; -+ [[nodiscard]] int expressiveSlowEffects() const; -+ -+signals: -+ void valuesChanged(); -+ -+private: -+ AnimDurationTokens* m_tokens = nullptr; -+}; -+ -+class AppearanceAnim : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_SUBOBJECT(AnimDurations, durations) -+ -+public: -+ explicit AppearanceAnim(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_durations(new AnimDurations(this)) {} -+}; -+ -+class AppearanceTransparency : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(bool, enabled, false) -+ CONFIG_GLOBAL_PROPERTY(qreal, base, 0.85) -+ CONFIG_GLOBAL_PROPERTY(qreal, layers, 0.4) -+ -+public: -+ explicit AppearanceTransparency(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class AppearanceConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, deformScale, 1) -+ CONFIG_SUBOBJECT(AppearanceRounding, rounding) -+ CONFIG_SUBOBJECT(AppearanceSpacing, spacing) -+ CONFIG_SUBOBJECT(AppearancePadding, padding) -+ CONFIG_SUBOBJECT(AppearanceFont, font) -+ CONFIG_SUBOBJECT(AppearanceAnim, anim) -+ CONFIG_SUBOBJECT(AppearanceTransparency, transparency) -+ -+public: -+ explicit AppearanceConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_rounding(new AppearanceRounding(this)) -+ , m_spacing(new AppearanceSpacing(this)) -+ , m_padding(new AppearancePadding(this)) -+ , m_font(new AppearanceFont(this)) -+ , m_anim(new AppearanceAnim(this)) -+ , m_transparency(new AppearanceTransparency(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/backgroundconfig.hpp b/plugin/src/Caelestia/Config/backgroundconfig.hpp -new file mode 100644 -index 00000000..743cd787 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/backgroundconfig.hpp -@@ -0,0 +1,84 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+class DesktopClockBackground : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, false) -+ CONFIG_PROPERTY(qreal, opacity, 0.7) -+ CONFIG_PROPERTY(bool, blur, true) -+ -+public: -+ explicit DesktopClockBackground(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class DesktopClockShadow : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(qreal, opacity, 0.7) -+ CONFIG_PROPERTY(qreal, blur, 0.4) -+ -+public: -+ explicit DesktopClockShadow(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class DesktopClock : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, false) -+ CONFIG_PROPERTY(qreal, scale, 1.0) -+ CONFIG_PROPERTY(QString, position, QStringLiteral("bottom-right")) -+ CONFIG_PROPERTY(bool, invertColors, false) -+ CONFIG_SUBOBJECT(DesktopClockBackground, background) -+ CONFIG_SUBOBJECT(DesktopClockShadow, shadow) -+ -+public: -+ explicit DesktopClock(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_background(new DesktopClockBackground(this)) -+ , m_shadow(new DesktopClockShadow(this)) {} -+}; -+ -+class BackgroundVisualiser : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, false) -+ CONFIG_PROPERTY(bool, autoHide, true) -+ CONFIG_PROPERTY(bool, blur, false) -+ CONFIG_PROPERTY(qreal, rounding, 1) -+ CONFIG_PROPERTY(qreal, spacing, 1) -+ -+public: -+ explicit BackgroundVisualiser(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BackgroundConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(bool, wallpaperEnabled, true) -+ CONFIG_SUBOBJECT(DesktopClock, desktopClock) -+ CONFIG_SUBOBJECT(BackgroundVisualiser, visualiser) -+ -+public: -+ explicit BackgroundConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_desktopClock(new DesktopClock(this)) -+ , m_visualiser(new BackgroundVisualiser(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/barconfig.hpp b/plugin/src/Caelestia/Config/barconfig.hpp -new file mode 100644 -index 00000000..43d43205 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/barconfig.hpp -@@ -0,0 +1,166 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+using Qt::StringLiterals::operator""_s; -+ -+class BarScrollActions : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, workspaces, true) -+ CONFIG_PROPERTY(bool, volume, true) -+ CONFIG_PROPERTY(bool, brightness, true) -+ -+public: -+ explicit BarScrollActions(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BarPopouts : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, activeWindow, true) -+ CONFIG_PROPERTY(bool, tray, true) -+ CONFIG_PROPERTY(bool, statusIcons, true) -+ -+public: -+ explicit BarPopouts(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BarWorkspaces : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, shown, 5) -+ CONFIG_PROPERTY(bool, activeIndicator, true) -+ CONFIG_PROPERTY(bool, occupiedBg, false) -+ CONFIG_PROPERTY(bool, showWindows, true) -+ CONFIG_PROPERTY(bool, showWindowsOnSpecialWorkspaces, true) -+ CONFIG_PROPERTY(int, maxWindowIcons, 5) -+ CONFIG_PROPERTY(bool, activeTrail, false) -+ CONFIG_GLOBAL_PROPERTY(bool, perMonitorWorkspaces, true) -+ CONFIG_PROPERTY(QString, label, u" "_s) -+ CONFIG_PROPERTY(QString, occupiedLabel, u"󰮯"_s) -+ CONFIG_PROPERTY(QString, activeLabel, u"󰮯"_s) -+ CONFIG_PROPERTY(QString, capitalisation, u"preserve"_s) -+ CONFIG_GLOBAL_PROPERTY(QVariantList, specialWorkspaceIcons) -+ CONFIG_GLOBAL_PROPERTY(QVariantList, windowIcons, -+ { vmap({ -+ { u"regex"_s, u"steam(_app_(default|[0-9]+))?"_s }, -+ { u"icon"_s, u"sports_esports"_s }, -+ }) }) -+ -+public: -+ explicit BarWorkspaces(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BarActiveWindow : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, compact, false) -+ CONFIG_PROPERTY(bool, inverted, false) -+ CONFIG_PROPERTY(bool, showOnHover, true) -+ -+public: -+ explicit BarActiveWindow(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BarTray : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, background, false) -+ CONFIG_PROPERTY(bool, recolour, false) -+ CONFIG_PROPERTY(bool, compact, false) -+ CONFIG_GLOBAL_PROPERTY(QVariantList, iconSubs) -+ CONFIG_GLOBAL_PROPERTY(QStringList, hiddenIcons) -+ -+public: -+ explicit BarTray(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BarStatus : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, showAudio, false) -+ CONFIG_PROPERTY(bool, showMicrophone, false) -+ CONFIG_PROPERTY(bool, showKbLayout, false) -+ CONFIG_PROPERTY(bool, showNetwork, true) -+ CONFIG_PROPERTY(bool, showWifi, true) -+ CONFIG_PROPERTY(bool, showBluetooth, true) -+ CONFIG_PROPERTY(bool, showBattery, true) -+ CONFIG_PROPERTY(bool, showLockStatus, true) -+ -+public: -+ explicit BarStatus(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BarClock : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, background, false) -+ CONFIG_PROPERTY(bool, showDate, false) -+ CONFIG_PROPERTY(bool, showIcon, true) -+ -+public: -+ explicit BarClock(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class BarConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, persistent, true) -+ CONFIG_PROPERTY(bool, showOnHover, true) -+ CONFIG_PROPERTY(int, dragThreshold, 20) -+ CONFIG_SUBOBJECT(BarScrollActions, scrollActions) -+ CONFIG_SUBOBJECT(BarPopouts, popouts) -+ CONFIG_SUBOBJECT(BarWorkspaces, workspaces) -+ CONFIG_SUBOBJECT(BarActiveWindow, activeWindow) -+ CONFIG_SUBOBJECT(BarTray, tray) -+ CONFIG_SUBOBJECT(BarStatus, status) -+ CONFIG_SUBOBJECT(BarClock, clock) -+ CONFIG_PROPERTY(QVariantList, entries, -+ { -+ vmap({ { u"id"_s, u"logo"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"workspaces"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"activeWindow"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"spacer"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"tray"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"clock"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"statusIcons"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"power"_s }, { u"enabled"_s, true } }), -+ }) -+ CONFIG_PROPERTY(QStringList, excludedScreens) -+ -+public: -+ explicit BarConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_scrollActions(new BarScrollActions(this)) -+ , m_popouts(new BarPopouts(this)) -+ , m_workspaces(new BarWorkspaces(this)) -+ , m_activeWindow(new BarActiveWindow(this)) -+ , m_tray(new BarTray(this)) -+ , m_status(new BarStatus(this)) -+ , m_clock(new BarClock(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/borderconfig.hpp b/plugin/src/Caelestia/Config/borderconfig.hpp -new file mode 100644 -index 00000000..9de604e8 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/borderconfig.hpp -@@ -0,0 +1,29 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+class BorderConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, thickness, 10) -+ CONFIG_PROPERTY(int, rounding, 25) -+ CONFIG_PROPERTY(int, smoothing, 32) -+ -+ Q_PROPERTY(int minThickness READ minThickness CONSTANT) -+ Q_PROPERTY(int clampedThickness READ clampedThickness NOTIFY thicknessChanged) -+ -+public: -+ explicit BorderConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+ -+ [[nodiscard]] static int minThickness() { return 2; } -+ -+ [[nodiscard]] int clampedThickness() const { return std::max(minThickness(), m_thickness); } -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/config.cpp b/plugin/src/Caelestia/Config/config.cpp -new file mode 100644 -index 00000000..ff939f99 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/config.cpp -@@ -0,0 +1,120 @@ -+#include "config.hpp" -+#include "appearanceconfig.hpp" -+#include "backgroundconfig.hpp" -+#include "barconfig.hpp" -+#include "borderconfig.hpp" -+#include "controlcenterconfig.hpp" -+#include "dashboardconfig.hpp" -+#include "generalconfig.hpp" -+#include "launcherconfig.hpp" -+#include "lockconfig.hpp" -+#include "monitorconfigmanager.hpp" -+#include "notifsconfig.hpp" -+#include "osdconfig.hpp" -+#include "serviceconfig.hpp" -+#include "sessionconfig.hpp" -+#include "sidebarconfig.hpp" -+#include "tokens.hpp" -+#include "userpaths.hpp" -+#include "utilitiesconfig.hpp" -+#include "winfoconfig.hpp" -+ -+#include -+#include -+ -+namespace caelestia::config { -+ -+namespace { -+ -+QString configDir() { -+ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); -+} -+ -+} // namespace -+ -+GlobalConfig::GlobalConfig(QObject* parent) -+ : RootConfig(parent) -+ , m_appearance(new AppearanceConfig(this)) -+ , m_general(new GeneralConfig(this)) -+ , m_background(new BackgroundConfig(this)) -+ , m_bar(new BarConfig(this)) -+ , m_border(new BorderConfig(this)) -+ , m_dashboard(new DashboardConfig(this)) -+ , m_controlCenter(new ControlCenterConfig(this)) -+ , m_launcher(new LauncherConfig(this)) -+ , m_notifs(new NotifsConfig(this)) -+ , m_osd(new OsdConfig(this)) -+ , m_session(new SessionConfig(this)) -+ , m_winfo(new WInfoConfig(this)) -+ , m_lock(new LockConfig(this)) -+ , m_utilities(new UtilitiesConfig(this)) -+ , m_sidebar(new SidebarConfig(this)) -+ , m_services(new ServiceConfig(this)) -+ , m_paths(new UserPaths(this)) { -+ setupFileBackend(configDir() + QStringLiteral("shell.json")); -+} -+ -+GlobalConfig::GlobalConfig(GlobalConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) -+ : RootConfig(parent) -+ , m_appearance(new AppearanceConfig(this)) -+ , m_general(new GeneralConfig(this)) -+ , m_background(new BackgroundConfig(this)) -+ , m_bar(new BarConfig(this)) -+ , m_border(new BorderConfig(this)) -+ , m_dashboard(new DashboardConfig(this)) -+ , m_controlCenter(new ControlCenterConfig(this)) -+ , m_launcher(new LauncherConfig(this)) -+ , m_notifs(new NotifsConfig(this)) -+ , m_osd(new OsdConfig(this)) -+ , m_session(new SessionConfig(this)) -+ , m_winfo(new WInfoConfig(this)) -+ , m_lock(new LockConfig(this)) -+ , m_utilities(new UtilitiesConfig(this)) -+ , m_sidebar(new SidebarConfig(this)) -+ , m_services(new ServiceConfig(this)) -+ , m_paths(new UserPaths(this)) { -+ if (!filePath.isEmpty()) -+ setupFileBackend(filePath, screen); -+ if (fallback) -+ syncFromGlobal(fallback); -+ -+ // Bind appearance computed properties to token base values -+ bindAppearanceTokens(); -+} -+ -+GlobalConfig* GlobalConfig::instance() { -+ static GlobalConfig instance; -+ instance.bindAppearanceTokens(); -+ return &instance; -+} -+ -+GlobalConfig* GlobalConfig::defaults() { -+ if (!m_defaults) -+ m_defaults = new GlobalConfig(nullptr, QString(), QString(), this); -+ return m_defaults; -+} -+ -+void GlobalConfig::bindAppearanceTokens() { -+ if (m_tokensBound) -+ return; -+ -+ qCDebug(lcConfig) << "GlobalConfig::bindAppearanceTokens: binding appearance to token values"; -+ auto* const tokenAppearance = TokenConfig::instance()->appearance(); -+ m_appearance->rounding()->bindTokens(tokenAppearance->rounding()); -+ m_appearance->spacing()->bindTokens(tokenAppearance->spacing()); -+ m_appearance->padding()->bindTokens(tokenAppearance->padding()); -+ m_appearance->font()->size()->bindTokens(tokenAppearance->fontSize()); -+ m_appearance->anim()->durations()->bindTokens(tokenAppearance->animDurations()); -+ m_tokensBound = true; -+} -+ -+GlobalConfig* GlobalConfig::forScreen(const QString& screen) { -+ return MonitorConfigManager::instance()->configForScreen(screen); -+} -+ -+GlobalConfig* GlobalConfig::create(QQmlEngine*, QJSEngine*) { -+ QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); -+ return instance(); -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/config.hpp b/plugin/src/Caelestia/Config/config.hpp -new file mode 100644 -index 00000000..248a3f22 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/config.hpp -@@ -0,0 +1,86 @@ -+#pragma once -+ -+#include "rootconfig.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+class AppearanceConfig; -+class BackgroundConfig; -+class BarConfig; -+class BorderConfig; -+class ControlCenterConfig; -+class DashboardConfig; -+class GeneralConfig; -+class LauncherConfig; -+class LockConfig; -+class NotifsConfig; -+class OsdConfig; -+class ServiceConfig; -+class SessionConfig; -+class SidebarConfig; -+class UserPaths; -+class UtilitiesConfig; -+class WInfoConfig; -+ -+class GlobalConfig : public RootConfig { -+ Q_OBJECT -+ QML_ELEMENT -+ QML_SINGLETON -+ Q_MOC_INCLUDE("appearanceconfig.hpp") -+ Q_MOC_INCLUDE("backgroundconfig.hpp") -+ Q_MOC_INCLUDE("barconfig.hpp") -+ Q_MOC_INCLUDE("borderconfig.hpp") -+ Q_MOC_INCLUDE("controlcenterconfig.hpp") -+ Q_MOC_INCLUDE("dashboardconfig.hpp") -+ Q_MOC_INCLUDE("generalconfig.hpp") -+ Q_MOC_INCLUDE("launcherconfig.hpp") -+ Q_MOC_INCLUDE("lockconfig.hpp") -+ Q_MOC_INCLUDE("notifsconfig.hpp") -+ Q_MOC_INCLUDE("osdconfig.hpp") -+ Q_MOC_INCLUDE("serviceconfig.hpp") -+ Q_MOC_INCLUDE("sessionconfig.hpp") -+ Q_MOC_INCLUDE("sidebarconfig.hpp") -+ Q_MOC_INCLUDE("userpaths.hpp") -+ Q_MOC_INCLUDE("utilitiesconfig.hpp") -+ Q_MOC_INCLUDE("winfoconfig.hpp") -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_SUBOBJECT(AppearanceConfig, appearance) -+ CONFIG_SUBOBJECT(GeneralConfig, general) -+ CONFIG_SUBOBJECT(BackgroundConfig, background) -+ CONFIG_SUBOBJECT(BarConfig, bar) -+ CONFIG_SUBOBJECT(BorderConfig, border) -+ CONFIG_SUBOBJECT(DashboardConfig, dashboard) -+ CONFIG_SUBOBJECT(ControlCenterConfig, controlCenter) -+ CONFIG_SUBOBJECT(LauncherConfig, launcher) -+ CONFIG_SUBOBJECT(NotifsConfig, notifs) -+ CONFIG_SUBOBJECT(OsdConfig, osd) -+ CONFIG_SUBOBJECT(SessionConfig, session) -+ CONFIG_SUBOBJECT(WInfoConfig, winfo) -+ CONFIG_SUBOBJECT(LockConfig, lock) -+ CONFIG_SUBOBJECT(UtilitiesConfig, utilities) -+ CONFIG_SUBOBJECT(SidebarConfig, sidebar) -+ CONFIG_SUBOBJECT(ServiceConfig, services) -+ CONFIG_SUBOBJECT(UserPaths, paths) -+ -+public: -+ static GlobalConfig* instance(); -+ [[nodiscard]] Q_INVOKABLE GlobalConfig* defaults(); -+ [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); -+ static GlobalConfig* create(QQmlEngine*, QJSEngine*); -+ -+ void bindAppearanceTokens(); -+ -+private: -+ friend class MonitorConfigManager; -+ explicit GlobalConfig(QObject* parent = nullptr); -+ explicit GlobalConfig( -+ GlobalConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); -+ -+ GlobalConfig* m_defaults = nullptr; -+ bool m_tokensBound = false; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/configattached.cpp b/plugin/src/Caelestia/Config/configattached.cpp -new file mode 100644 -index 00000000..8d229049 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/configattached.cpp -@@ -0,0 +1,96 @@ -+#include "configattached.hpp" -+#include "config.hpp" -+#include "monitorconfigmanager.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+Config::Config(QObject* parent) -+ : QQuickAttachedPropertyPropagator(parent) { -+ initialize(); -+} -+ -+void Config::classBegin() {} -+ -+void Config::componentComplete() { -+ m_complete = true; -+} -+ -+QString Config::screen() const { -+ return m_screen; -+} -+ -+void Config::inheritScreen(const QString& screen) { -+ if (screen == m_screen) -+ return; -+ -+ m_screen = screen; -+ -+ if (m_screen.isEmpty()) -+ m_config = nullptr; -+ else -+ m_config = MonitorConfigManager::instance()->configForScreen(m_screen); -+ -+ propagateScreen(); -+ emit sourceChanged(); -+} -+ -+void Config::propagateScreen() { -+ const auto children = attachedChildren(); -+ for (auto* const child : children) { -+ auto* const config = qobject_cast(child); -+ if (config) -+ config->inheritScreen(m_screen); -+ } -+} -+ -+void Config::attachedParentChange( -+ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { -+ Q_UNUSED(oldParent); -+ auto* const config = qobject_cast(newParent); -+ if (config) -+ inheritScreen(config->screen()); -+} -+ -+#define CONFIG_ATTACHED_GETTER(Type, name) \ -+ const Type* Config::name() const { \ -+ if (m_config) \ -+ return m_config->name(); \ -+ /* Suppress warnings before component is complete if attached to a QQuickItem. */ \ -+ /* Raw QObjects are unable to inherit the screen (only QQuickItems can). */ \ -+ if ((m_complete || !qobject_cast(parent())) && parent()) \ -+ qCWarning(lcConfig, "Config.%s accessed without a screen set on %s", #name, \ -+ parent()->metaObject()->className()); \ -+ return GlobalConfig::instance()->name(); \ -+ } -+ -+CONFIG_ATTACHED_GETTER(AppearanceConfig, appearance) -+CONFIG_ATTACHED_GETTER(GeneralConfig, general) -+CONFIG_ATTACHED_GETTER(BackgroundConfig, background) -+CONFIG_ATTACHED_GETTER(BarConfig, bar) -+CONFIG_ATTACHED_GETTER(BorderConfig, border) -+CONFIG_ATTACHED_GETTER(DashboardConfig, dashboard) -+CONFIG_ATTACHED_GETTER(ControlCenterConfig, controlCenter) -+CONFIG_ATTACHED_GETTER(LauncherConfig, launcher) -+CONFIG_ATTACHED_GETTER(NotifsConfig, notifs) -+CONFIG_ATTACHED_GETTER(OsdConfig, osd) -+CONFIG_ATTACHED_GETTER(SessionConfig, session) -+CONFIG_ATTACHED_GETTER(WInfoConfig, winfo) -+CONFIG_ATTACHED_GETTER(LockConfig, lock) -+CONFIG_ATTACHED_GETTER(UtilitiesConfig, utilities) -+CONFIG_ATTACHED_GETTER(SidebarConfig, sidebar) -+CONFIG_ATTACHED_GETTER(ServiceConfig, services) -+CONFIG_ATTACHED_GETTER(UserPaths, paths) -+ -+#undef CONFIG_ATTACHED_GETTER -+ -+GlobalConfig* Config::forScreen(const QString& screen) { -+ return GlobalConfig::forScreen(screen); -+} -+ -+Config* Config::qmlAttachedProperties(QObject* object) { -+ return new Config(object); -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/configattached.hpp b/plugin/src/Caelestia/Config/configattached.hpp -new file mode 100644 -index 00000000..815b1080 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/configattached.hpp -@@ -0,0 +1,98 @@ -+#pragma once -+ -+#include "config.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+class Config : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { -+ Q_OBJECT -+ Q_INTERFACES(QQmlParserStatus) -+ QML_ELEMENT -+ QML_UNCREATABLE("") -+ QML_ATTACHED(Config) -+ Q_MOC_INCLUDE("appearanceconfig.hpp") -+ Q_MOC_INCLUDE("backgroundconfig.hpp") -+ Q_MOC_INCLUDE("barconfig.hpp") -+ Q_MOC_INCLUDE("borderconfig.hpp") -+ Q_MOC_INCLUDE("controlcenterconfig.hpp") -+ Q_MOC_INCLUDE("dashboardconfig.hpp") -+ Q_MOC_INCLUDE("generalconfig.hpp") -+ Q_MOC_INCLUDE("launcherconfig.hpp") -+ Q_MOC_INCLUDE("lockconfig.hpp") -+ Q_MOC_INCLUDE("notifsconfig.hpp") -+ Q_MOC_INCLUDE("osdconfig.hpp") -+ Q_MOC_INCLUDE("serviceconfig.hpp") -+ Q_MOC_INCLUDE("sessionconfig.hpp") -+ Q_MOC_INCLUDE("sidebarconfig.hpp") -+ Q_MOC_INCLUDE("userpaths.hpp") -+ Q_MOC_INCLUDE("utilitiesconfig.hpp") -+ Q_MOC_INCLUDE("winfoconfig.hpp") -+ -+ Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::AppearanceConfig* appearance READ appearance NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::GeneralConfig* general READ general NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::BackgroundConfig* background READ background NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::BarConfig* bar READ bar NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::BorderConfig* border READ border NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::DashboardConfig* dashboard READ dashboard NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::ControlCenterConfig* controlCenter READ controlCenter NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::LauncherConfig* launcher READ launcher NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::NotifsConfig* notifs READ notifs NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::OsdConfig* osd READ osd NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::SessionConfig* session READ session NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::WInfoConfig* winfo READ winfo NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::LockConfig* lock READ lock NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::UtilitiesConfig* utilities READ utilities NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::SidebarConfig* sidebar READ sidebar NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::ServiceConfig* services READ services NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::UserPaths* paths READ paths NOTIFY sourceChanged) -+ -+public: -+ explicit Config(QObject* parent = nullptr); -+ -+ [[nodiscard]] QString screen() const; -+ void inheritScreen(const QString& screen); -+ -+ [[nodiscard]] const AppearanceConfig* appearance() const; -+ [[nodiscard]] const GeneralConfig* general() const; -+ [[nodiscard]] const BackgroundConfig* background() const; -+ [[nodiscard]] const BarConfig* bar() const; -+ [[nodiscard]] const BorderConfig* border() const; -+ [[nodiscard]] const DashboardConfig* dashboard() const; -+ [[nodiscard]] const ControlCenterConfig* controlCenter() const; -+ [[nodiscard]] const LauncherConfig* launcher() const; -+ [[nodiscard]] const NotifsConfig* notifs() const; -+ [[nodiscard]] const OsdConfig* osd() const; -+ [[nodiscard]] const SessionConfig* session() const; -+ [[nodiscard]] const WInfoConfig* winfo() const; -+ [[nodiscard]] const LockConfig* lock() const; -+ [[nodiscard]] const UtilitiesConfig* utilities() const; -+ [[nodiscard]] const SidebarConfig* sidebar() const; -+ [[nodiscard]] const ServiceConfig* services() const; -+ [[nodiscard]] const UserPaths* paths() const; -+ -+ [[nodiscard]] Q_INVOKABLE static GlobalConfig* forScreen(const QString& screen); -+ -+ static Config* qmlAttachedProperties(QObject* object); -+ -+signals: -+ void sourceChanged(); -+ -+protected: -+ void attachedParentChange( -+ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; -+ -+private: -+ void classBegin() override; -+ void componentComplete() override; -+ -+ void propagateScreen(); -+ -+ bool m_complete = false; -+ QString m_screen; -+ GlobalConfig* m_config = nullptr; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/configobject.cpp b/plugin/src/Caelestia/Config/configobject.cpp -new file mode 100644 -index 00000000..030ecabe ---- /dev/null -+++ b/plugin/src/Caelestia/Config/configobject.cpp -@@ -0,0 +1,316 @@ -+#include "configobject.hpp" -+ -+#include -+#include -+#include -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+Q_LOGGING_CATEGORY(lcConfig, "caelestia.config", QtInfoMsg) -+ -+// ConfigObject -+ -+ConfigObject::ConfigObject(QObject* parent) -+ : QObject(parent) {} -+ -+void ConfigObject::loadFromJson(const QJsonObject& obj) { -+ const auto* meta = metaObject(); -+ -+ qCDebug(lcConfig) << "Loading JSON into" << meta->className() << "with" << obj.keys().size() -+ << "keys:" << obj.keys(); -+ -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ auto prop = meta->property(i); -+ const auto key = QString::fromUtf8(prop.name()); -+ -+ if (!obj.contains(key)) -+ continue; -+ -+ if (isGlobalOnly(key)) -+ qCWarning(lcConfig, "Option '%s' is global-only and will be ignored in per-monitor config", -+ qUtf8Printable(propertyPath(key))); -+ -+ const auto jsonVal = obj.value(key); -+ -+ // Recurse into sub-objects -+ auto current = prop.read(this); -+ auto* subObj = current.value(); -+ -+ if (subObj) { -+ qCDebug(lcConfig) << " Recursing into sub-object" << key; -+ subObj->loadFromJson(jsonVal.toObject()); -+ continue; -+ } -+ -+ // Skip read-only properties -+ if (!prop.isWritable()) -+ continue; -+ -+ // Handle QStringList explicitly (QJsonArray → QStringList needs manual conversion) -+ if (prop.metaType().id() == QMetaType::QStringList) { -+ QStringList list; -+ const auto jsonArr = jsonVal.toArray(); -+ for (const auto& v : jsonArr) -+ list.append(v.toString()); -+ prop.write(this, QVariant::fromValue(list)); -+ m_loadedKeys.insert(key); -+ qCDebug(lcConfig) << " Loaded" << key << "=" << list; -+ continue; -+ } -+ -+ // For all other types, let Qt's variant conversion handle it -+ prop.write(this, jsonVal.toVariant()); -+ m_loadedKeys.insert(key); -+ qCDebug(lcConfig) << " Loaded" << key << "=" << jsonVal.toVariant(); -+ } -+} -+ -+QJsonObject ConfigObject::toJsonObject() const { -+ QJsonObject obj; -+ const auto* meta = metaObject(); -+ -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ const auto prop = meta->property(i); -+ -+ if (!prop.isReadable()) -+ continue; -+ -+ const auto key = QString::fromUtf8(prop.name()); -+ -+ if (isGlobalOnly(key)) -+ continue; -+ -+ const auto value = prop.read(this); -+ -+ // Recurse into sub-objects — include only if they have loaded keys -+ if (value.canView()) { -+ auto* const subObj = value.value(); -+ if (subObj) { -+ auto subJson = subObj->toJsonObject(); -+ if (!subJson.isEmpty()) -+ obj.insert(key, subJson); -+ } -+ continue; -+ } -+ -+ // Only include properties that were explicitly loaded -+ if (!m_loadedKeys.contains(key)) -+ continue; -+ -+ if (!prop.isWritable()) -+ continue; -+ -+ if (prop.metaType().id() == QMetaType::QStringList) { -+ QJsonArray arr; -+ const auto strList = value.toStringList(); -+ for (const auto& s : strList) -+ arr.append(s); -+ obj.insert(key, arr); -+ continue; -+ } -+ -+ if (prop.metaType().id() == QMetaType::QVariantList) { -+ obj.insert(key, QJsonArray::fromVariantList(value.toList())); -+ continue; -+ } -+ -+ obj.insert(key, QJsonValue::fromVariant(value)); -+ } -+ -+ return obj; -+} -+ -+void ConfigObject::clearLoadedKeys() { -+ m_loadedKeys.clear(); -+ -+ const auto* meta = metaObject(); -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ auto prop = meta->property(i); -+ if (isGlobalOnly(QString::fromUtf8(prop.name()))) -+ continue; -+ auto value = prop.read(this); -+ auto* subObj = value.value(); -+ if (subObj) -+ subObj->clearLoadedKeys(); -+ } -+} -+ -+void ConfigObject::syncFromGlobal(ConfigObject* global) { -+ m_global = global; -+ -+ const auto* meta = metaObject(); -+ qCDebug(lcConfig) << "Syncing" << meta->className() << "from global, loaded keys:" << m_loadedKeys; -+ -+ // Connect batched change signal (single connection per ConfigObject pair) -+ connect(global, &ConfigObject::propertiesChanged, this, &ConfigObject::onGlobalPropertiesChanged); -+ -+ // Initial sync: copy all non-loaded property values from global -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ auto prop = meta->property(i); -+ const auto key = QString::fromUtf8(prop.name()); -+ -+ if (isGlobalOnly(key)) -+ continue; -+ -+ auto current = prop.read(this); -+ auto* subObj = current.value(); -+ -+ if (subObj) { -+ auto globalVal = prop.read(global); -+ auto* globalSub = globalVal.value(); -+ if (globalSub) -+ subObj->syncFromGlobal(globalSub); -+ continue; -+ } -+ -+ if (!prop.isWritable()) -+ continue; -+ -+ if (!m_loadedKeys.contains(key)) { -+ auto val = prop.read(global); -+ prop.write(this, val); -+ m_loadedKeys.remove(key); // setter added it — remove since this is a synced value -+ qCDebug(lcConfig) << " Synced" << key << "=" << val << "from global"; -+ } else { -+ qCDebug(lcConfig) << " Keeping loaded" << key << "=" << prop.read(this); -+ } -+ } -+} -+ -+void ConfigObject::resyncFromGlobal() { -+ if (!m_global) -+ return; -+ -+ const auto* meta = metaObject(); -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ auto prop = meta->property(i); -+ const auto key = QString::fromUtf8(prop.name()); -+ -+ if (isGlobalOnly(key)) -+ continue; -+ -+ auto current = prop.read(this); -+ auto* subObj = current.value(); -+ -+ if (subObj) { -+ subObj->resyncFromGlobal(); -+ continue; -+ } -+ -+ if (!prop.isWritable()) -+ continue; -+ -+ if (!m_loadedKeys.contains(key)) { -+ prop.write(this, prop.read(m_global)); -+ m_loadedKeys.remove(key); // setter added it — remove since this is a synced value -+ } -+ } -+} -+ -+QString ConfigObject::propertyPath(const QString& name) const { -+ QStringList parts; -+ parts.append(name); -+ -+ const QObject* obj = this; -+ while (auto* parentObj = obj->parent()) { -+ auto* parentConfig = qobject_cast(parentObj); -+ if (!parentConfig) -+ break; -+ -+ // Find which property name this child is on the parent -+ const auto* meta = parentConfig->metaObject(); -+ bool found = false; -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ auto prop = meta->property(i); -+ auto val = prop.read(parentObj); -+ if (val.value() == obj) { -+ parts.prepend(QString::fromUtf8(prop.name())); -+ found = true; -+ break; -+ } -+ } -+ -+ if (!found) -+ break; -+ -+ obj = parentObj; -+ } -+ -+ return parts.join(QLatin1Char('.')); -+} -+ -+bool ConfigObject::isPropertyLoaded(const QString& name) const { -+ return m_loadedKeys.contains(name); -+} -+ -+bool ConfigObject::isOverlay() const { -+ return m_global != nullptr; -+} -+ -+bool ConfigObject::isGlobalOnly(const QString& name) const { -+ return isOverlay() && m_globalOnlyKeys.contains(name); -+} -+ -+void ConfigObject::markPropertyLoaded(const QString& name) { -+ m_loadedKeys.insert(name); -+} -+ -+void ConfigObject::resetOption(const QString& name) { -+ m_loadedKeys.remove(name); -+ -+ // If synced from global, re-copy the global value -+ if (m_global) { -+ int idx = metaObject()->indexOfProperty(name.toUtf8().constData()); -+ if (idx >= 0) { -+ auto prop = metaObject()->property(idx); -+ if (prop.isWritable()) -+ prop.write(this, prop.read(m_global)); -+ } -+ } -+} -+ -+void ConfigObject::onGlobalPropertiesChanged(const QMap& changed) { -+ for (auto it = changed.begin(); it != changed.end(); ++it) { -+ if (m_loadedKeys.contains(it.key()) || isGlobalOnly(it.key())) -+ continue; -+ -+ int idx = metaObject()->indexOfProperty(it.key().toUtf8().constData()); -+ if (idx >= 0) { -+ metaObject()->property(idx).write(this, it.value()); -+ m_loadedKeys.remove(it.key()); // setter added it — remove since this is a synced value -+ qCDebug(lcConfig) << metaObject()->className() << "synced" << it.key() << "=" << it.value() -+ << "from global change"; -+ } -+ } -+} -+ -+void ConfigObject::markGlobalOnly(const QString& name) { -+ m_globalOnlyKeys.insert(name); -+} -+ -+void ConfigObject::notifyPropertyChanged(const QString& name, const QVariant& value) { -+ m_pendingChanges.insert(name, value); -+ -+ if (!m_batchTimer) { -+ m_batchTimer = new QTimer(this); -+ m_batchTimer->setSingleShot(true); -+ m_batchTimer->setInterval(0); -+ connect(m_batchTimer, &QTimer::timeout, this, &ConfigObject::emitBatchedChanges); -+ } -+ -+ m_batchTimer->start(); -+} -+ -+void ConfigObject::emitBatchedChanges() { -+ if (m_pendingChanges.isEmpty()) -+ return; -+ -+ auto changes = std::move(m_pendingChanges); -+ m_pendingChanges.clear(); -+ emit propertiesChanged(changes); -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/configobject.hpp b/plugin/src/Caelestia/Config/configobject.hpp -new file mode 100644 -index 00000000..c4f4428b ---- /dev/null -+++ b/plugin/src/Caelestia/Config/configobject.hpp -@@ -0,0 +1,143 @@ -+#pragma once -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+inline QVariantMap vmap(std::initializer_list> entries) { -+ QVariantMap map; -+ for (const auto& [key, value] : entries) -+ map.insert(std::move(key), std::move(value)); -+ return map; -+} -+ -+} // namespace caelestia::config -+ -+// Declares a serialized config property with getter, setter (change-detected), signal, and member. -+#define CONFIG_PROPERTY(Type, name, ...) \ -+ Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ -+ \ -+public: \ -+ [[nodiscard]] Type name() const { \ -+ return m_##name; \ -+ } \ -+ void set_##name(const Type& val) { \ -+ if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ -+ markPropertyLoaded(QStringLiteral(#name)); \ -+ Q_EMIT name##Changed(); \ -+ notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ -+ } \ -+ } \ -+ Q_SIGNAL void name##Changed(); \ -+ \ -+private: \ -+ Type m_##name __VA_OPT__(= __VA_ARGS__); -+ -+// Declares a CONSTANT sub-object property. Initialize the member in the constructor. -+#define CONFIG_SUBOBJECT(Type, name) \ -+ Q_PROPERTY(caelestia::config::Type* name READ name CONSTANT) \ -+ \ -+public: \ -+ [[nodiscard]] Type* name() const { \ -+ return m_##name; \ -+ } \ -+ \ -+private: \ -+ Type* m_##name = nullptr; -+ -+// Like CONFIG_PROPERTY but warns on read/write when accessed on a per-monitor overlay. -+#define CONFIG_GLOBAL_PROPERTY(Type, name, ...) \ -+ Q_PROPERTY(Type name READ name WRITE set_##name NOTIFY name##Changed) \ -+ \ -+public: \ -+ [[nodiscard]] Type name() const { \ -+ if (isOverlay()) \ -+ qCWarning(caelestia::config::lcConfig, "Reading global-only option '%s' on per-monitor overlay", \ -+ qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ -+ return m_##name; \ -+ } \ -+ void set_##name(const Type& val) { \ -+ if (isOverlay()) \ -+ qCWarning(caelestia::config::lcConfig, "Writing global-only option '%s' on per-monitor overlay", \ -+ qUtf8Printable(propertyPath(QStringLiteral(#name)))); \ -+ if (caelestia::config::ConfigObject::updateMember(m_##name, val)) { \ -+ markPropertyLoaded(QStringLiteral(#name)); \ -+ Q_EMIT name##Changed(); \ -+ notifyPropertyChanged(QStringLiteral(#name), QVariant::fromValue(m_##name)); \ -+ } \ -+ } \ -+ Q_SIGNAL void name##Changed(); \ -+ \ -+private: \ -+ Type m_##name __VA_OPT__(= __VA_ARGS__); \ -+ const bool m_##name##_go = [this] { \ -+ markGlobalOnly(QStringLiteral(#name)); \ -+ return true; \ -+ }(); -+ -+namespace caelestia::config { -+ -+Q_DECLARE_LOGGING_CATEGORY(lcConfig) -+ -+class ConfigObject : public QObject { -+ Q_OBJECT -+ -+public: -+ explicit ConfigObject(QObject* parent = nullptr); -+ -+ void loadFromJson(const QJsonObject& obj); -+ [[nodiscard]] QJsonObject toJsonObject() const; -+ -+ // Per-monitor overlay support (Qt Resolve Mask pattern). -+ void syncFromGlobal(ConfigObject* global); -+ void resyncFromGlobal(); -+ void clearLoadedKeys(); -+ -+ [[nodiscard]] bool isPropertyLoaded(const QString& name) const; -+ [[nodiscard]] QString propertyPath(const QString& name) const; -+ [[nodiscard]] bool isOverlay() const; -+ // Returns true only on overlays — global singleton always returns false. -+ [[nodiscard]] bool isGlobalOnly(const QString& name) const; -+ -+ Q_INVOKABLE void resetOption(const QString& name); -+ -+ template static bool updateMember(T& member, const T& value) { -+ if constexpr (std::is_floating_point_v) { -+ if (qFuzzyCompare(member + 1.0, value + 1.0)) -+ return false; -+ } else { -+ if (member == value) -+ return false; -+ } -+ member = value; -+ return true; -+ } -+ -+signals: -+ void propertiesChanged(const QMap& changed); -+ -+protected: -+ void markPropertyLoaded(const QString& name); -+ void markGlobalOnly(const QString& name); -+ void notifyPropertyChanged(const QString& name, const QVariant& value); -+ -+private: -+ void onGlobalPropertiesChanged(const QMap& changed); -+ void emitBatchedChanges(); -+ -+ // Per-monitor overlay state -+ ConfigObject* m_global = nullptr; -+ QSet m_loadedKeys; -+ QSet m_globalOnlyKeys; -+ QMap m_pendingChanges; -+ QTimer* m_batchTimer = nullptr; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/controlcenterconfig.hpp b/plugin/src/Caelestia/Config/controlcenterconfig.hpp -new file mode 100644 -index 00000000..80b40b87 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/controlcenterconfig.hpp -@@ -0,0 +1,18 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+namespace caelestia::config { -+ -+// ControlCenterConfig has no serialized properties (serializer returns {}) -+// All properties are in AdvancedConfig.controlCenter -+class ControlCenterConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+public: -+ explicit ControlCenterConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/dashboardconfig.hpp b/plugin/src/Caelestia/Config/dashboardconfig.hpp -new file mode 100644 -index 00000000..cb872d1d ---- /dev/null -+++ b/plugin/src/Caelestia/Config/dashboardconfig.hpp -@@ -0,0 +1,44 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+namespace caelestia::config { -+ -+class DashboardPerformance : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, showBattery, true) -+ CONFIG_PROPERTY(bool, showGpu, true) -+ CONFIG_PROPERTY(bool, showCpu, true) -+ CONFIG_PROPERTY(bool, showMemory, true) -+ CONFIG_PROPERTY(bool, showStorage, true) -+ CONFIG_PROPERTY(bool, showNetwork, true) -+ -+public: -+ explicit DashboardPerformance(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class DashboardConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(bool, showOnHover, true) -+ CONFIG_PROPERTY(bool, showDashboard, true) -+ CONFIG_PROPERTY(bool, showMedia, true) -+ CONFIG_PROPERTY(bool, showPerformance, true) -+ CONFIG_PROPERTY(bool, showWeather, true) -+ CONFIG_GLOBAL_PROPERTY(int, mediaUpdateInterval, 500) -+ CONFIG_GLOBAL_PROPERTY(int, resourceUpdateInterval, 1000) -+ CONFIG_PROPERTY(int, dragThreshold, 50) -+ CONFIG_SUBOBJECT(DashboardPerformance, performance) -+ -+public: -+ explicit DashboardConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_performance(new DashboardPerformance(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/generalconfig.hpp b/plugin/src/Caelestia/Config/generalconfig.hpp -new file mode 100644 -index 00000000..859302fe ---- /dev/null -+++ b/plugin/src/Caelestia/Config/generalconfig.hpp -@@ -0,0 +1,108 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+using Qt::StringLiterals::operator""_s; -+ -+class GeneralApps : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(QStringList, terminal, { u"foot"_s }) -+ CONFIG_GLOBAL_PROPERTY(QStringList, audio, { u"pavucontrol"_s }) -+ CONFIG_GLOBAL_PROPERTY(QStringList, playback, { u"mpv"_s }) -+ CONFIG_GLOBAL_PROPERTY(QStringList, explorer, { u"thunar"_s }) -+ -+public: -+ explicit GeneralApps(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class GeneralIdle : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(bool, lockBeforeSleep, true) -+ CONFIG_GLOBAL_PROPERTY(bool, inhibitWhenAudio, true) -+ CONFIG_GLOBAL_PROPERTY(QVariantList, timeouts, -+ { -+ vmap({ -+ { u"timeout"_s, 180 }, -+ { u"idleAction"_s, u"lock"_s }, -+ }), -+ vmap({ -+ { u"timeout"_s, 300 }, -+ { u"idleAction"_s, u"dpms off"_s }, -+ { u"returnAction"_s, u"dpms on"_s }, -+ }), -+ vmap({ -+ { u"timeout"_s, 600 }, -+ { u"idleAction"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, -+ }), -+ }) -+ -+public: -+ explicit GeneralIdle(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class GeneralBattery : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(QVariantList, warnLevels, -+ { -+ vmap({ -+ { u"level"_s, 20 }, -+ { u"title"_s, u"Low battery"_s }, -+ { u"message"_s, u"You might want to plug in a charger"_s }, -+ { u"icon"_s, u"battery_android_frame_2"_s }, -+ }), -+ vmap({ -+ { u"level"_s, 10 }, -+ { u"title"_s, u"Did you see the previous message?"_s }, -+ { u"message"_s, u"You should probably plug in a charger now"_s }, -+ { u"icon"_s, u"battery_android_frame_1"_s }, -+ }), -+ vmap({ -+ { u"level"_s, 5 }, -+ { u"title"_s, u"Critical battery level"_s }, -+ { u"message"_s, u"PLUG THE CHARGER RIGHT NOW!!"_s }, -+ { u"icon"_s, u"battery_android_alert"_s }, -+ { u"critical"_s, true }, -+ }), -+ }) -+ CONFIG_GLOBAL_PROPERTY(int, criticalLevel, 3) -+ -+public: -+ explicit GeneralBattery(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class GeneralConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(QString, logo) -+ CONFIG_PROPERTY(bool, showOverFullscreen, false) -+ CONFIG_PROPERTY(qreal, mediaGifSpeedAdjustment, 300) -+ CONFIG_PROPERTY(qreal, sessionGifSpeed, 0.7) -+ CONFIG_SUBOBJECT(GeneralApps, apps) -+ CONFIG_SUBOBJECT(GeneralIdle, idle) -+ CONFIG_SUBOBJECT(GeneralBattery, battery) -+ -+public: -+ explicit GeneralConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_apps(new GeneralApps(this)) -+ , m_idle(new GeneralIdle(this)) -+ , m_battery(new GeneralBattery(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/launcherconfig.hpp b/plugin/src/Caelestia/Config/launcherconfig.hpp -new file mode 100644 -index 00000000..049e213a ---- /dev/null -+++ b/plugin/src/Caelestia/Config/launcherconfig.hpp -@@ -0,0 +1,135 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+using Qt::StringLiterals::operator""_s; -+ -+class LauncherUseFuzzy : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(bool, apps, false) -+ CONFIG_GLOBAL_PROPERTY(bool, actions, false) -+ CONFIG_GLOBAL_PROPERTY(bool, schemes, false) -+ CONFIG_GLOBAL_PROPERTY(bool, variants, false) -+ CONFIG_GLOBAL_PROPERTY(bool, wallpapers, false) -+ -+public: -+ explicit LauncherUseFuzzy(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class LauncherConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(bool, showOnHover, false) -+ CONFIG_PROPERTY(int, maxShown, 7) -+ CONFIG_PROPERTY(int, maxWallpapers, 9) -+ CONFIG_GLOBAL_PROPERTY(QString, specialPrefix, u"@"_s) -+ CONFIG_GLOBAL_PROPERTY(QString, actionPrefix, u">"_s) -+ CONFIG_GLOBAL_PROPERTY(bool, enableDangerousActions, false) -+ CONFIG_PROPERTY(int, dragThreshold, 50) -+ CONFIG_GLOBAL_PROPERTY(bool, vimKeybinds, false) -+ CONFIG_GLOBAL_PROPERTY(QStringList, favouriteApps) -+ CONFIG_GLOBAL_PROPERTY(QStringList, hiddenApps) -+ CONFIG_SUBOBJECT(LauncherUseFuzzy, useFuzzy) -+ CONFIG_GLOBAL_PROPERTY(QVariantList, actions, -+ { -+ vmap({ -+ { u"name"_s, u"Calculator"_s }, -+ { u"icon"_s, u"calculate"_s }, -+ { u"description"_s, u"Do simple math equations (powered by Qalc)"_s }, -+ { u"command"_s, QStringList{ u"autocomplete"_s, u"calc"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Scheme"_s }, -+ { u"icon"_s, u"palette"_s }, -+ { u"description"_s, u"Change the current colour scheme"_s }, -+ { u"command"_s, QStringList{ u"autocomplete"_s, u"scheme"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Wallpaper"_s }, -+ { u"icon"_s, u"image"_s }, -+ { u"description"_s, u"Change the current wallpaper"_s }, -+ { u"command"_s, QStringList{ u"autocomplete"_s, u"wallpaper"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Variant"_s }, -+ { u"icon"_s, u"colors"_s }, -+ { u"description"_s, u"Change the current scheme variant"_s }, -+ { u"command"_s, QStringList{ u"autocomplete"_s, u"variant"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Random"_s }, -+ { u"icon"_s, u"casino"_s }, -+ { u"description"_s, u"Switch to a random wallpaper"_s }, -+ { u"command"_s, QStringList{ u"caelestia"_s, u"wallpaper"_s, u"-r"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Light"_s }, -+ { u"icon"_s, u"light_mode"_s }, -+ { u"description"_s, u"Change the scheme to light mode"_s }, -+ { u"command"_s, QStringList{ u"setMode"_s, u"light"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Dark"_s }, -+ { u"icon"_s, u"dark_mode"_s }, -+ { u"description"_s, u"Change the scheme to dark mode"_s }, -+ { u"command"_s, QStringList{ u"setMode"_s, u"dark"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Shutdown"_s }, -+ { u"icon"_s, u"power_settings_new"_s }, -+ { u"description"_s, u"Shutdown the system"_s }, -+ { u"command"_s, QStringList{ u"systemctl"_s, u"poweroff"_s } }, -+ { u"dangerous"_s, true }, -+ }), -+ vmap({ -+ { u"name"_s, u"Reboot"_s }, -+ { u"icon"_s, u"cached"_s }, -+ { u"description"_s, u"Reboot the system"_s }, -+ { u"command"_s, QStringList{ u"systemctl"_s, u"reboot"_s } }, -+ { u"dangerous"_s, true }, -+ }), -+ vmap({ -+ { u"name"_s, u"Logout"_s }, -+ { u"icon"_s, u"exit_to_app"_s }, -+ { u"description"_s, u"Log out of the current session"_s }, -+ { u"command"_s, QStringList{ u"loginctl"_s, u"terminate-user"_s, u""_s } }, -+ { u"dangerous"_s, true }, -+ }), -+ vmap({ -+ { u"name"_s, u"Lock"_s }, -+ { u"icon"_s, u"lock"_s }, -+ { u"description"_s, u"Lock the current session"_s }, -+ { u"command"_s, QStringList{ u"loginctl"_s, u"lock-session"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Sleep"_s }, -+ { u"icon"_s, u"bedtime"_s }, -+ { u"description"_s, u"Suspend then hibernate"_s }, -+ { u"command"_s, QStringList{ u"systemctl"_s, u"suspend-then-hibernate"_s } }, -+ }), -+ vmap({ -+ { u"name"_s, u"Settings"_s }, -+ { u"icon"_s, u"settings"_s }, -+ { u"description"_s, u"Configure the shell"_s }, -+ { u"command"_s, QStringList{ u"caelestia"_s, u"shell"_s, u"controlCenter"_s, u"open"_s } }, -+ }), -+ }) -+ -+public: -+ explicit LauncherConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_useFuzzy(new LauncherUseFuzzy(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/lockconfig.hpp b/plugin/src/Caelestia/Config/lockconfig.hpp -new file mode 100644 -index 00000000..0d8aa6ba ---- /dev/null -+++ b/plugin/src/Caelestia/Config/lockconfig.hpp -@@ -0,0 +1,21 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+namespace caelestia::config { -+ -+class LockConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, recolourLogo, false) -+ CONFIG_GLOBAL_PROPERTY(bool, enableFprint, true) -+ CONFIG_GLOBAL_PROPERTY(int, maxFprintTries, 3) -+ CONFIG_PROPERTY(bool, hideNotifs, false) -+ -+public: -+ explicit LockConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.cpp b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp -new file mode 100644 -index 00000000..fb1745ec ---- /dev/null -+++ b/plugin/src/Caelestia/Config/monitorconfigmanager.cpp -@@ -0,0 +1,64 @@ -+#include "monitorconfigmanager.hpp" -+#include "config.hpp" -+#include "tokens.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+namespace { -+ -+QString monitorConfigDir(const QString& screen) { -+ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + -+ QStringLiteral("/caelestia/monitors/") + screen + QStringLiteral("/"); -+} -+ -+} // namespace -+ -+MonitorConfigManager::MonitorConfigManager(QObject* parent) -+ : QObject(parent) {} -+ -+MonitorConfigManager* MonitorConfigManager::instance() { -+ static MonitorConfigManager instance; -+ return &instance; -+} -+ -+MonitorConfigManager* MonitorConfigManager::create(QQmlEngine*, QJSEngine*) { -+ QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); -+ return instance(); -+} -+ -+GlobalConfig* MonitorConfigManager::configForScreen(const QString& screen) { -+ auto& overlay = m_overlays[screen]; -+ if (!overlay.config) { -+ auto dir = monitorConfigDir(screen); -+ overlay.config = new GlobalConfig(GlobalConfig::instance(), dir + QStringLiteral("shell.json"), screen, this); -+ -+ auto* const global = GlobalConfig::instance(); -+ connect(overlay.config, &GlobalConfig::loaded, global, &GlobalConfig::loaded); -+ connect(overlay.config, &GlobalConfig::saved, global, &GlobalConfig::saved); -+ connect(overlay.config, &GlobalConfig::loadFailed, global, &GlobalConfig::loadFailed); -+ connect(overlay.config, &GlobalConfig::saveFailed, global, &GlobalConfig::saveFailed); -+ connect(overlay.config, &GlobalConfig::unknownOption, global, &GlobalConfig::unknownOption); -+ } -+ return overlay.config; -+} -+ -+TokenConfig* MonitorConfigManager::tokensForScreen(const QString& screen) { -+ auto& overlay = m_overlays[screen]; -+ if (!overlay.tokens) { -+ auto dir = monitorConfigDir(screen); -+ overlay.tokens = -+ new TokenConfig(TokenConfig::instance(), dir + QStringLiteral("shell-tokens.json"), screen, this); -+ -+ auto* const global = TokenConfig::instance(); -+ connect(overlay.tokens, &TokenConfig::loaded, global, &TokenConfig::loaded); -+ connect(overlay.tokens, &TokenConfig::saved, global, &TokenConfig::saved); -+ connect(overlay.tokens, &TokenConfig::loadFailed, global, &TokenConfig::loadFailed); -+ connect(overlay.tokens, &TokenConfig::saveFailed, global, &TokenConfig::saveFailed); -+ connect(overlay.tokens, &TokenConfig::unknownOption, global, &TokenConfig::unknownOption); -+ } -+ return overlay.tokens; -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/monitorconfigmanager.hpp b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp -new file mode 100644 -index 00000000..70bbdc0d ---- /dev/null -+++ b/plugin/src/Caelestia/Config/monitorconfigmanager.hpp -@@ -0,0 +1,35 @@ -+#pragma once -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+class GlobalConfig; -+class TokenConfig; -+ -+class MonitorConfigManager : public QObject { -+ Q_OBJECT -+ QML_ELEMENT -+ QML_SINGLETON -+ -+public: -+ static MonitorConfigManager* instance(); -+ static MonitorConfigManager* create(QQmlEngine*, QJSEngine*); -+ -+ [[nodiscard]] Q_INVOKABLE GlobalConfig* configForScreen(const QString& screen); -+ [[nodiscard]] Q_INVOKABLE TokenConfig* tokensForScreen(const QString& screen); -+ -+private: -+ explicit MonitorConfigManager(QObject* parent = nullptr); -+ -+ struct ScreenOverlay { -+ GlobalConfig* config = nullptr; -+ TokenConfig* tokens = nullptr; -+ }; -+ -+ QHash m_overlays; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/notifsconfig.hpp b/plugin/src/Caelestia/Config/notifsconfig.hpp -new file mode 100644 -index 00000000..cdac94bd ---- /dev/null -+++ b/plugin/src/Caelestia/Config/notifsconfig.hpp -@@ -0,0 +1,28 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+class NotifsConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(bool, expire, true) -+ CONFIG_GLOBAL_PROPERTY(QString, fullscreen, QStringLiteral("on")) -+ CONFIG_GLOBAL_PROPERTY(int, defaultExpireTimeout, 5000) -+ CONFIG_GLOBAL_PROPERTY(int, fullscreenExpireTimeout, 2000) -+ CONFIG_PROPERTY(qreal, clearThreshold, 0.3) -+ CONFIG_PROPERTY(int, expandThreshold, 20) -+ CONFIG_GLOBAL_PROPERTY(bool, actionOnClick, false) -+ CONFIG_PROPERTY(int, groupPreviewNum, 3) -+ CONFIG_PROPERTY(bool, openExpanded, false) -+ -+public: -+ explicit NotifsConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/osdconfig.hpp b/plugin/src/Caelestia/Config/osdconfig.hpp -new file mode 100644 -index 00000000..18770294 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/osdconfig.hpp -@@ -0,0 +1,21 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+namespace caelestia::config { -+ -+class OsdConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(int, hideDelay, 2000) -+ CONFIG_PROPERTY(bool, enableBrightness, true) -+ CONFIG_PROPERTY(bool, enableMicrophone, false) -+ -+public: -+ explicit OsdConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/rootconfig.cpp b/plugin/src/Caelestia/Config/rootconfig.cpp -new file mode 100644 -index 00000000..9e976ab2 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/rootconfig.cpp -@@ -0,0 +1,258 @@ -+#include "rootconfig.hpp" -+ -+#include -+#include -+#include -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+namespace { -+ -+QString watchRoot() { -+ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation); -+} -+ -+} // namespace -+ -+RootConfig::RootConfig(QObject* parent) -+ : ConfigObject(parent) {} -+ -+bool RootConfig::recentlySaved() const { -+ return m_recentlySaved; -+} -+ -+QStringList RootConfig::collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json) { -+ QStringList unknown; -+ const auto* meta = obj->metaObject(); -+ -+ QSet known; -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) -+ known.insert(QString::fromUtf8(meta->property(i).name())); -+ -+ for (auto it = json.begin(); it != json.end(); ++it) { -+ if (!known.contains(it.key())) { -+ unknown.append(it.key()); -+ } else if (it.value().isObject()) { -+ int idx = meta->indexOfProperty(it.key().toUtf8().constData()); -+ if (idx >= 0) { -+ auto prop = meta->property(idx); -+ auto value = prop.read(obj); -+ auto* subObj = value.value(); -+ if (subObj) { -+ const auto subUnknown = collectUnknownKeys(subObj, it.value().toObject()); -+ for (const auto& subKey : subUnknown) -+ unknown.append(it.key() + QStringLiteral(".") + subKey); -+ } -+ } -+ } -+ } -+ -+ return unknown; -+} -+ -+void RootConfig::setupFileBackend(const QString& path, const QString& screen) { -+ m_filePath = path; -+ m_screen = screen; -+ -+ m_watcher = new QFileSystemWatcher(this); -+ m_saveTimer = new QTimer(this); -+ m_cooldownTimer = new QTimer(this); -+ m_retryTimer = new QTimer(this); -+ -+ m_retryTimer->setSingleShot(true); -+ m_retryTimer->setInterval(50); -+ connect(m_retryTimer, &QTimer::timeout, this, &RootConfig::reload); -+ -+ m_saveTimer->setSingleShot(true); -+ m_saveTimer->setInterval(500); -+ connect(m_saveTimer, &QTimer::timeout, this, [this] { -+ QDir().mkpath(QFileInfo(m_filePath).absolutePath()); -+ -+ QFile file(m_filePath); -+ if (!file.open(QIODevice::WriteOnly)) { -+ auto err = QStringLiteral("Failed to write %1: %2").arg(m_filePath, file.errorString()); -+ qCWarning(lcConfig, "%s", qUtf8Printable(err)); -+ emit saveFailed(err, m_screen); -+ return; -+ } -+ -+ auto json = toJsonObject(); -+ file.write(QJsonDocument(json).toJson(QJsonDocument::Indented)); -+ file.close(); -+ -+ // Update watches — save may have created directories -+ updateWatch(); -+ -+ emit saved(m_screen); -+ }); -+ -+ m_cooldownTimer->setSingleShot(true); -+ m_cooldownTimer->setInterval(2000); -+ connect(m_cooldownTimer, &QTimer::timeout, this, [this] { -+ m_recentlySaved = false; -+ }); -+ -+ m_reloadDebounce = new QTimer(this); -+ m_reloadDebounce->setSingleShot(true); -+ m_reloadDebounce->setInterval(50); -+ connect(m_reloadDebounce, &QTimer::timeout, this, &RootConfig::reload); -+ -+ // Auto-save when any property changes (debounced by the save timer) -+ connectAutoSave(this); -+ -+ connect(m_watcher, &QFileSystemWatcher::directoryChanged, this, &RootConfig::onWatcherEvent); -+ connect(m_watcher, &QFileSystemWatcher::fileChanged, this, &RootConfig::onWatcherEvent); -+ -+ qCDebug(lcConfig) << "Setting up file backend for" << metaObject()->className() << "at" << path; -+ -+ updateWatch(); -+ -+ // Load immediately so values are available during construction. -+ // Defer signal emissions to next event loop tick so QML has time to connect. -+ auto result = reloadFromFile(); -+ QTimer::singleShot(0, this, [this, result] { -+ emitLoadSignals(result, false); -+ }); -+} -+ -+void RootConfig::connectAutoSave(ConfigObject* obj) { -+ connect(obj, &ConfigObject::propertiesChanged, this, [this] { -+ if (!m_loading) -+ saveToFile(); -+ }); -+ -+ // Recurse into sub-objects -+ const auto* meta = obj->metaObject(); -+ for (int i = meta->propertyOffset(); i < meta->propertyCount(); ++i) { -+ auto prop = meta->property(i); -+ auto value = prop.read(obj); -+ auto* subObj = value.value(); -+ if (subObj) -+ connectAutoSave(subObj); -+ } -+} -+ -+void RootConfig::updateWatch() { -+ auto targetDir = QFileInfo(m_filePath).absolutePath(); -+ -+ // Find the nearest existing directory, walking up toward the watch root -+ auto dir = targetDir; -+ while (!QFile::exists(dir) && dir != watchRoot() && !dir.isEmpty()) { -+ auto parent = QFileInfo(dir).absolutePath(); -+ if (parent == dir) -+ break; // reached filesystem root -+ dir = parent; -+ } -+ -+ // Update directory watch if it changed -+ if (dir != m_watchedDir) { -+ if (!m_watchedDir.isEmpty()) -+ m_watcher->removePath(m_watchedDir); -+ -+ m_watchedDir = dir; -+ -+ if (QFile::exists(dir)) -+ m_watcher->addPath(dir); -+ } -+ -+ // Watch the file itself if it exists (for in-place modifications) -+ if (QFile::exists(m_filePath)) { -+ if (!m_watcher->files().contains(m_filePath)) -+ m_watcher->addPath(m_filePath); -+ } -+} -+ -+void RootConfig::onWatcherEvent() { -+ // Re-evaluate what to watch — directories may have been created or deleted -+ updateWatch(); -+ -+ if (!m_recentlySaved) -+ m_reloadDebounce->start(); -+} -+ -+void RootConfig::saveToFile() { -+ if (!m_saveTimer) -+ return; -+ m_saveTimer->start(); -+ m_recentlySaved = true; -+ m_cooldownTimer->start(); -+} -+ -+std::optional RootConfig::reloadFromFile() { -+ QFile file(m_filePath); -+ -+ if (!file.exists()) { -+ qCDebug(lcConfig) << "File does not exist:" << m_filePath; -+ return std::nullopt; -+ } -+ -+ if (!file.open(QIODevice::ReadOnly)) { -+ auto err = QStringLiteral("Failed to open %1: %2").arg(m_filePath, file.errorString()); -+ qCDebug(lcConfig, "%s", qUtf8Printable(err)); -+ return err; -+ } -+ -+ QJsonParseError error{}; -+ auto doc = QJsonDocument::fromJson(file.readAll(), &error); -+ -+ if (error.error != QJsonParseError::NoError) { -+ if (m_retryTimer && m_parseRetries < 3) { -+ m_parseRetries++; -+ qCDebug(lcConfig, "Failed to parse %s: %s - retrying (%d/3)", qUtf8Printable(m_filePath), -+ qUtf8Printable(error.errorString()), m_parseRetries); -+ m_retryTimer->start(); -+ return std::nullopt; // pending retry — no signal -+ } -+ -+ qCWarning(lcConfig, "Failed to parse %s: %s", qUtf8Printable(m_filePath), qUtf8Printable(error.errorString())); -+ m_parseRetries = 0; -+ return QStringLiteral("JSON parse error: %1").arg(error.errorString()); -+ } -+ -+ m_parseRetries = 0; -+ -+ qCDebug(lcConfig) << "Reloading" << metaObject()->className() << "from" << m_filePath; -+ -+ m_loading = true; -+ -+ clearLoadedKeys(); -+ -+ auto jsonObj = doc.object(); -+ loadFromJson(jsonObj); -+ -+ m_loading = false; -+ -+ // Collect unknown keys — caller is responsible for emitting signals -+ m_lastUnknownKeys = collectUnknownKeys(this, jsonObj); -+ -+ return QString(); // success -+} -+ -+void RootConfig::save() { -+ saveToFile(); -+} -+ -+void RootConfig::emitLoadSignals(const std::optional& result, bool emitLoaded) { -+ if (!result.has_value()) -+ return; -+ -+ for (const auto& key : std::as_const(m_lastUnknownKeys)) -+ emit unknownOption(key, m_screen); -+ m_lastUnknownKeys.clear(); -+ -+ if (result->isEmpty()) { -+ if (emitLoaded) -+ emit loaded(m_screen); -+ } else { -+ emit loadFailed(*result, m_screen); -+ } -+} -+ -+void RootConfig::reload() { -+ emitLoadSignals(reloadFromFile()); -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/rootconfig.hpp b/plugin/src/Caelestia/Config/rootconfig.hpp -new file mode 100644 -index 00000000..bf9b8e2c ---- /dev/null -+++ b/plugin/src/Caelestia/Config/rootconfig.hpp -@@ -0,0 +1,59 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+// Intermediate base for singleton config roots (GlobalConfig, TokenConfig). -+// Provides file-backed persistence, save/reload, and lifecycle signals. -+class RootConfig : public ConfigObject { -+ Q_OBJECT -+ -+public: -+ explicit RootConfig(QObject* parent = nullptr); -+ -+ void setupFileBackend(const QString& path, const QString& screen = {}); -+ void saveToFile(); -+ // Returns nullopt if retrying, empty string on success, error message on failure. -+ [[nodiscard]] std::optional reloadFromFile(); -+ -+ [[nodiscard]] bool recentlySaved() const; -+ -+ Q_INVOKABLE void save(); -+ Q_INVOKABLE void reload(); -+ -+signals: -+ void loaded(const QString& screen); -+ void loadFailed(const QString& error, const QString& screen); -+ void saved(const QString& screen); -+ void saveFailed(const QString& error, const QString& screen); -+ void unknownOption(const QString& key, const QString& screen); -+ -+private: -+ static QStringList collectUnknownKeys(const ConfigObject* obj, const QJsonObject& json); -+ void emitLoadSignals(const std::optional& result, bool emitLoaded = true); -+ void updateWatch(); -+ void onWatcherEvent(); -+ -+ void connectAutoSave(ConfigObject* obj); -+ -+ QString m_filePath; -+ QString m_screen; -+ QString m_watchedDir; -+ bool m_recentlySaved = false; -+ bool m_loading = false; -+ -+ QFileSystemWatcher* m_watcher = nullptr; -+ QTimer* m_saveTimer = nullptr; -+ QTimer* m_cooldownTimer = nullptr; -+ QTimer* m_retryTimer = nullptr; -+ QTimer* m_reloadDebounce = nullptr; -+ int m_parseRetries = 0; -+ QStringList m_lastUnknownKeys; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/serviceconfig.hpp b/plugin/src/Caelestia/Config/serviceconfig.hpp -new file mode 100644 -index 00000000..b97c69c1 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/serviceconfig.hpp -@@ -0,0 +1,43 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+ -+namespace caelestia::config { -+ -+using Qt::StringLiterals::operator""_s; -+ -+class ServiceConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(QString, weatherLocation) -+ // Guess based on locale -+ CONFIG_GLOBAL_PROPERTY(bool, useFahrenheit, -+ QLocale().measurementSystem() == QLocale::ImperialUSSystem || -+ QLocale().measurementSystem() == QLocale::ImperialUKSystem) -+ // This is always false by default cause apparently even imperial system users don't use it for perf temps? -+ CONFIG_GLOBAL_PROPERTY(bool, useFahrenheitPerformance, false) -+ // Attempt to guess based on locale -+ CONFIG_GLOBAL_PROPERTY( -+ bool, useTwelveHourClock, QLocale().timeFormat(QLocale::ShortFormat).toLower().contains(u"a"_s)) -+ CONFIG_GLOBAL_PROPERTY(QString, gpuType) -+ CONFIG_GLOBAL_PROPERTY(int, visualiserBars, 45) -+ CONFIG_GLOBAL_PROPERTY(qreal, audioIncrement, 0.1) -+ CONFIG_GLOBAL_PROPERTY(qreal, brightnessIncrement, 0.1) -+ CONFIG_GLOBAL_PROPERTY(qreal, maxVolume, 1.0) -+ CONFIG_GLOBAL_PROPERTY(bool, smartScheme, true) -+ CONFIG_GLOBAL_PROPERTY(QString, defaultPlayer, u"Spotify"_s) -+ CONFIG_GLOBAL_PROPERTY(QVariantList, playerAliases, -+ { vmap({ { u"from"_s, u"com.github.th_ch.youtube_music"_s }, { u"to"_s, u"YT Music"_s } }) }) -+ CONFIG_GLOBAL_PROPERTY(bool, showLyrics, false) -+ CONFIG_GLOBAL_PROPERTY(QString, lyricsBackend, u"Auto"_s) -+ -+public: -+ explicit ServiceConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/sessionconfig.hpp b/plugin/src/Caelestia/Config/sessionconfig.hpp -new file mode 100644 -index 00000000..7e7548f1 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/sessionconfig.hpp -@@ -0,0 +1,57 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+ -+namespace caelestia::config { -+ -+using Qt::StringLiterals::operator""_s; -+ -+class SessionIcons : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(QString, logout, u"logout"_s) -+ CONFIG_PROPERTY(QString, shutdown, u"power_settings_new"_s) -+ CONFIG_PROPERTY(QString, hibernate, u"downloading"_s) -+ CONFIG_PROPERTY(QString, reboot, u"cached"_s) -+ -+public: -+ explicit SessionIcons(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class SessionCommands : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(QStringList, logout, { u"loginctl"_s, u"terminate-user"_s, u""_s }) -+ CONFIG_PROPERTY(QStringList, shutdown, { u"systemctl"_s, u"poweroff"_s }) -+ CONFIG_PROPERTY(QStringList, hibernate, { u"systemctl"_s, u"hibernate"_s }) -+ CONFIG_PROPERTY(QStringList, reboot, { u"systemctl"_s, u"reboot"_s }) -+ -+public: -+ explicit SessionCommands(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class SessionConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(int, dragThreshold, 30) -+ CONFIG_PROPERTY(bool, vimKeybinds, false) -+ CONFIG_SUBOBJECT(SessionIcons, icons) -+ CONFIG_SUBOBJECT(SessionCommands, commands) -+ -+public: -+ explicit SessionConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_icons(new SessionIcons(this)) -+ , m_commands(new SessionCommands(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/sidebarconfig.hpp b/plugin/src/Caelestia/Config/sidebarconfig.hpp -new file mode 100644 -index 00000000..4460872e ---- /dev/null -+++ b/plugin/src/Caelestia/Config/sidebarconfig.hpp -@@ -0,0 +1,19 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+namespace caelestia::config { -+ -+class SidebarConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(int, dragThreshold, 80) -+ -+public: -+ explicit SidebarConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/tokens.cpp b/plugin/src/Caelestia/Config/tokens.cpp -new file mode 100644 -index 00000000..6d171cb3 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/tokens.cpp -@@ -0,0 +1,54 @@ -+#include "tokens.hpp" -+#include "monitorconfigmanager.hpp" -+ -+#include -+#include -+ -+namespace caelestia::config { -+ -+namespace { -+ -+QString configDir() { -+ return QStandardPaths::writableLocation(QStandardPaths::GenericConfigLocation) + QStringLiteral("/caelestia/"); -+} -+ -+} // namespace -+ -+TokenConfig::TokenConfig(QObject* parent) -+ : RootConfig(parent) -+ , m_appearance(new AppearanceTokens(this)) -+ , m_sizes(new SizeTokens(this)) { -+ setupFileBackend(configDir() + QStringLiteral("shell-tokens.json")); -+} -+ -+TokenConfig::TokenConfig(TokenConfig* fallback, const QString& filePath, const QString& screen, QObject* parent) -+ : RootConfig(parent) -+ , m_appearance(new AppearanceTokens(this)) -+ , m_sizes(new SizeTokens(this)) { -+ if (!filePath.isEmpty()) -+ setupFileBackend(filePath, screen); -+ if (fallback) -+ syncFromGlobal(fallback); -+} -+ -+TokenConfig* TokenConfig::instance() { -+ static TokenConfig instance; -+ return &instance; -+} -+ -+TokenConfig* TokenConfig::defaults() { -+ if (!m_defaults) -+ m_defaults = new TokenConfig(nullptr, QString(), QString(), this); -+ return m_defaults; -+} -+ -+TokenConfig* TokenConfig::forScreen(const QString& screen) { -+ return MonitorConfigManager::instance()->tokensForScreen(screen); -+} -+ -+TokenConfig* TokenConfig::create(QQmlEngine*, QJSEngine*) { -+ QQmlEngine::setObjectOwnership(instance(), QQmlEngine::CppOwnership); -+ return instance(); -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/tokens.hpp b/plugin/src/Caelestia/Config/tokens.hpp -new file mode 100644 -index 00000000..28bac1f1 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/tokens.hpp -@@ -0,0 +1,351 @@ -+#pragma once -+ -+#include "rootconfig.hpp" -+ -+#include -+#include -+ -+namespace caelestia::config { -+ -+class AnimCurves : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(QList, emphasized) -+ CONFIG_GLOBAL_PROPERTY(QList, emphasizedAccel) -+ CONFIG_GLOBAL_PROPERTY(QList, emphasizedDecel) -+ CONFIG_GLOBAL_PROPERTY(QList, standard) -+ CONFIG_GLOBAL_PROPERTY(QList, standardAccel) -+ CONFIG_GLOBAL_PROPERTY(QList, standardDecel) -+ CONFIG_GLOBAL_PROPERTY(QList, expressiveFastSpatial) -+ CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultSpatial) -+ CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowSpatial) -+ CONFIG_GLOBAL_PROPERTY(QList, expressiveFastEffects) -+ CONFIG_GLOBAL_PROPERTY(QList, expressiveDefaultEffects) -+ CONFIG_GLOBAL_PROPERTY(QList, expressiveSlowEffects) -+ -+public: -+ explicit AnimCurves(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_emphasized({ 0.05, 0, 2.0 / 15.0, 0.06, 1.0 / 6.0, 0.4, 5.0 / 24.0, 0.82, 0.25, 1, 1, 1 }) -+ , m_emphasizedAccel({ 0.3, 0, 0.8, 0.15, 1, 1 }) -+ , m_emphasizedDecel({ 0.05, 0.7, 0.1, 1, 1, 1 }) -+ , m_standard({ 0.2, 0, 0, 1, 1, 1 }) -+ , m_standardAccel({ 0.3, 0, 1, 1, 1, 1 }) -+ , m_standardDecel({ 0, 0, 0, 1, 1, 1 }) -+ , m_expressiveFastSpatial({ 0.42, 1.67, 0.21, 0.9, 1, 1 }) -+ , m_expressiveDefaultSpatial({ 0.38, 1.21, 0.22, 1, 1, 1 }) -+ , m_expressiveSlowSpatial({ 0.39, 1.29, 0.35, 0.98, 1, 1 }) -+ , m_expressiveFastEffects({ 0.31, 0.94, 0.34, 1, 1, 1 }) -+ , m_expressiveDefaultEffects({ 0.34, 0.8, 0.34, 1, 1, 1 }) -+ , m_expressiveSlowEffects({ 0.34, 0.88, 0.34, 1, 1, 1 }) {} -+}; -+ -+class RoundingTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, extraSmall, 4) -+ CONFIG_PROPERTY(int, small, 12) -+ CONFIG_PROPERTY(int, normal, 17) -+ CONFIG_PROPERTY(int, large, 25) -+ CONFIG_PROPERTY(int, full, 1000) -+ -+public: -+ explicit RoundingTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class SpacingTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, small, 7) -+ CONFIG_PROPERTY(int, smaller, 10) -+ CONFIG_PROPERTY(int, normal, 12) -+ CONFIG_PROPERTY(int, larger, 15) -+ CONFIG_PROPERTY(int, large, 20) -+ -+public: -+ explicit SpacingTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class PaddingTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, small, 5) -+ CONFIG_PROPERTY(int, smaller, 7) -+ CONFIG_PROPERTY(int, normal, 10) -+ CONFIG_PROPERTY(int, larger, 12) -+ CONFIG_PROPERTY(int, large, 15) -+ -+public: -+ explicit PaddingTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class FontSizeTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, small, 11) -+ CONFIG_PROPERTY(int, smaller, 12) -+ CONFIG_PROPERTY(int, normal, 13) -+ CONFIG_PROPERTY(int, larger, 15) -+ CONFIG_PROPERTY(int, large, 18) -+ CONFIG_PROPERTY(int, extraLarge, 28) -+ -+public: -+ explicit FontSizeTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class AnimDurationTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(int, small, 200) -+ CONFIG_GLOBAL_PROPERTY(int, normal, 400) -+ CONFIG_GLOBAL_PROPERTY(int, large, 600) -+ CONFIG_GLOBAL_PROPERTY(int, extraLarge, 1000) -+ CONFIG_GLOBAL_PROPERTY(int, expressiveFastSpatial, 350) -+ CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultSpatial, 500) -+ CONFIG_GLOBAL_PROPERTY(int, expressiveSlowSpatial, 650) -+ CONFIG_GLOBAL_PROPERTY(int, expressiveFastEffects, 150) -+ CONFIG_GLOBAL_PROPERTY(int, expressiveDefaultEffects, 200) -+ CONFIG_GLOBAL_PROPERTY(int, expressiveSlowEffects, 300) -+ -+public: -+ explicit AnimDurationTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class AppearanceTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_SUBOBJECT(AnimCurves, curves) -+ CONFIG_SUBOBJECT(RoundingTokens, rounding) -+ CONFIG_SUBOBJECT(SpacingTokens, spacing) -+ CONFIG_SUBOBJECT(PaddingTokens, padding) -+ CONFIG_SUBOBJECT(FontSizeTokens, fontSize) -+ CONFIG_SUBOBJECT(AnimDurationTokens, animDurations) -+ -+public: -+ explicit AppearanceTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_curves(new AnimCurves(this)) -+ , m_rounding(new RoundingTokens(this)) -+ , m_spacing(new SpacingTokens(this)) -+ , m_padding(new PaddingTokens(this)) -+ , m_fontSize(new FontSizeTokens(this)) -+ , m_animDurations(new AnimDurationTokens(this)) {} -+}; -+ -+class BarTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, innerWidth, 40) -+ CONFIG_PROPERTY(int, windowPreviewSize, 400) -+ CONFIG_PROPERTY(int, trayMenuWidth, 300) -+ CONFIG_PROPERTY(int, batteryWidth, 250) -+ CONFIG_PROPERTY(int, networkWidth, 320) -+ CONFIG_PROPERTY(int, kbLayoutWidth, 320) -+ -+public: -+ explicit BarTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class DashboardTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, tabIndicatorHeight, 3) -+ CONFIG_PROPERTY(int, tabIndicatorSpacing, 5) -+ CONFIG_PROPERTY(int, infoWidth, 200) -+ CONFIG_PROPERTY(int, infoIconSize, 25) -+ CONFIG_PROPERTY(int, dateTimeWidth, 110) -+ CONFIG_PROPERTY(int, mediaWidth, 200) -+ CONFIG_PROPERTY(int, mediaProgressSweep, 180) -+ CONFIG_PROPERTY(int, mediaProgressThickness, 8) -+ CONFIG_PROPERTY(int, resourceProgressThickness, 10) -+ CONFIG_PROPERTY(int, weatherWidth, 250) -+ CONFIG_PROPERTY(int, mediaCoverArtSize, 150) -+ CONFIG_PROPERTY(int, mediaVisualiserSize, 80) -+ CONFIG_PROPERTY(int, resourceSize, 200) -+ -+public: -+ explicit DashboardTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class LauncherTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, itemWidth, 600) -+ CONFIG_PROPERTY(int, itemHeight, 57) -+ CONFIG_PROPERTY(int, wallpaperWidth, 280) -+ CONFIG_PROPERTY(int, wallpaperHeight, 200) -+ -+public: -+ explicit LauncherTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class NotifsTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, width, 400) -+ CONFIG_GLOBAL_PROPERTY(int, image, 41) -+ CONFIG_PROPERTY(int, badge, 20) -+ -+public: -+ explicit NotifsTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class OsdTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, sliderWidth, 30) -+ CONFIG_PROPERTY(int, sliderHeight, 150) -+ -+public: -+ explicit OsdTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class SessionTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, button, 80) -+ -+public: -+ explicit SessionTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class SidebarTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, width, 430) -+ -+public: -+ explicit SidebarTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class UtilitiesTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(int, width, 430) -+ CONFIG_PROPERTY(int, toastWidth, 430) -+ -+public: -+ explicit UtilitiesTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class LockTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, heightMult, 0.7) -+ CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) -+ CONFIG_PROPERTY(int, centerWidth, 600) -+ -+public: -+ explicit LockTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class WInfoTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, heightMult, 0.7) -+ CONFIG_PROPERTY(qreal, detailsWidth, 500) -+ -+public: -+ explicit WInfoTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class ControlCenterTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(qreal, heightMult, 0.7) -+ CONFIG_PROPERTY(qreal, ratio, 16.0 / 9.0) -+ -+public: -+ explicit ControlCenterTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class SizeTokens : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_SUBOBJECT(BarTokens, bar) -+ CONFIG_SUBOBJECT(DashboardTokens, dashboard) -+ CONFIG_SUBOBJECT(LauncherTokens, launcher) -+ CONFIG_SUBOBJECT(NotifsTokens, notifs) -+ CONFIG_SUBOBJECT(OsdTokens, osd) -+ CONFIG_SUBOBJECT(SessionTokens, session) -+ CONFIG_SUBOBJECT(SidebarTokens, sidebar) -+ CONFIG_SUBOBJECT(UtilitiesTokens, utilities) -+ CONFIG_SUBOBJECT(LockTokens, lock) -+ CONFIG_SUBOBJECT(WInfoTokens, winfo) -+ CONFIG_SUBOBJECT(ControlCenterTokens, controlCenter) -+ -+public: -+ explicit SizeTokens(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_bar(new BarTokens(this)) -+ , m_dashboard(new DashboardTokens(this)) -+ , m_launcher(new LauncherTokens(this)) -+ , m_notifs(new NotifsTokens(this)) -+ , m_osd(new OsdTokens(this)) -+ , m_session(new SessionTokens(this)) -+ , m_sidebar(new SidebarTokens(this)) -+ , m_utilities(new UtilitiesTokens(this)) -+ , m_lock(new LockTokens(this)) -+ , m_winfo(new WInfoTokens(this)) -+ , m_controlCenter(new ControlCenterTokens(this)) {} -+}; -+ -+class TokenConfig : public RootConfig { -+ Q_OBJECT -+ QML_ELEMENT -+ QML_SINGLETON -+ -+ CONFIG_SUBOBJECT(AppearanceTokens, appearance) -+ CONFIG_SUBOBJECT(SizeTokens, sizes) -+ -+public: -+ static TokenConfig* instance(); -+ [[nodiscard]] Q_INVOKABLE TokenConfig* defaults(); -+ [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); -+ static TokenConfig* create(QQmlEngine*, QJSEngine*); -+ -+private: -+ friend class MonitorConfigManager; -+ explicit TokenConfig(QObject* parent = nullptr); -+ explicit TokenConfig( -+ TokenConfig* fallback, const QString& filePath, const QString& screen = {}, QObject* parent = nullptr); -+ -+ TokenConfig* m_defaults = nullptr; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/tokensattached.cpp b/plugin/src/Caelestia/Config/tokensattached.cpp -new file mode 100644 -index 00000000..16f863b8 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/tokensattached.cpp -@@ -0,0 +1,118 @@ -+#include "tokensattached.hpp" -+#include "anim.hpp" -+#include "appearanceconfig.hpp" -+#include "config.hpp" -+#include "monitorconfigmanager.hpp" -+#include "tokens.hpp" -+ -+#include -+ -+namespace caelestia::config { -+ -+namespace { -+ -+const AppearanceConfig* resolveAppearance(GlobalConfig* config, bool complete, const char* prop, QObject* parent) { -+ if (config) -+ return config->appearance(); -+ if ((complete || !qobject_cast(parent)) && parent) -+ qCWarning(lcConfig, "Tokens.%s accessed without a screen set on %s", prop, parent->metaObject()->className()); -+ return GlobalConfig::instance()->appearance(); -+} -+ -+} // namespace -+ -+Tokens::Tokens(QObject* parent) -+ : QQuickAttachedPropertyPropagator(parent) -+ , m_anim(new AnimTokens(this)) { -+ bindAnim(); -+ initialize(); -+} -+ -+void Tokens::classBegin() {} -+ -+void Tokens::componentComplete() { -+ m_complete = true; -+} -+ -+QString Tokens::screen() const { -+ return m_screen; -+} -+ -+void Tokens::inheritScreen(const QString& screen) { -+ if (screen == m_screen) -+ return; -+ -+ m_screen = screen; -+ -+ if (m_screen.isEmpty()) { -+ m_config = nullptr; -+ m_tokens = nullptr; -+ } else { -+ m_config = MonitorConfigManager::instance()->configForScreen(m_screen); -+ m_tokens = MonitorConfigManager::instance()->tokensForScreen(m_screen); -+ } -+ -+ propagateScreen(); -+ emit sourceChanged(); -+} -+ -+void Tokens::propagateScreen() { -+ const auto children = attachedChildren(); -+ for (auto* const child : children) { -+ auto* const tokens = qobject_cast(child); -+ if (tokens) -+ tokens->inheritScreen(m_screen); -+ } -+} -+ -+void Tokens::attachedParentChange( -+ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) { -+ Q_UNUSED(oldParent); -+ auto* const tokens = qobject_cast(newParent); -+ if (tokens) -+ inheritScreen(tokens->screen()); -+} -+ -+void Tokens::bindAnim() { -+ m_anim->bindDurations(GlobalConfig::instance()->appearance()->anim()->durations()); -+ m_anim->bindCurves(TokenConfig::instance()->appearance()->curves()); -+} -+ -+#define TOKENS_ATTACHED_GETTER(Type, name) \ -+ const Type* Tokens::name() const { \ -+ auto* a = resolveAppearance(m_config, m_complete, #name, parent()); \ -+ return a ? a->name() : nullptr; \ -+ } -+ -+TOKENS_ATTACHED_GETTER(AppearanceRounding, rounding) -+TOKENS_ATTACHED_GETTER(AppearanceSpacing, spacing) -+TOKENS_ATTACHED_GETTER(AppearancePadding, padding) -+TOKENS_ATTACHED_GETTER(AppearanceFont, font) -+ -+#undef TOKENS_ATTACHED_GETTER -+ -+const AppearanceTransparency* Tokens::transparency() const { -+ return GlobalConfig::instance()->appearance()->transparency(); // Transparency is always global -+} -+ -+const SizeTokens* Tokens::sizes() const { -+ if (m_tokens) -+ return m_tokens->sizes(); -+ if ((m_complete || !qobject_cast(parent())) && parent()) -+ qCWarning(lcConfig, "Tokens.sizes accessed without a screen set on %s", parent()->metaObject()->className()); -+ return TokenConfig::instance()->sizes(); -+} -+ -+const AnimTokens* Tokens::anim() const { -+ return m_anim; -+} -+ -+TokenConfig* Tokens::forScreen(const QString& screen) { -+ return TokenConfig::forScreen(screen); -+} -+ -+Tokens* Tokens::qmlAttachedProperties(QObject* object) { -+ return new Tokens(object); -+} -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/tokensattached.hpp b/plugin/src/Caelestia/Config/tokensattached.hpp -new file mode 100644 -index 00000000..810b0c0d ---- /dev/null -+++ b/plugin/src/Caelestia/Config/tokensattached.hpp -@@ -0,0 +1,78 @@ -+#pragma once -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+class AnimTokens; -+class AppearanceFont; -+class AppearancePadding; -+class AppearanceRounding; -+class AppearanceSpacing; -+class AppearanceTransparency; -+class GlobalConfig; -+class SizeTokens; -+class TokenConfig; -+ -+class Tokens : public QQuickAttachedPropertyPropagator, public QQmlParserStatus { -+ Q_OBJECT -+ Q_INTERFACES(QQmlParserStatus) -+ QML_ELEMENT -+ QML_UNCREATABLE("") -+ QML_ATTACHED(Tokens) -+ Q_MOC_INCLUDE("anim.hpp") -+ Q_MOC_INCLUDE("appearanceconfig.hpp") -+ Q_MOC_INCLUDE("tokens.hpp") -+ -+ Q_PROPERTY(QString screen READ screen WRITE inheritScreen NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::AppearanceRounding* rounding READ rounding NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::AppearanceSpacing* spacing READ spacing NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::AppearancePadding* padding READ padding NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::AppearanceFont* font READ font NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::AppearanceTransparency* transparency READ transparency NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::SizeTokens* sizes READ sizes NOTIFY sourceChanged) -+ Q_PROPERTY(const caelestia::config::AnimTokens* anim READ anim NOTIFY sourceChanged) -+ -+public: -+ explicit Tokens(QObject* parent = nullptr); -+ -+ [[nodiscard]] QString screen() const; -+ void inheritScreen(const QString& screen); -+ -+ [[nodiscard]] const AppearanceRounding* rounding() const; -+ [[nodiscard]] const AppearanceSpacing* spacing() const; -+ [[nodiscard]] const AppearancePadding* padding() const; -+ [[nodiscard]] const AppearanceFont* font() const; -+ [[nodiscard]] const AppearanceTransparency* transparency() const; -+ -+ [[nodiscard]] const SizeTokens* sizes() const; -+ [[nodiscard]] const AnimTokens* anim() const; -+ -+ [[nodiscard]] Q_INVOKABLE static TokenConfig* forScreen(const QString& screen); -+ -+ static Tokens* qmlAttachedProperties(QObject* object); -+ -+signals: -+ void sourceChanged(); -+ -+protected: -+ void attachedParentChange( -+ QQuickAttachedPropertyPropagator* newParent, QQuickAttachedPropertyPropagator* oldParent) override; -+ -+private: -+ void classBegin() override; -+ void componentComplete() override; -+ -+ void propagateScreen(); -+ void bindAnim(); -+ -+ bool m_complete = false; -+ QString m_screen; -+ GlobalConfig* m_config = nullptr; -+ TokenConfig* m_tokens = nullptr; -+ AnimTokens* m_anim = nullptr; -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/userpaths.hpp b/plugin/src/Caelestia/Config/userpaths.hpp -new file mode 100644 -index 00000000..da6973a7 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/userpaths.hpp -@@ -0,0 +1,30 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+#include -+ -+namespace caelestia::config { -+ -+using Qt::StringLiterals::operator""_s; -+ -+class UserPaths : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY( -+ QString, wallpaperDir, QStandardPaths::writableLocation(QStandardPaths::PicturesLocation) + u"/Wallpapers"_s) -+ CONFIG_GLOBAL_PROPERTY(QString, lyricsDir, QDir::homePath() + u"/Music/lyrics/"_s) -+ CONFIG_PROPERTY(QString, sessionGif, u"root:/assets/kurukuru.gif"_s) -+ CONFIG_PROPERTY(QString, mediaGif, u"root:/assets/bongocat.gif"_s) -+ CONFIG_PROPERTY(QString, noNotifsPic, u"root:/assets/dino.png"_s) -+ CONFIG_PROPERTY(QString, lockNoNotifsPic, u"root:/assets/dino.png"_s) -+ -+public: -+ explicit UserPaths(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/utilitiesconfig.hpp b/plugin/src/Caelestia/Config/utilitiesconfig.hpp -new file mode 100644 -index 00000000..153f21d5 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/utilitiesconfig.hpp -@@ -0,0 +1,88 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+#include -+#include -+ -+namespace caelestia::config { -+ -+using Qt::StringLiterals::operator""_s; -+ -+class UtilitiesToasts : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(QString, fullscreen, u"off"_s) -+ CONFIG_GLOBAL_PROPERTY(bool, configLoaded, true) -+ CONFIG_GLOBAL_PROPERTY(bool, chargingChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, gameModeChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, dndChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, audioOutputChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, audioInputChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, capsLockChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, numLockChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, kbLayoutChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, kbLimit, true) -+ CONFIG_GLOBAL_PROPERTY(bool, vpnChanged, true) -+ CONFIG_GLOBAL_PROPERTY(bool, nowPlaying, false) -+ -+public: -+ explicit UtilitiesToasts(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class UtilitiesVpn : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_GLOBAL_PROPERTY(bool, enabled, false) -+ CONFIG_GLOBAL_PROPERTY(QVariantList, provider) -+ -+public: -+ explicit UtilitiesVpn(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class UtilitiesRecording : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(QString, videoMode, u"fullscreen"_s) -+ CONFIG_PROPERTY(bool, recordSystem, true) -+ CONFIG_PROPERTY(bool, recordMicrophone, false) -+ -+public: -+ explicit UtilitiesRecording(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+class UtilitiesConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+ CONFIG_PROPERTY(bool, enabled, true) -+ CONFIG_PROPERTY(int, maxToasts, 4) -+ CONFIG_SUBOBJECT(UtilitiesToasts, toasts) -+ CONFIG_SUBOBJECT(UtilitiesVpn, vpn) -+ CONFIG_SUBOBJECT(UtilitiesRecording, recording) -+ CONFIG_PROPERTY(QVariantList, quickToggles, -+ { -+ vmap({ { u"id"_s, u"wifi"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"bluetooth"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"mic"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"settings"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"gameMode"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"dnd"_s }, { u"enabled"_s, true } }), -+ vmap({ { u"id"_s, u"vpn"_s }, { u"enabled"_s, false } }), -+ }) -+ -+public: -+ explicit UtilitiesConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) -+ , m_toasts(new UtilitiesToasts(this)) -+ , m_vpn(new UtilitiesVpn(this)) -+ , m_recording(new UtilitiesRecording(this)) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Config/winfoconfig.hpp b/plugin/src/Caelestia/Config/winfoconfig.hpp -new file mode 100644 -index 00000000..30d4f5e6 ---- /dev/null -+++ b/plugin/src/Caelestia/Config/winfoconfig.hpp -@@ -0,0 +1,18 @@ -+#pragma once -+ -+#include "configobject.hpp" -+ -+namespace caelestia::config { -+ -+// WInfoConfig has no serialized properties (serializer returns {}) -+// All properties are in AdvancedConfig.winfo -+class WInfoConfig : public ConfigObject { -+ Q_OBJECT -+ QML_ANONYMOUS -+ -+public: -+ explicit WInfoConfig(QObject* parent = nullptr) -+ : ConfigObject(parent) {} -+}; -+ -+} // namespace caelestia::config -diff --git a/plugin/src/Caelestia/Images/CMakeLists.txt b/plugin/src/Caelestia/Images/CMakeLists.txt -new file mode 100644 -index 00000000..d869667d ---- /dev/null -+++ b/plugin/src/Caelestia/Images/CMakeLists.txt -@@ -0,0 +1,11 @@ -+qml_module(caelestia-images -+ URI Caelestia.Images -+ SOURCES -+ cachingimageprovider.cpp -+ imagecacher.cpp -+ iutils.cpp -+ LIBRARIES -+ Qt::Gui -+ Qt::Quick -+ Qt::Concurrent -+) -diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.cpp b/plugin/src/Caelestia/Images/cachingimageprovider.cpp -new file mode 100644 -index 00000000..768e05da ---- /dev/null -+++ b/plugin/src/Caelestia/Images/cachingimageprovider.cpp -@@ -0,0 +1,120 @@ -+#include "cachingimageprovider.hpp" -+ -+#include "imagecacher.hpp" -+ -+#include -+#include -+#include -+#include -+#include -+#include -+ -+Q_LOGGING_CATEGORY(lcCProv, "caelestia.images.cacheprovider", QtInfoMsg) -+ -+namespace caelestia::images { -+ -+namespace { -+ -+class CachingImageResponse final : public QQuickImageResponse, public QRunnable { -+public: -+ CachingImageResponse(const QString& id, const QSize& requestedSize, ImageCacher::FillMode fillMode) -+ : m_id(id) -+ , m_requestedSize(requestedSize) -+ , m_fillMode(fillMode) { -+ setAutoDelete(false); -+ } -+ -+ [[nodiscard]] QQuickTextureFactory* textureFactory() const override { -+ return QQuickTextureFactory::textureFactoryForImage(m_image); -+ } -+ -+ [[nodiscard]] QString errorString() const override { return m_error; } -+ -+ void run() override { -+ process(); -+ emit finished(); -+ } -+ -+private: -+ void process() { -+ QString path = QString::fromUtf8(m_id.toUtf8().percentDecoded()); -+ if (!path.startsWith(QLatin1Char('/'))) -+ path.prepend(QLatin1Char('/')); -+ -+ if (!QFileInfo::exists(path)) { -+ m_error = QStringLiteral("Source file does not exist: ") + path; -+ qCWarning(lcCProv).noquote() << m_error; -+ return; -+ } -+ -+ QSize size = m_requestedSize; -+ const bool needsW = size.width() <= 0; -+ const bool needsH = size.height() <= 0; -+ -+ // If both dimensions are missing, return the original directly -+ if (needsW && needsH) { -+ qCDebug(lcCProv).noquote() << "Given source size is invalid, returning original:" << path; -+ m_image = QImage(path); -+ if (m_image.isNull()) { -+ m_error = QStringLiteral("Failed to decode source: ") + path; -+ qCWarning(lcCProv).noquote() << m_error; -+ } -+ return; -+ } -+ -+ // If one dimension is missing, derive it from the source aspect ratio -+ if (needsW || needsH) { -+ const QImageReader sourceReader(path); -+ const QSize sourceSize = sourceReader.size(); -+ if (!sourceSize.isValid() || sourceSize.isEmpty()) { -+ m_error = QStringLiteral("Could not determine source size for: ") + path; -+ qCWarning(lcCProv).noquote() << m_error; -+ return; -+ } -+ -+ if (needsW) -+ size.setWidth(qRound(size.height() * sourceSize.width() / static_cast(sourceSize.height()))); -+ else -+ size.setHeight(qRound(size.width() * sourceSize.height() / static_cast(sourceSize.width()))); -+ } -+ -+ // Try to use cached image -+ const auto cachePath = ImageCacher::cachePathFor(path, size, m_fillMode); -+ if (!cachePath.isEmpty()) { -+ QImageReader cacheReader(cachePath); -+ if (cacheReader.canRead()) { -+ m_image = cacheReader.read(); -+ if (!m_image.isNull()) -+ return; -+ } -+ } -+ -+ // Schedule cache job (this call will return the original image, but later ones will use cache) -+ ImageCacher::instance()->schedule(path, cachePath, size, m_fillMode); -+ -+ m_image = QImage(path); -+ if (m_image.isNull()) { -+ m_error = QStringLiteral("Failed to decode source: ") + path; -+ qCWarning(lcCProv).noquote() << m_error; -+ } -+ } -+ -+ QString m_id; -+ QSize m_requestedSize; -+ ImageCacher::FillMode m_fillMode; -+ QImage m_image; -+ QString m_error; -+}; -+ -+} // namespace -+ -+CachingImageProvider::CachingImageProvider(FillMode fillMode) -+ : m_fillMode(fillMode) {} -+ -+QQuickImageResponse* CachingImageProvider::requestImageResponse(const QString& id, const QSize& requestedSize) { -+ auto* const response = new CachingImageResponse(id, requestedSize, m_fillMode); -+ QThreadPool::globalInstance()->start(response); -+ return response; -+} -+ -+} // namespace caelestia::images -diff --git a/plugin/src/Caelestia/Images/cachingimageprovider.hpp b/plugin/src/Caelestia/Images/cachingimageprovider.hpp -new file mode 100644 -index 00000000..97082c76 ---- /dev/null -+++ b/plugin/src/Caelestia/Images/cachingimageprovider.hpp -@@ -0,0 +1,21 @@ -+#pragma once -+ -+#include "imagecacher.hpp" -+ -+#include -+ -+namespace caelestia::images { -+ -+class CachingImageProvider : public QQuickAsyncImageProvider { -+public: -+ using FillMode = ImageCacher::FillMode; -+ -+ explicit CachingImageProvider(FillMode fillMode); -+ -+ QQuickImageResponse* requestImageResponse(const QString& id, const QSize& requestedSize) override; -+ -+private: -+ FillMode m_fillMode; -+}; -+ -+} // namespace caelestia::images -diff --git a/plugin/src/Caelestia/Images/imagecacher.cpp b/plugin/src/Caelestia/Images/imagecacher.cpp -new file mode 100644 -index 00000000..dfef8f0b ---- /dev/null -+++ b/plugin/src/Caelestia/Images/imagecacher.cpp -@@ -0,0 +1,159 @@ -+#include "imagecacher.hpp" -+ -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+#include -+ -+Q_LOGGING_CATEGORY(lcCacher, "caelestia.images.cacher", QtInfoMsg) -+ -+namespace caelestia::images { -+ -+namespace { -+ -+QString sha256sum(const QString& path) { -+ QFile file(path); -+ if (!file.open(QIODevice::ReadOnly)) { -+ qCWarning(lcCacher).noquote() << "sha256sum: failed to open" << path; -+ return {}; -+ } -+ -+ QCryptographicHash hash(QCryptographicHash::Sha256); -+ hash.addData(&file); -+ file.close(); -+ -+ return hash.result().toHex(); -+} -+ -+QString fillSuffix(ImageCacher::FillMode fillMode) { -+ switch (fillMode) { -+ case ImageCacher::FillMode::Crop: -+ return QStringLiteral("crop"); -+ case ImageCacher::FillMode::Fit: -+ return QStringLiteral("fit"); -+ default: -+ return QStringLiteral("stretch"); -+ } -+} -+ -+} // namespace -+ -+const QString& ImageCacher::cacheDir() { -+ static const QString s_dir = [] { -+ QString cache = qEnvironmentVariable("XDG_CACHE_HOME"); -+ if (cache.isEmpty()) -+ cache = QDir::homePath() + QStringLiteral("/.cache"); -+ return cache + QStringLiteral("/caelestia/imagecache"); -+ }(); -+ return s_dir; -+} -+ -+QString ImageCacher::cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode) { -+ const QString sha = sha256sum(sourcePath); -+ if (sha.isEmpty()) -+ return {}; -+ -+ const QString filename = -+ QStringLiteral("%1@%2x%3-%4.png") -+ .arg(sha, QString::number(size.width()), QString::number(size.height()), fillSuffix(fillMode)); -+ -+ return cacheDir() + QLatin1Char('/') + filename; -+} -+ -+ImageCacher* ImageCacher::instance() { -+ static ImageCacher s_instance; -+ return &s_instance; -+} -+ -+ImageCacher::ImageCacher(QObject* parent) -+ : QObject(parent) {} -+ -+void ImageCacher::schedule(const QString& sourcePath, const QSize& size, FillMode fillMode) { -+ schedule(sourcePath, cachePathFor(sourcePath, size, fillMode), size, fillMode); -+} -+ -+void ImageCacher::schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { -+ if (cachePath.isEmpty()) -+ return; -+ -+ { -+ QMutexLocker locker(&m_mutex); -+ if (m_inflight.contains(cachePath)) -+ return; -+ m_inflight.insert(cachePath); -+ } -+ -+ QThreadPool::globalInstance()->start([this, sourcePath, cachePath, size, fillMode]() { -+ runJob(sourcePath, cachePath, size, fillMode); -+ QMutexLocker locker(&m_mutex); -+ m_inflight.remove(cachePath); -+ }); -+} -+ -+void ImageCacher::runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode) { -+ if (QFile::exists(cachePath)) { -+ return; -+ } -+ -+ QImage image(sourcePath); -+ if (image.isNull()) { -+ qCWarning(lcCacher).noquote() << "Failed to decode source" << sourcePath; -+ return; -+ } -+ -+ Qt::AspectRatioMode scaleMode; -+ switch (fillMode) { -+ case FillMode::Crop: -+ scaleMode = Qt::KeepAspectRatioByExpanding; -+ break; -+ case FillMode::Fit: -+ scaleMode = Qt::KeepAspectRatio; -+ break; -+ case FillMode::Stretch: -+ scaleMode = Qt::IgnoreAspectRatio; -+ break; -+ } -+ -+ image.convertTo(QImage::Format_ARGB32); -+ image = image.scaled(size, scaleMode, Qt::SmoothTransformation); -+ -+ if (image.isNull()) { -+ qCWarning(lcCacher).noquote() << "Failed to scale" << sourcePath; -+ return; -+ } -+ -+ QImage canvas; -+ if (fillMode == FillMode::Stretch) { -+ canvas = image; -+ } else { -+ canvas = QImage(size, QImage::Format_ARGB32); -+ canvas.fill(Qt::transparent); -+ -+ QPainter painter(&canvas); -+ painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); -+ painter.end(); -+ } -+ -+ const QString parent = QFileInfo(cachePath).absolutePath(); -+ if (!QDir().mkpath(parent)) { -+ qCWarning(lcCacher).noquote() << "Failed to create cache dir" << parent; -+ return; -+ } -+ -+ QSaveFile saveFile(cachePath); -+ if (!saveFile.open(QIODevice::WriteOnly) || !canvas.save(&saveFile, "PNG") || !saveFile.commit()) { -+ qCWarning( -+ lcCacher, "Failed to save to %s: %s", qUtf8Printable(cachePath), qUtf8Printable(saveFile.errorString())); -+ return; -+ } -+ -+ qCDebug(lcCacher).noquote() << "Saved to" << cachePath; -+} -+ -+} // namespace caelestia::images -diff --git a/plugin/src/Caelestia/Images/imagecacher.hpp b/plugin/src/Caelestia/Images/imagecacher.hpp -new file mode 100644 -index 00000000..79608419 ---- /dev/null -+++ b/plugin/src/Caelestia/Images/imagecacher.hpp -@@ -0,0 +1,38 @@ -+#pragma once -+ -+#include -+#include -+#include -+#include -+#include -+ -+namespace caelestia::images { -+ -+class ImageCacher : public QObject { -+ Q_OBJECT -+ -+public: -+ enum class FillMode { -+ Crop, -+ Fit, -+ Stretch, -+ }; -+ -+ static ImageCacher* instance(); -+ -+ static const QString& cacheDir(); -+ static QString cachePathFor(const QString& sourcePath, const QSize& size, FillMode fillMode); -+ -+ void schedule(const QString& sourcePath, const QSize& size, FillMode fillMode); -+ void schedule(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); -+ -+private: -+ explicit ImageCacher(QObject* parent = nullptr); -+ -+ static void runJob(const QString& sourcePath, const QString& cachePath, const QSize& size, FillMode fillMode); -+ -+ QMutex m_mutex; -+ QSet m_inflight; -+}; -+ -+} // namespace caelestia::images -diff --git a/plugin/src/Caelestia/Images/iutils.cpp b/plugin/src/Caelestia/Images/iutils.cpp -new file mode 100644 -index 00000000..996f93dd ---- /dev/null -+++ b/plugin/src/Caelestia/Images/iutils.cpp -@@ -0,0 +1,42 @@ -+#include "iutils.hpp" -+ -+#include "cachingimageprovider.hpp" -+ -+namespace caelestia::images { -+ -+IUtils* IUtils::create(QQmlEngine* engine, QJSEngine* jsEngine) { -+ Q_UNUSED(jsEngine); -+ -+ engine->addImageProvider(QStringLiteral("ccache"), new CachingImageProvider(CachingImageProvider::FillMode::Crop)); -+ engine->addImageProvider(QStringLiteral("fcache"), new CachingImageProvider(CachingImageProvider::FillMode::Fit)); -+ engine->addImageProvider( -+ QStringLiteral("scache"), new CachingImageProvider(CachingImageProvider::FillMode::Stretch)); -+ -+ return new IUtils(engine); -+} -+ -+QUrl IUtils::urlForPath(const QString& path, int fillMode) { -+ if (path.isEmpty()) -+ return QUrl(); -+ -+ QString prefix; -+ switch (fillMode) { -+ case 1: // Image.PreserveAspectFit -+ prefix = QStringLiteral("fcache"); -+ break; -+ case 2: // Image.PreserveAspectCrop -+ prefix = QStringLiteral("ccache"); -+ break; -+ default: // Image.Stretch or any other ones -+ prefix = QStringLiteral("scache"); -+ break; -+ } -+ -+ QUrl url; -+ url.setScheme(QStringLiteral("image")); -+ url.setHost(prefix); -+ url.setPath(path.startsWith(QLatin1Char('/')) ? path : QLatin1Char('/') + path); -+ return url; -+} -+ -+} // namespace caelestia::images -diff --git a/plugin/src/Caelestia/Images/iutils.hpp b/plugin/src/Caelestia/Images/iutils.hpp -new file mode 100644 -index 00000000..2187c8d0 ---- /dev/null -+++ b/plugin/src/Caelestia/Images/iutils.hpp -@@ -0,0 +1,24 @@ -+#pragma once -+ -+#include -+#include -+#include -+ -+namespace caelestia::images { -+ -+class IUtils : public QObject { -+ Q_OBJECT -+ QML_ELEMENT -+ QML_SINGLETON -+ -+public: -+ static IUtils* create(QQmlEngine* engine, QJSEngine* jsEngine); -+ -+ Q_INVOKABLE static QUrl urlForPath(const QString& path, int fillMode); -+ -+private: -+ explicit IUtils(QObject* parent = nullptr) -+ : QObject(parent) {}; -+}; -+ -+} // namespace caelestia::images -diff --git a/plugin/src/Caelestia/Internal/CMakeLists.txt b/plugin/src/Caelestia/Internal/CMakeLists.txt -index bdc58dbf..f4bbc5fd 100644 ---- a/plugin/src/Caelestia/Internal/CMakeLists.txt -+++ b/plugin/src/Caelestia/Internal/CMakeLists.txt -@@ -1,15 +1,17 @@ - qml_module(caelestia-internal - URI Caelestia.Internal - SOURCES -- cachingimagemanager.hpp cachingimagemanager.cpp -- circularindicatormanager.hpp circularindicatormanager.cpp -- hyprdevices.hpp hyprdevices.cpp -- hyprextras.hpp hyprextras.cpp -- logindmanager.hpp logindmanager.cpp -+ arcgauge.cpp -+ circularbuffer.cpp -+ circularindicatormanager.cpp -+ hyprdevices.cpp -+ hyprextras.cpp -+ logindmanager.cpp -+ sparklineitem.cpp -+ visualiserbars.cpp - LIBRARIES - Qt::Gui - Qt::Quick -- Qt::Concurrent - Qt::Network - Qt::DBus - ) -diff --git a/plugin/src/Caelestia/Internal/arcgauge.cpp b/plugin/src/Caelestia/Internal/arcgauge.cpp -new file mode 100644 -index 00000000..d534f5f7 ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/arcgauge.cpp -@@ -0,0 +1,119 @@ -+#include "arcgauge.hpp" -+ -+#include -+#include -+#include -+ -+namespace caelestia::internal { -+ -+ArcGauge::ArcGauge(QQuickItem* parent) -+ : QQuickPaintedItem(parent) { -+ setAntialiasing(true); -+} -+ -+void ArcGauge::paint(QPainter* painter) { -+ const qreal w = width(); -+ const qreal h = height(); -+ const qreal side = qMin(w, h); -+ const qreal radius = (side - m_lineWidth - 2.0) / 2.0; -+ const qreal cx = w / 2.0; -+ const qreal cy = h / 2.0; -+ -+ const QRectF arcRect(cx - radius, cy - radius, radius * 2.0, radius * 2.0); -+ -+ // Convert from Canvas convention (CW radians from 3 o'clock) to QPainter (CCW 1/16th degrees) -+ const int startAngle16 = qRound(-(m_startAngle * 180.0 / M_PI) * 16.0); -+ const int sweepAngle16 = qRound(-(m_sweepAngle * 180.0 / M_PI) * 16.0); -+ -+ painter->setRenderHint(QPainter::Antialiasing, true); -+ -+ // Draw track arc -+ QPen trackPen(m_trackColor, m_lineWidth); -+ trackPen.setCapStyle(Qt::RoundCap); -+ painter->setPen(trackPen); -+ painter->setBrush(Qt::NoBrush); -+ painter->drawArc(arcRect, startAngle16, sweepAngle16); -+ -+ // Draw value arc -+ if (m_percentage > 0.0) { -+ const int valueSweep16 = qRound(static_cast(sweepAngle16) * m_percentage); -+ QPen valuePen(m_accentColor, m_lineWidth); -+ valuePen.setCapStyle(Qt::RoundCap); -+ painter->setPen(valuePen); -+ painter->drawArc(arcRect, startAngle16, valueSweep16); -+ } -+} -+ -+qreal ArcGauge::percentage() const { -+ return m_percentage; -+} -+ -+void ArcGauge::setPercentage(qreal percentage) { -+ if (qFuzzyCompare(m_percentage, percentage)) -+ return; -+ m_percentage = percentage; -+ emit percentageChanged(); -+ update(); -+} -+ -+QColor ArcGauge::accentColor() const { -+ return m_accentColor; -+} -+ -+void ArcGauge::setAccentColor(const QColor& color) { -+ if (m_accentColor == color) -+ return; -+ m_accentColor = color; -+ emit accentColorChanged(); -+ update(); -+} -+ -+QColor ArcGauge::trackColor() const { -+ return m_trackColor; -+} -+ -+void ArcGauge::setTrackColor(const QColor& color) { -+ if (m_trackColor == color) -+ return; -+ m_trackColor = color; -+ emit trackColorChanged(); -+ update(); -+} -+ -+qreal ArcGauge::startAngle() const { -+ return m_startAngle; -+} -+ -+void ArcGauge::setStartAngle(qreal angle) { -+ if (qFuzzyCompare(m_startAngle, angle)) -+ return; -+ m_startAngle = angle; -+ emit startAngleChanged(); -+ update(); -+} -+ -+qreal ArcGauge::sweepAngle() const { -+ return m_sweepAngle; -+} -+ -+void ArcGauge::setSweepAngle(qreal angle) { -+ if (qFuzzyCompare(m_sweepAngle, angle)) -+ return; -+ m_sweepAngle = angle; -+ emit sweepAngleChanged(); -+ update(); -+} -+ -+qreal ArcGauge::lineWidth() const { -+ return m_lineWidth; -+} -+ -+void ArcGauge::setLineWidth(qreal width) { -+ if (qFuzzyCompare(m_lineWidth, width)) -+ return; -+ m_lineWidth = width; -+ emit lineWidthChanged(); -+ update(); -+} -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/arcgauge.hpp b/plugin/src/Caelestia/Internal/arcgauge.hpp -new file mode 100644 -index 00000000..4ccb1fd0 ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/arcgauge.hpp -@@ -0,0 +1,61 @@ -+#pragma once -+ -+#include -+#include -+#include -+#include -+ -+namespace caelestia::internal { -+ -+class ArcGauge : public QQuickPaintedItem { -+ Q_OBJECT -+ QML_ELEMENT -+ -+ Q_PROPERTY(qreal percentage READ percentage WRITE setPercentage NOTIFY percentageChanged) -+ Q_PROPERTY(QColor accentColor READ accentColor WRITE setAccentColor NOTIFY accentColorChanged) -+ Q_PROPERTY(QColor trackColor READ trackColor WRITE setTrackColor NOTIFY trackColorChanged) -+ Q_PROPERTY(qreal startAngle READ startAngle WRITE setStartAngle NOTIFY startAngleChanged) -+ Q_PROPERTY(qreal sweepAngle READ sweepAngle WRITE setSweepAngle NOTIFY sweepAngleChanged) -+ Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) -+ -+public: -+ explicit ArcGauge(QQuickItem* parent = nullptr); -+ -+ void paint(QPainter* painter) override; -+ -+ [[nodiscard]] qreal percentage() const; -+ void setPercentage(qreal percentage); -+ -+ [[nodiscard]] QColor accentColor() const; -+ void setAccentColor(const QColor& color); -+ -+ [[nodiscard]] QColor trackColor() const; -+ void setTrackColor(const QColor& color); -+ -+ [[nodiscard]] qreal startAngle() const; -+ void setStartAngle(qreal angle); -+ -+ [[nodiscard]] qreal sweepAngle() const; -+ void setSweepAngle(qreal angle); -+ -+ [[nodiscard]] qreal lineWidth() const; -+ void setLineWidth(qreal width); -+ -+signals: -+ void percentageChanged(); -+ void accentColorChanged(); -+ void trackColorChanged(); -+ void startAngleChanged(); -+ void sweepAngleChanged(); -+ void lineWidthChanged(); -+ -+private: -+ qreal m_percentage = 0.0; -+ QColor m_accentColor; -+ QColor m_trackColor; -+ qreal m_startAngle = 0.75 * M_PI; -+ qreal m_sweepAngle = 1.5 * M_PI; -+ qreal m_lineWidth = 10.0; -+}; -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp b/plugin/src/Caelestia/Internal/cachingimagemanager.cpp -deleted file mode 100644 -index 1c15cd20..00000000 ---- a/plugin/src/Caelestia/Internal/cachingimagemanager.cpp -+++ /dev/null -@@ -1,223 +0,0 @@ --#include "cachingimagemanager.hpp" -- --#include --#include --#include --#include --#include --#include --#include --#include -- --namespace caelestia::internal { -- --qreal CachingImageManager::effectiveScale() const { -- if (m_item && m_item->window()) { -- return m_item->window()->devicePixelRatio(); -- } -- -- return 1.0; --} -- --QSize CachingImageManager::effectiveSize() const { -- if (!m_item) { -- return QSize(); -- } -- -- const qreal scale = effectiveScale(); -- const QSize size = QSizeF(m_item->width() * scale, m_item->height() * scale).toSize(); -- m_item->setProperty("sourceSize", size); -- return size; --} -- --QQuickItem* CachingImageManager::item() const { -- return m_item; --} -- --void CachingImageManager::setItem(QQuickItem* item) { -- if (m_item == item) { -- return; -- } -- -- if (m_widthConn) { -- disconnect(m_widthConn); -- } -- if (m_heightConn) { -- disconnect(m_heightConn); -- } -- -- m_item = item; -- emit itemChanged(); -- -- if (item) { -- m_widthConn = connect(item, &QQuickItem::widthChanged, this, [this]() { -- updateSource(); -- }); -- m_heightConn = connect(item, &QQuickItem::heightChanged, this, [this]() { -- updateSource(); -- }); -- updateSource(); -- } --} -- --QUrl CachingImageManager::cacheDir() const { -- return m_cacheDir; --} -- --void CachingImageManager::setCacheDir(const QUrl& cacheDir) { -- if (m_cacheDir == cacheDir) { -- return; -- } -- -- m_cacheDir = cacheDir; -- if (!m_cacheDir.path().endsWith("/")) { -- m_cacheDir.setPath(m_cacheDir.path() + "/"); -- } -- emit cacheDirChanged(); --} -- --QString CachingImageManager::path() const { -- return m_path; --} -- --void CachingImageManager::setPath(const QString& path) { -- if (m_path == path) { -- return; -- } -- -- m_path = path; -- emit pathChanged(); -- -- if (!path.isEmpty()) { -- updateSource(path); -- } --} -- --void CachingImageManager::updateSource() { -- updateSource(m_path); --} -- --void CachingImageManager::updateSource(const QString& path) { -- if (path.isEmpty() || path == m_shaPath) { -- // Path is empty or already calculating sha for path -- return; -- } -- -- m_shaPath = path; -- -- const auto future = QtConcurrent::run(&CachingImageManager::sha256sum, path); -- -- const auto watcher = new QFutureWatcher(this); -- -- connect(watcher, &QFutureWatcher::finished, this, [watcher, path, this]() { -- if (m_path != path) { -- // Object is destroyed or path has changed, ignore -- watcher->deleteLater(); -- return; -- } -- -- const QSize size = effectiveSize(); -- -- if (!m_item || !size.width() || !size.height()) { -- watcher->deleteLater(); -- return; -- } -- -- const QString fillMode = m_item->property("fillMode").toString(); -- // clang-format off -- const QString filename = QString("%1@%2x%3-%4.png") -- .arg(watcher->result()).arg(size.width()).arg(size.height()) -- .arg(fillMode == "PreserveAspectCrop" ? "crop" : fillMode == "PreserveAspectFit" ? "fit" : "stretch"); -- // clang-format on -- -- const QUrl cache = m_cacheDir.resolved(QUrl(filename)); -- if (m_cachePath == cache) { -- watcher->deleteLater(); -- return; -- } -- -- m_cachePath = cache; -- emit cachePathChanged(); -- -- if (!cache.isLocalFile()) { -- qWarning() << "CachingImageManager::updateSource: cachePath" << cache << "is not a local file"; -- watcher->deleteLater(); -- return; -- } -- -- const QImageReader reader(cache.toLocalFile()); -- if (reader.canRead()) { -- m_item->setProperty("source", cache); -- } else { -- m_item->setProperty("source", QUrl::fromLocalFile(path)); -- createCache(path, cache.toLocalFile(), fillMode, size); -- } -- -- // Clear current running sha if same -- if (m_shaPath == path) { -- m_shaPath = QString(); -- } -- -- watcher->deleteLater(); -- }); -- -- watcher->setFuture(future); --} -- --QUrl CachingImageManager::cachePath() const { -- return m_cachePath; --} -- --void CachingImageManager::createCache( -- const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const { -- QThreadPool::globalInstance()->start([path, cache, fillMode, size] { -- QImage image(path); -- -- if (image.isNull()) { -- qWarning() << "CachingImageManager::createCache: failed to read" << path; -- return; -- } -- -- image.convertTo(QImage::Format_ARGB32); -- -- if (fillMode == "PreserveAspectCrop") { -- image = image.scaled(size, Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation); -- } else if (fillMode == "PreserveAspectFit") { -- image = image.scaled(size, Qt::KeepAspectRatio, Qt::SmoothTransformation); -- } else { -- image = image.scaled(size, Qt::IgnoreAspectRatio, Qt::SmoothTransformation); -- } -- -- if (fillMode == "PreserveAspectCrop" || fillMode == "PreserveAspectFit") { -- QImage canvas(size, QImage::Format_ARGB32); -- canvas.fill(Qt::transparent); -- -- QPainter painter(&canvas); -- painter.drawImage((size.width() - image.width()) / 2, (size.height() - image.height()) / 2, image); -- painter.end(); -- -- image = canvas; -- } -- -- const QString parent = QFileInfo(cache).absolutePath(); -- if (!QDir().mkpath(parent) || !image.save(cache)) { -- qWarning() << "CachingImageManager::createCache: failed to save to" << cache; -- } -- }); --} -- --QString CachingImageManager::sha256sum(const QString& path) { -- QFile file(path); -- if (!file.open(QIODevice::ReadOnly)) { -- qWarning() << "CachingImageManager::sha256sum: failed to open" << path; -- return ""; -- } -- -- QCryptographicHash hash(QCryptographicHash::Sha256); -- hash.addData(&file); -- file.close(); -- -- return hash.result().toHex(); --} -- --} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp b/plugin/src/Caelestia/Internal/cachingimagemanager.hpp -deleted file mode 100644 -index 3611699b..00000000 ---- a/plugin/src/Caelestia/Internal/cachingimagemanager.hpp -+++ /dev/null -@@ -1,65 +0,0 @@ --#pragma once -- --#include --#include --#include -- --namespace caelestia::internal { -- --class CachingImageManager : public QObject { -- Q_OBJECT -- QML_ELEMENT -- -- Q_PROPERTY(QQuickItem* item READ item WRITE setItem NOTIFY itemChanged REQUIRED) -- Q_PROPERTY(QUrl cacheDir READ cacheDir WRITE setCacheDir NOTIFY cacheDirChanged REQUIRED) -- -- Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) -- Q_PROPERTY(QUrl cachePath READ cachePath NOTIFY cachePathChanged) -- --public: -- explicit CachingImageManager(QObject* parent = nullptr) -- : QObject(parent) -- , m_item(nullptr) {} -- -- [[nodiscard]] QQuickItem* item() const; -- void setItem(QQuickItem* item); -- -- [[nodiscard]] QUrl cacheDir() const; -- void setCacheDir(const QUrl& cacheDir); -- -- [[nodiscard]] QString path() const; -- void setPath(const QString& path); -- -- [[nodiscard]] QUrl cachePath() const; -- -- Q_INVOKABLE void updateSource(); -- Q_INVOKABLE void updateSource(const QString& path); -- --signals: -- void itemChanged(); -- void cacheDirChanged(); -- -- void pathChanged(); -- void cachePathChanged(); -- void usingCacheChanged(); -- --private: -- QString m_shaPath; -- -- QQuickItem* m_item; -- QUrl m_cacheDir; -- -- QString m_path; -- QUrl m_cachePath; -- -- QMetaObject::Connection m_widthConn; -- QMetaObject::Connection m_heightConn; -- -- [[nodiscard]] qreal effectiveScale() const; -- [[nodiscard]] QSize effectiveSize() const; -- -- void createCache(const QString& path, const QString& cache, const QString& fillMode, const QSize& size) const; -- [[nodiscard]] static QString sha256sum(const QString& path); --}; -- --} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/circularbuffer.cpp b/plugin/src/Caelestia/Internal/circularbuffer.cpp -new file mode 100644 -index 00000000..9701e7fa ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/circularbuffer.cpp -@@ -0,0 +1,94 @@ -+#include "circularbuffer.hpp" -+ -+#include -+ -+namespace caelestia::internal { -+ -+CircularBuffer::CircularBuffer(QObject* parent) -+ : QObject(parent) {} -+ -+int CircularBuffer::capacity() const { -+ return m_capacity; -+} -+ -+void CircularBuffer::setCapacity(int capacity) { -+ if (capacity < 0) -+ capacity = 0; -+ if (m_capacity == capacity) -+ return; -+ -+ const auto old = values(); -+ -+ m_capacity = capacity; -+ m_data.resize(capacity); -+ m_data.fill(0.0); -+ m_head = 0; -+ m_count = 0; -+ -+ // Re-push old values, keeping the most recent ones -+ const auto start = old.size() > capacity ? old.size() - capacity : 0; -+ for (auto i = start; i < old.size(); ++i) { -+ m_data[m_head] = old[i]; -+ m_head = (m_head + 1) % m_capacity; -+ m_count++; -+ } -+ -+ emit capacityChanged(); -+ emit countChanged(); -+ emit valuesChanged(); -+} -+ -+int CircularBuffer::count() const { -+ return m_count; -+} -+ -+QList CircularBuffer::values() const { -+ QList result; -+ result.reserve(m_count); -+ for (int i = 0; i < m_count; ++i) -+ result.append(at(i)); -+ return result; -+} -+ -+qreal CircularBuffer::maximum() const { -+ if (m_count == 0) -+ return 0.0; -+ -+ qreal maxVal = at(0); -+ for (int i = 1; i < m_count; ++i) -+ maxVal = std::max(maxVal, at(i)); -+ return maxVal; -+} -+ -+void CircularBuffer::push(qreal value) { -+ if (m_capacity <= 0) -+ return; -+ -+ m_data[m_head] = value; -+ m_head = (m_head + 1) % m_capacity; -+ if (m_count < m_capacity) { -+ m_count++; -+ emit countChanged(); -+ } -+ emit valuesChanged(); -+} -+ -+void CircularBuffer::clear() { -+ if (m_count == 0) -+ return; -+ -+ m_head = 0; -+ m_count = 0; -+ emit countChanged(); -+ emit valuesChanged(); -+} -+ -+qreal CircularBuffer::at(int index) const { -+ if (index < 0 || index >= m_count) -+ return 0.0; -+ -+ const int actualIndex = (m_head - m_count + index + m_capacity) % m_capacity; -+ return m_data[actualIndex]; -+} -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/circularbuffer.hpp b/plugin/src/Caelestia/Internal/circularbuffer.hpp -new file mode 100644 -index 00000000..ab2dba56 ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/circularbuffer.hpp -@@ -0,0 +1,44 @@ -+#pragma once -+ -+#include -+#include -+#include -+ -+namespace caelestia::internal { -+ -+class CircularBuffer : public QObject { -+ Q_OBJECT -+ QML_ELEMENT -+ -+ Q_PROPERTY(int capacity READ capacity WRITE setCapacity NOTIFY capacityChanged) -+ Q_PROPERTY(int count READ count NOTIFY countChanged) -+ Q_PROPERTY(QList values READ values NOTIFY valuesChanged) -+ Q_PROPERTY(qreal maximum READ maximum NOTIFY valuesChanged) -+ -+public: -+ explicit CircularBuffer(QObject* parent = nullptr); -+ -+ [[nodiscard]] int capacity() const; -+ void setCapacity(int capacity); -+ -+ [[nodiscard]] int count() const; -+ [[nodiscard]] QList values() const; -+ [[nodiscard]] qreal maximum() const; -+ -+ Q_INVOKABLE void push(qreal value); -+ Q_INVOKABLE void clear(); -+ Q_INVOKABLE [[nodiscard]] qreal at(int index) const; -+ -+signals: -+ void capacityChanged(); -+ void countChanged(); -+ void valuesChanged(); -+ -+private: -+ QVector m_data; -+ int m_head = 0; -+ int m_count = 0; -+ int m_capacity = 0; -+}; -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp -index 434b7562..21249979 100644 ---- a/plugin/src/Caelestia/Internal/circularindicatormanager.cpp -+++ b/plugin/src/Caelestia/Internal/circularindicatormanager.cpp -@@ -153,8 +153,10 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { - spinRotation += m_curve.valueForProgress(getFractionInRange(playtime, spinDelay, DURATION_SPIN_IN_MS)) * - SPIN_ROTATION_DEGREES; - } -+ const auto oldRotation = m_rotation; - m_rotation = constantRotation + spinRotation; -- emit rotationChanged(); -+ if (!qFuzzyCompare(m_rotation + 1.0, oldRotation + 1.0)) -+ emit rotationChanged(); - - // Grow active indicator. - qreal fraction = -@@ -182,6 +184,8 @@ void CircularIndicatorManager::updateRetreat(qreal progress) { - void CircularIndicatorManager::updateAdvance(qreal progress) { - using namespace advance; - const auto playtime = progress * TOTAL_DURATION_IN_MS; -+ const auto oldStart = m_startFraction; -+ const auto oldEnd = m_endFraction; - - // Adds constant rotation to segment positions. - m_startFraction = CONSTANT_ROTATION_DEGREES * progress + TAIL_DEGREES_OFFSET; -@@ -204,8 +208,10 @@ void CircularIndicatorManager::updateAdvance(qreal progress) { - m_startFraction /= 360.0; - m_endFraction /= 360.0; - -- emit startFractionChanged(); -- emit endFractionChanged(); -+ if (!qFuzzyCompare(m_startFraction + 1.0, oldStart + 1.0)) -+ emit startFractionChanged(); -+ if (!qFuzzyCompare(m_endFraction + 1.0, oldEnd + 1.0)) -+ emit endFractionChanged(); - } - - } // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/hyprextras.cpp b/plugin/src/Caelestia/Internal/hyprextras.cpp -index 5308524d..c73b6e0b 100644 ---- a/plugin/src/Caelestia/Internal/hyprextras.cpp -+++ b/plugin/src/Caelestia/Internal/hyprextras.cpp -@@ -1,10 +1,14 @@ - #include "hyprextras.hpp" -+#include "hyprdevices.hpp" - - #include - #include - #include -+#include - #include - -+Q_LOGGING_CATEGORY(lcHypr, "caelestia.internal.hypr", QtInfoMsg) -+ - namespace caelestia::internal::hypr { - - HyprExtras::HyprExtras(QObject* parent) -@@ -16,8 +20,7 @@ HyprExtras::HyprExtras(QObject* parent) - , m_devices(new HyprDevices(this)) { - const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); - if (his.isEmpty()) { -- qWarning() -- << "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; -+ qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; - return; - } - -@@ -26,8 +29,7 @@ HyprExtras::HyprExtras(QObject* parent) - hyprDir = "/tmp/hypr/" + his; - - if (!QDir(hyprDir).exists()) { -- qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " -- "Hyprland socket."; -+ qCWarning(lcHypr) << "Hyprland socket directory does not exist. Unable to connect to Hyprland socket."; - return; - } - } -@@ -62,7 +64,7 @@ void HyprExtras::message(const QString& message) { - - makeRequest(message, [](bool success, const QByteArray& res) { - if (!success) { -- qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); -+ qCWarning(lcHypr) << "message: request error:" << QString::fromUtf8(res); - } - }); - } -@@ -74,7 +76,7 @@ void HyprExtras::batchMessage(const QStringList& messages) { - - makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { - if (!success) { -- qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); -+ qCWarning(lcHypr) << "batchMessage: request error:" << QString::fromUtf8(res); - } - }); - } -@@ -84,16 +86,18 @@ void HyprExtras::applyOptions(const QVariantHash& options) { - return; - } - -- QString request = "[[BATCH]]"; -+ QString request; -+ request.reserve(12 + options.size() * 40); -+ request += QLatin1String("[[BATCH]]"); - for (auto it = options.constBegin(); it != options.constEnd(); ++it) { -- request += QString("keyword %1 %2;").arg(it.key(), it.value().toString()); -+ request += QLatin1String("keyword ") + it.key() + QLatin1Char(' ') + it.value().toString() + QLatin1Char(';'); - } - - makeRequest(request, [this](bool success, const QByteArray& res) { - if (success) { - refreshOptions(); - } else { -- qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); -+ qCWarning(lcHypr) << "applyOptions: request error" << QString::fromUtf8(res); - } - }); - } -@@ -143,15 +147,15 @@ void HyprExtras::refreshDevices() { - - void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { - if (!m_socketValid) { -- qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; -+ qCWarning(lcHypr) << "socketError: unable to connect to Hyprland event socket:" << error; - } else { -- qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; -+ qCWarning(lcHypr) << "socketError: Hyprland event socket error:" << error; - } - } - - void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { - if (state == QLocalSocket::UnconnectedState && m_socketValid) { -- qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; -+ qCWarning(lcHypr) << "socketStateChanged: Hyprland event socket disconnected."; - } - - m_socketValid = state == QLocalSocket::ConnectedState; -@@ -204,7 +208,7 @@ HyprExtras::SocketPtr HyprExtras::makeRequest( - }); - - QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { -- qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; -+ qCWarning(lcHypr) << "makeRequest: error making request:" << err << "| request:" << request; - callback(false, {}); - socket->close(); - }); -diff --git a/plugin/src/Caelestia/Internal/hyprextras.hpp b/plugin/src/Caelestia/Internal/hyprextras.hpp -index 14563c0a..48eceea9 100644 ---- a/plugin/src/Caelestia/Internal/hyprextras.hpp -+++ b/plugin/src/Caelestia/Internal/hyprextras.hpp -@@ -1,15 +1,19 @@ - #pragma once - --#include "hyprdevices.hpp" - #include - #include - #include -+#include -+#include - - namespace caelestia::internal::hypr { - -+class HyprDevices; -+ - class HyprExtras : public QObject { - Q_OBJECT - QML_ELEMENT -+ Q_MOC_INCLUDE("hyprdevices.hpp") - - Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) - Q_PROPERTY(caelestia::internal::hypr::HyprDevices* devices READ devices CONSTANT) -diff --git a/plugin/src/Caelestia/Internal/logindmanager.cpp b/plugin/src/Caelestia/Internal/logindmanager.cpp -index 4194ee1e..740005d9 100644 ---- a/plugin/src/Caelestia/Internal/logindmanager.cpp -+++ b/plugin/src/Caelestia/Internal/logindmanager.cpp -@@ -4,6 +4,9 @@ - #include - #include - #include -+#include -+ -+Q_LOGGING_CATEGORY(lcLogindManager, "caelestia.internal.logindmanager", QtInfoMsg) - - namespace caelestia::internal { - -@@ -11,7 +14,7 @@ LogindManager::LogindManager(QObject* parent) - : QObject(parent) { - auto bus = QDBusConnection::systemBus(); - if (!bus.isConnected()) { -- qWarning() << "LogindManager::LogindManager: failed to connect to system bus:" << bus.lastError().message(); -+ qCWarning(lcLogindManager) << "Failed to connect to system bus:" << bus.lastError().message(); - return; - } - -@@ -19,14 +22,13 @@ LogindManager::LogindManager(QObject* parent) - "PrepareForSleep", this, SLOT(handlePrepareForSleep(bool))); - - if (!ok) { -- qWarning() << "LogindManager::LogindManager: failed to connect to PrepareForSleep signal:" -- << bus.lastError().message(); -+ qCWarning(lcLogindManager) << "Failed to connect to PrepareForSleep signal:" << bus.lastError().message(); - } - - QDBusInterface login1("org.freedesktop.login1", "/org/freedesktop/login1", "org.freedesktop.login1.Manager", bus); - const QDBusReply reply = login1.call("GetSession", "auto"); - if (!reply.isValid()) { -- qWarning() << "LogindManager::LogindManager: failed to get session path"; -+ qCWarning(lcLogindManager) << "Failed to get session path"; - return; - } - const auto sessionPath = reply.value().path(); -@@ -35,14 +37,14 @@ LogindManager::LogindManager(QObject* parent) - SLOT(handleLockRequested())); - - if (!ok) { -- qWarning() << "LogindManager::LogindManager: failed to connect to Lock signal:" << bus.lastError().message(); -+ qCWarning(lcLogindManager) << "Failed to connect to Lock signal:" << bus.lastError().message(); - } - - ok = bus.connect("org.freedesktop.login1", sessionPath, "org.freedesktop.login1.Session", "Unlock", this, - SLOT(handleUnlockRequested())); - - if (!ok) { -- qWarning() << "LogindManager::LogindManager: failed to connect to Unlock signal:" << bus.lastError().message(); -+ qCWarning(lcLogindManager) << "Failed to connect to Unlock signal:" << bus.lastError().message(); - } - } - -diff --git a/plugin/src/Caelestia/Internal/sparklineitem.cpp b/plugin/src/Caelestia/Internal/sparklineitem.cpp -new file mode 100644 -index 00000000..4e6b0719 ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/sparklineitem.cpp -@@ -0,0 +1,216 @@ -+#include "sparklineitem.hpp" -+#include "circularbuffer.hpp" -+ -+#include -+#include -+#include -+ -+namespace caelestia::internal { -+ -+SparklineItem::SparklineItem(QQuickItem* parent) -+ : QQuickPaintedItem(parent) { -+ setAntialiasing(true); -+} -+ -+void SparklineItem::paint(QPainter* painter) { -+ const bool has1 = m_line1 && m_line1->count() >= 2; -+ const bool has2 = m_line2 && m_line2->count() >= 2; -+ if (!has1 && !has2) -+ return; -+ -+ painter->setRenderHint(QPainter::Antialiasing, true); -+ -+ // Draw line1 first (behind), then line2 (in front) -+ if (has1) -+ drawLine(painter, m_line1, m_line1Color, m_line1FillAlpha); -+ if (has2) -+ drawLine(painter, m_line2, m_line2Color, m_line2FillAlpha); -+} -+ -+void SparklineItem::drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha) { -+ if (m_historyLength < 2) -+ return; -+ -+ const qreal w = width(); -+ const qreal h = height(); -+ const int len = buffer->count(); -+ const qreal stepX = w / static_cast(m_historyLength - 1); -+ const qreal startX = w - (len - 1) * stepX - stepX * m_slideProgress + stepX; -+ -+ // Build line path -+ QPainterPath linePath; -+ linePath.moveTo(startX, h - (buffer->at(0) / m_maxValue) * h); -+ for (int i = 1; i < len; ++i) { -+ const qreal x = startX + i * stepX; -+ const qreal y = h - (buffer->at(i) / m_maxValue) * h; -+ linePath.lineTo(x, y); -+ } -+ -+ // Stroke the line -+ QPen pen(color, m_lineWidth); -+ pen.setCapStyle(Qt::RoundCap); -+ pen.setJoinStyle(Qt::RoundJoin); -+ painter->setPen(pen); -+ painter->setBrush(Qt::NoBrush); -+ painter->drawPath(linePath); -+ -+ // Fill under the line -+ QPainterPath fillPath = linePath; -+ fillPath.lineTo(startX + (len - 1) * stepX, h); -+ fillPath.lineTo(startX, h); -+ fillPath.closeSubpath(); -+ -+ QColor fillColor = color; -+ fillColor.setAlphaF(static_cast(fillAlpha)); -+ painter->setPen(Qt::NoPen); -+ painter->setBrush(fillColor); -+ painter->drawPath(fillPath); -+} -+ -+void SparklineItem::connectBuffer(CircularBuffer* buffer) { -+ if (!buffer) -+ return; -+ -+ connect(buffer, &CircularBuffer::valuesChanged, this, [this]() { -+ update(); -+ }); -+ connect(buffer, &QObject::destroyed, this, [this, buffer]() { -+ if (m_line1 == buffer) { -+ m_line1 = nullptr; -+ emit line1Changed(); -+ } -+ if (m_line2 == buffer) { -+ m_line2 = nullptr; -+ emit line2Changed(); -+ } -+ update(); -+ }); -+} -+ -+CircularBuffer* SparklineItem::line1() const { -+ return m_line1; -+} -+ -+void SparklineItem::setLine1(CircularBuffer* buffer) { -+ if (m_line1 == buffer) -+ return; -+ if (m_line1) -+ disconnect(m_line1, nullptr, this, nullptr); -+ m_line1 = buffer; -+ connectBuffer(buffer); -+ emit line1Changed(); -+ update(); -+} -+ -+CircularBuffer* SparklineItem::line2() const { -+ return m_line2; -+} -+ -+void SparklineItem::setLine2(CircularBuffer* buffer) { -+ if (m_line2 == buffer) -+ return; -+ if (m_line2) -+ disconnect(m_line2, nullptr, this, nullptr); -+ m_line2 = buffer; -+ connectBuffer(buffer); -+ emit line2Changed(); -+ update(); -+} -+ -+QColor SparklineItem::line1Color() const { -+ return m_line1Color; -+} -+ -+void SparklineItem::setLine1Color(const QColor& color) { -+ if (m_line1Color == color) -+ return; -+ m_line1Color = color; -+ emit line1ColorChanged(); -+ update(); -+} -+ -+QColor SparklineItem::line2Color() const { -+ return m_line2Color; -+} -+ -+void SparklineItem::setLine2Color(const QColor& color) { -+ if (m_line2Color == color) -+ return; -+ m_line2Color = color; -+ emit line2ColorChanged(); -+ update(); -+} -+ -+qreal SparklineItem::line1FillAlpha() const { -+ return m_line1FillAlpha; -+} -+ -+void SparklineItem::setLine1FillAlpha(qreal alpha) { -+ if (qFuzzyCompare(m_line1FillAlpha, alpha)) -+ return; -+ m_line1FillAlpha = alpha; -+ emit line1FillAlphaChanged(); -+ update(); -+} -+ -+qreal SparklineItem::line2FillAlpha() const { -+ return m_line2FillAlpha; -+} -+ -+void SparklineItem::setLine2FillAlpha(qreal alpha) { -+ if (qFuzzyCompare(m_line2FillAlpha, alpha)) -+ return; -+ m_line2FillAlpha = alpha; -+ emit line2FillAlphaChanged(); -+ update(); -+} -+ -+qreal SparklineItem::maxValue() const { -+ return m_maxValue; -+} -+ -+void SparklineItem::setMaxValue(qreal value) { -+ if (qFuzzyCompare(m_maxValue, value)) -+ return; -+ m_maxValue = value; -+ emit maxValueChanged(); -+ update(); -+} -+ -+qreal SparklineItem::slideProgress() const { -+ return m_slideProgress; -+} -+ -+void SparklineItem::setSlideProgress(qreal progress) { -+ if (qFuzzyCompare(m_slideProgress, progress)) -+ return; -+ m_slideProgress = progress; -+ emit slideProgressChanged(); -+ update(); -+} -+ -+int SparklineItem::historyLength() const { -+ return m_historyLength; -+} -+ -+void SparklineItem::setHistoryLength(int length) { -+ if (m_historyLength == length) -+ return; -+ m_historyLength = length; -+ emit historyLengthChanged(); -+ update(); -+} -+ -+qreal SparklineItem::lineWidth() const { -+ return m_lineWidth; -+} -+ -+void SparklineItem::setLineWidth(qreal width) { -+ if (qFuzzyCompare(m_lineWidth, width)) -+ return; -+ m_lineWidth = width; -+ emit lineWidthChanged(); -+ update(); -+} -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/sparklineitem.hpp b/plugin/src/Caelestia/Internal/sparklineitem.hpp -new file mode 100644 -index 00000000..22632a90 ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/sparklineitem.hpp -@@ -0,0 +1,91 @@ -+#pragma once -+ -+#include -+#include -+#include -+#include -+ -+namespace caelestia::internal { -+ -+class CircularBuffer; -+ -+class SparklineItem : public QQuickPaintedItem { -+ Q_OBJECT -+ QML_ELEMENT -+ Q_MOC_INCLUDE("circularbuffer.hpp") -+ -+ Q_PROPERTY(CircularBuffer* line1 READ line1 WRITE setLine1 NOTIFY line1Changed) -+ Q_PROPERTY(CircularBuffer* line2 READ line2 WRITE setLine2 NOTIFY line2Changed) -+ Q_PROPERTY(QColor line1Color READ line1Color WRITE setLine1Color NOTIFY line1ColorChanged) -+ Q_PROPERTY(QColor line2Color READ line2Color WRITE setLine2Color NOTIFY line2ColorChanged) -+ Q_PROPERTY(qreal line1FillAlpha READ line1FillAlpha WRITE setLine1FillAlpha NOTIFY line1FillAlphaChanged) -+ Q_PROPERTY(qreal line2FillAlpha READ line2FillAlpha WRITE setLine2FillAlpha NOTIFY line2FillAlphaChanged) -+ Q_PROPERTY(qreal maxValue READ maxValue WRITE setMaxValue NOTIFY maxValueChanged) -+ Q_PROPERTY(qreal slideProgress READ slideProgress WRITE setSlideProgress NOTIFY slideProgressChanged) -+ Q_PROPERTY(int historyLength READ historyLength WRITE setHistoryLength NOTIFY historyLengthChanged) -+ Q_PROPERTY(qreal lineWidth READ lineWidth WRITE setLineWidth NOTIFY lineWidthChanged) -+ -+public: -+ explicit SparklineItem(QQuickItem* parent = nullptr); -+ -+ void paint(QPainter* painter) override; -+ -+ [[nodiscard]] CircularBuffer* line1() const; -+ void setLine1(CircularBuffer* buffer); -+ -+ [[nodiscard]] CircularBuffer* line2() const; -+ void setLine2(CircularBuffer* buffer); -+ -+ [[nodiscard]] QColor line1Color() const; -+ void setLine1Color(const QColor& color); -+ -+ [[nodiscard]] QColor line2Color() const; -+ void setLine2Color(const QColor& color); -+ -+ [[nodiscard]] qreal line1FillAlpha() const; -+ void setLine1FillAlpha(qreal alpha); -+ -+ [[nodiscard]] qreal line2FillAlpha() const; -+ void setLine2FillAlpha(qreal alpha); -+ -+ [[nodiscard]] qreal maxValue() const; -+ void setMaxValue(qreal value); -+ -+ [[nodiscard]] qreal slideProgress() const; -+ void setSlideProgress(qreal progress); -+ -+ [[nodiscard]] int historyLength() const; -+ void setHistoryLength(int length); -+ -+ [[nodiscard]] qreal lineWidth() const; -+ void setLineWidth(qreal width); -+ -+signals: -+ void line1Changed(); -+ void line2Changed(); -+ void line1ColorChanged(); -+ void line2ColorChanged(); -+ void line1FillAlphaChanged(); -+ void line2FillAlphaChanged(); -+ void maxValueChanged(); -+ void slideProgressChanged(); -+ void historyLengthChanged(); -+ void lineWidthChanged(); -+ -+private: -+ void drawLine(QPainter* painter, CircularBuffer* buffer, const QColor& color, qreal fillAlpha); -+ void connectBuffer(CircularBuffer* buffer); -+ -+ CircularBuffer* m_line1 = nullptr; -+ CircularBuffer* m_line2 = nullptr; -+ QColor m_line1Color; -+ QColor m_line2Color; -+ qreal m_line1FillAlpha = 0.15; -+ qreal m_line2FillAlpha = 0.2; -+ qreal m_maxValue = 1024.0; -+ qreal m_slideProgress = 0.0; -+ int m_historyLength = 30; -+ qreal m_lineWidth = 2.0; -+}; -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/visualiserbars.cpp b/plugin/src/Caelestia/Internal/visualiserbars.cpp -new file mode 100644 -index 00000000..926468b0 ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/visualiserbars.cpp -@@ -0,0 +1,198 @@ -+#include "visualiserbars.hpp" -+ -+#include -+#include -+#include -+#include -+#include -+#include -+ -+namespace caelestia::internal { -+ -+VisualiserBars::VisualiserBars(QQuickItem* parent) -+ : QQuickPaintedItem(parent) { -+ setAntialiasing(true); -+} -+ -+void VisualiserBars::advance(qreal dt) { -+ if (m_displayValues.isEmpty() || m_settled) -+ return; -+ -+ // dt is in seconds (from FrameAnimation.frameTime), convert to ms -+ const qreal dtMs = dt * 1000.0; -+ const qreal tau = m_animationDuration / 3.0; -+ const qreal alpha = 1.0 - std::exp(-dtMs / tau); -+ -+ bool allSettled = true; -+ -+ for (qsizetype i = 0; i < m_displayValues.size(); ++i) { -+ const double diff = m_targetValues[i] - m_displayValues[i]; -+ -+ if (std::abs(diff) > 0.001) { -+ m_displayValues[i] += diff * alpha; -+ allSettled = false; -+ } else { -+ m_displayValues[i] = m_targetValues[i]; -+ } -+ } -+ -+ update(); -+ -+ if (allSettled && !m_settled) { -+ m_settled = true; -+ emit settledChanged(); -+ } -+} -+ -+void VisualiserBars::paint(QPainter* painter) { -+ if (m_displayValues.isEmpty()) -+ return; -+ -+ painter->setRenderHint(QPainter::Antialiasing, true); -+ painter->setPen(Qt::NoPen); -+ -+ const qreal h = height(); -+ const qreal maxBarHeight = h * 0.4; -+ -+ QLinearGradient gradient(0, h - maxBarHeight, 0, h); -+ gradient.setColorAt(0, m_primaryColor); -+ gradient.setColorAt(1, m_secondaryColor); -+ painter->setBrush(gradient); -+ -+ drawSide(painter, false); -+ drawSide(painter, true); -+} -+ -+void VisualiserBars::drawSide(QPainter* painter, bool rightSide) { -+ const qreal w = width(); -+ const qreal h = height(); -+ const auto count = m_displayValues.size(); -+ -+ if (count == 0) -+ return; -+ -+ const qreal sideWidth = w * 0.4; -+ const qreal slotWidth = sideWidth / static_cast(count); -+ const qreal barWidth = slotWidth - m_spacing; -+ -+ if (barWidth <= 0) -+ return; -+ -+ const qreal sideOffset = rightSide ? w * 0.6 : 0; -+ const qreal maxBarHeight = h * 0.4; -+ -+ for (qsizetype i = 0; i < count; ++i) { -+ const qsizetype valueIndex = rightSide ? i : (count - i - 1); -+ const qreal value = std::clamp(m_displayValues[valueIndex], 0.0, 1.0); -+ const qreal barHeight = value * maxBarHeight; -+ -+ if (barHeight <= 0) -+ continue; -+ -+ const qreal x = static_cast(i) * slotWidth + sideOffset; -+ const qreal y = h - barHeight; -+ const qreal r = std::min({ m_rounding, barWidth / 2.0, barHeight }); -+ -+ QPainterPath path; -+ path.moveTo(x, h); -+ path.lineTo(x, y + r); -+ -+ if (r > 0) { -+ path.arcTo(x, y, r * 2, r * 2, 180, -90); -+ path.lineTo(x + barWidth - r, y); -+ path.arcTo(x + barWidth - r * 2, y, r * 2, r * 2, 90, -90); -+ } else { -+ path.lineTo(x, y); -+ path.lineTo(x + barWidth, y); -+ } -+ -+ path.lineTo(x + barWidth, h); -+ path.closeSubpath(); -+ -+ painter->drawPath(path); -+ } -+} -+ -+QVector VisualiserBars::values() const { -+ return m_targetValues; -+} -+ -+void VisualiserBars::setValues(const QVector& values) { -+ m_targetValues = values; -+ -+ if (m_displayValues.size() != values.size()) { -+ m_displayValues.resize(values.size(), 0.0); -+ } -+ -+ if (m_settled) { -+ m_settled = false; -+ emit settledChanged(); -+ } -+ -+ emit valuesChanged(); -+} -+ -+bool VisualiserBars::settled() const { -+ return m_settled; -+} -+ -+QColor VisualiserBars::primaryColor() const { -+ return m_primaryColor; -+} -+ -+void VisualiserBars::setPrimaryColor(const QColor& color) { -+ if (m_primaryColor == color) -+ return; -+ m_primaryColor = color; -+ emit primaryColorChanged(); -+ update(); -+} -+ -+QColor VisualiserBars::secondaryColor() const { -+ return m_secondaryColor; -+} -+ -+void VisualiserBars::setSecondaryColor(const QColor& color) { -+ if (m_secondaryColor == color) -+ return; -+ m_secondaryColor = color; -+ emit secondaryColorChanged(); -+ update(); -+} -+ -+qreal VisualiserBars::rounding() const { -+ return m_rounding; -+} -+ -+void VisualiserBars::setRounding(qreal rounding) { -+ if (qFuzzyCompare(m_rounding, rounding)) -+ return; -+ m_rounding = rounding; -+ emit roundingChanged(); -+ update(); -+} -+ -+qreal VisualiserBars::spacing() const { -+ return m_spacing; -+} -+ -+void VisualiserBars::setSpacing(qreal spacing) { -+ if (qFuzzyCompare(m_spacing, spacing)) -+ return; -+ m_spacing = spacing; -+ emit spacingChanged(); -+ update(); -+} -+ -+int VisualiserBars::animationDuration() const { -+ return m_animationDuration; -+} -+ -+void VisualiserBars::setAnimationDuration(int duration) { -+ if (m_animationDuration == duration) -+ return; -+ m_animationDuration = duration; -+ emit animationDurationChanged(); -+} -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Internal/visualiserbars.hpp b/plugin/src/Caelestia/Internal/visualiserbars.hpp -new file mode 100644 -index 00000000..95c07124 ---- /dev/null -+++ b/plugin/src/Caelestia/Internal/visualiserbars.hpp -@@ -0,0 +1,72 @@ -+#pragma once -+ -+#include -+#include -+#include -+#include -+#include -+ -+namespace caelestia::internal { -+ -+class VisualiserBars : public QQuickPaintedItem { -+ Q_OBJECT -+ QML_ELEMENT -+ -+ Q_PROPERTY(QVector values READ values WRITE setValues NOTIFY valuesChanged) -+ Q_PROPERTY(QColor primaryColor READ primaryColor WRITE setPrimaryColor NOTIFY primaryColorChanged) -+ Q_PROPERTY(QColor secondaryColor READ secondaryColor WRITE setSecondaryColor NOTIFY secondaryColorChanged) -+ Q_PROPERTY(qreal rounding READ rounding WRITE setRounding NOTIFY roundingChanged) -+ Q_PROPERTY(qreal spacing READ spacing WRITE setSpacing NOTIFY spacingChanged) -+ Q_PROPERTY(int animationDuration READ animationDuration WRITE setAnimationDuration NOTIFY animationDurationChanged) -+ Q_PROPERTY(bool settled READ settled NOTIFY settledChanged) -+ -+public: -+ explicit VisualiserBars(QQuickItem* parent = nullptr); -+ -+ void paint(QPainter* painter) override; -+ -+ Q_INVOKABLE void advance(qreal dt); -+ -+ [[nodiscard]] QVector values() const; -+ void setValues(const QVector& values); -+ -+ [[nodiscard]] QColor primaryColor() const; -+ void setPrimaryColor(const QColor& color); -+ -+ [[nodiscard]] QColor secondaryColor() const; -+ void setSecondaryColor(const QColor& color); -+ -+ [[nodiscard]] qreal rounding() const; -+ void setRounding(qreal rounding); -+ -+ [[nodiscard]] qreal spacing() const; -+ void setSpacing(qreal spacing); -+ -+ [[nodiscard]] int animationDuration() const; -+ void setAnimationDuration(int duration); -+ -+ [[nodiscard]] bool settled() const; -+ -+signals: -+ void valuesChanged(); -+ void primaryColorChanged(); -+ void secondaryColorChanged(); -+ void roundingChanged(); -+ void spacingChanged(); -+ void animationDurationChanged(); -+ void settledChanged(); -+ -+private: -+ void drawSide(QPainter* painter, bool rightSide); -+ -+ QVector m_targetValues; -+ QVector m_displayValues; -+ QColor m_primaryColor; -+ QColor m_secondaryColor; -+ qreal m_rounding = 0.0; -+ qreal m_spacing = 0.0; -+ int m_animationDuration = 200; -+ bool m_settled = true; -+}; -+ -+} // namespace caelestia::internal -diff --git a/plugin/src/Caelestia/Models/filesystemmodel.cpp b/plugin/src/Caelestia/Models/filesystemmodel.cpp -index e387ecd0..267a4394 100644 ---- a/plugin/src/Caelestia/Models/filesystemmodel.cpp -+++ b/plugin/src/Caelestia/Models/filesystemmodel.cpp -@@ -57,8 +57,8 @@ bool FileSystemEntry::isImage() const { - - QString FileSystemEntry::mimeType() const { - if (!m_mimeTypeInitialised) { -- const QMimeDatabase db; -- m_mimeType = db.mimeTypeForFile(m_path).name(); -+ static const QMimeDatabase s_db; -+ m_mimeType = s_db.mimeTypeForFile(m_path).name(); - m_mimeTypeInitialised = true; - } - return m_mimeType; -@@ -219,7 +219,7 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { - if (m_recursive && m_watchChanges) { - const auto currentDir = m_dir; - const bool showHidden = m_showHidden; -- const auto future = QtConcurrent::run([showHidden, path]() { -+ auto future = QtConcurrent::run([showHidden, path]() { - QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; - if (showHidden) { - filters |= QDir::Hidden; -@@ -232,16 +232,12 @@ void FileSystemModel::watchDirIfRecursive(const QString& path) { - } - return dirs; - }); -- const auto watcher = new QFutureWatcher(this); -- connect(watcher, &QFutureWatcher::finished, this, [currentDir, showHidden, watcher, this]() { -- const auto paths = watcher->result(); -+ future.then(this, [currentDir, showHidden, this](const QStringList& paths) { - if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { - // Ignore if dir or showHidden has changed - m_watcher.addPaths(paths); - } -- watcher->deleteLater(); - }); -- watcher->setFuture(future); - } - } - -@@ -295,7 +291,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { - oldPaths << entry->path(); - } - -- const auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { -+ auto future = QtConcurrent::run([=](QPromise, QSet>>& promise) { - const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; - - std::optional iter; -@@ -353,7 +349,7 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { - newPaths.insert(path); - } - -- if (promise.isCanceled() || newPaths == oldPaths) { -+ if (promise.isCanceled()) { - return; - } - -@@ -365,23 +361,17 @@ void FileSystemModel::updateEntriesForDir(const QString& dir) { - } - m_futures.insert(dir, future); - -- const auto watcher = new QFutureWatcher, QSet>>(this); -- -- connect(watcher, &QFutureWatcher, QSet>>::finished, this, [dir, watcher, this]() { -- m_futures.remove(dir); -- -- if (!watcher->future().isResultReadyAt(0)) { -- watcher->deleteLater(); -- return; -- } -- -- const auto result = watcher->result(); -- applyChanges(result.first, result.second); -- -- watcher->deleteLater(); -- }); -- -- watcher->setFuture(future); -+ future -+ .then(this, -+ [dir, this](QPair, QSet> result) { -+ m_futures.remove(dir); -+ if (!result.first.isEmpty() || !result.second.isEmpty()) { -+ applyChanges(result.first, result.second); -+ } -+ }) -+ .onCanceled(this, [dir, this]() { -+ m_futures.remove(dir); -+ }); - } - - void FileSystemModel::applyChanges(const QSet& removedPaths, const QSet& addedPaths) { -diff --git a/plugin/src/Caelestia/Models/filesystemmodel.hpp b/plugin/src/Caelestia/Models/filesystemmodel.hpp -index cf8eae82..c3315858 100644 ---- a/plugin/src/Caelestia/Models/filesystemmodel.hpp -+++ b/plugin/src/Caelestia/Models/filesystemmodel.hpp -@@ -132,7 +132,7 @@ private: - bool m_recursive; - bool m_watchChanges; - bool m_showHidden; -- bool m_sortReverse; -+ bool m_sortReverse = false; - Filter m_filter; - QStringList m_nameFilters; - -diff --git a/plugin/src/Caelestia/Services/audiocollector.cpp b/plugin/src/Caelestia/Services/audiocollector.cpp -index 15634059..69309f75 100644 ---- a/plugin/src/Caelestia/Services/audiocollector.cpp -+++ b/plugin/src/Caelestia/Services/audiocollector.cpp -@@ -3,13 +3,16 @@ - #include "service.hpp" - #include - #include --#include -+#include - #include - #include - #include - #include - #include - -+Q_LOGGING_CATEGORY(lcAc, "caelestia.services.ac", QtInfoMsg) -+Q_LOGGING_CATEGORY(lcAcWorker, "caelestia.services.ac.worker", QtInfoMsg) -+ - namespace caelestia::services { - - PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) -@@ -23,13 +26,19 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) - - m_loop = pw_main_loop_new(nullptr); - if (!m_loop) { -- qWarning() << "PipeWireWorker::init: failed to create PipeWire main loop"; -+ qCWarning(lcAcWorker) << "init: failed to create PipeWire main loop"; - pw_deinit(); - return; - } - - timespec timeout = { 0, 10 * SPA_NSEC_PER_MSEC }; - m_timer = pw_loop_add_timer(pw_main_loop_get_loop(m_loop), handleTimeout, this); -+ if (!m_timer) { -+ qCWarning(lcAcWorker) << "init: failed to create timer"; -+ pw_main_loop_destroy(m_loop); -+ pw_deinit(); -+ return; -+ } - pw_loop_update_timer(pw_main_loop_get_loop(m_loop), m_timer, &timeout, &timeout, false); - - auto props = pw_properties_new( -@@ -55,6 +64,7 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) - params[0] = spa_format_audio_raw_build(&b, SPA_PARAM_EnumFormat, &info); - - pw_stream_events events{}; -+ events.version = PW_VERSION_STREAM_EVENTS; - events.state_changed = [](void* data, pw_stream_state, pw_stream_state state, const char*) { - auto* self = static_cast(data); - self->streamStateChanged(state); -@@ -65,13 +75,19 @@ PipeWireWorker::PipeWireWorker(std::stop_token token, AudioCollector* collector) - }; - - m_stream = pw_stream_new_simple(pw_main_loop_get_loop(m_loop), "caelestia-shell", props, &events, this); -+ if (!m_stream) { -+ qCWarning(lcAcWorker) << "init: failed to create stream"; -+ pw_main_loop_destroy(m_loop); -+ pw_deinit(); -+ return; -+ } - - const int success = pw_stream_connect(m_stream, PW_DIRECTION_INPUT, PW_ID_ANY, - static_cast( - PW_STREAM_FLAG_AUTOCONNECT | PW_STREAM_FLAG_MAP_BUFFERS | PW_STREAM_FLAG_RT_PROCESS), - params, 1); - if (success < 0) { -- qWarning() << "PipeWireWorker::init: failed to connect stream"; -+ qCWarning(lcAcWorker) << "init: failed to connect stream"; - pw_stream_destroy(m_stream); - pw_main_loop_destroy(m_loop); - pw_deinit(); -@@ -221,7 +237,7 @@ AudioCollector::AudioCollector(QObject* parent) - , m_writeBuffer(&m_buffer2) {} - - AudioCollector::~AudioCollector() { -- stop(); -+ AudioCollector::stop(); - } - - void AudioCollector::start() { -diff --git a/plugin/src/Caelestia/Services/audioprovider.cpp b/plugin/src/Caelestia/Services/audioprovider.cpp -index 1fac9eea..d2916f45 100644 ---- a/plugin/src/Caelestia/Services/audioprovider.cpp -+++ b/plugin/src/Caelestia/Services/audioprovider.cpp -@@ -2,9 +2,12 @@ - - #include "audiocollector.hpp" - #include "service.hpp" --#include -+#include - #include - -+Q_LOGGING_CATEGORY(lcAp, "caelestia.services.ap", QtInfoMsg) -+Q_LOGGING_CATEGORY(lcApProcessor, "caelestia.services.ap.processor", QtInfoMsg) -+ - namespace caelestia::services { - - AudioProcessor::AudioProcessor(QObject* parent) -@@ -48,7 +51,7 @@ AudioProvider::~AudioProvider() { - - void AudioProvider::init() { - if (!m_processor) { -- qWarning() << "AudioProvider::init: attempted to init with no processor set"; -+ qCWarning(lcAp) << "init: attempted to init with no processor set"; - return; - } - -diff --git a/plugin/src/Caelestia/Services/audioprovider.hpp b/plugin/src/Caelestia/Services/audioprovider.hpp -index 5bf9bb00..4b85929f 100644 ---- a/plugin/src/Caelestia/Services/audioprovider.hpp -+++ b/plugin/src/Caelestia/Services/audioprovider.hpp -@@ -23,7 +23,7 @@ protected: - virtual void process() = 0; - - private: -- QTimer* m_timer; -+ QTimer* m_timer = nullptr; - }; - - class AudioProvider : public Service { -diff --git a/plugin/src/Caelestia/Services/beattracker.cpp b/plugin/src/Caelestia/Services/beattracker.cpp -index 93addc67..64970579 100644 ---- a/plugin/src/Caelestia/Services/beattracker.cpp -+++ b/plugin/src/Caelestia/Services/beattracker.cpp -@@ -19,7 +19,9 @@ BeatProcessor::~BeatProcessor() { - if (m_in) { - del_fvec(m_in); - } -- del_fvec(m_out); -+ if (m_out) { -+ del_fvec(m_out); -+ } - } - - void BeatProcessor::process() { -diff --git a/plugin/src/Caelestia/Services/cavaprovider.cpp b/plugin/src/Caelestia/Services/cavaprovider.cpp -index 7b6cc1f2..a57f4040 100644 ---- a/plugin/src/Caelestia/Services/cavaprovider.cpp -+++ b/plugin/src/Caelestia/Services/cavaprovider.cpp -@@ -4,7 +4,10 @@ - #include "audioprovider.hpp" - #include - #include --#include -+#include -+ -+Q_LOGGING_CATEGORY(lcCava, "caelestia.services.cava", QtInfoMsg) -+Q_LOGGING_CATEGORY(lcCavaProcessor, "caelestia.services.cava.processor", QtInfoMsg) - - namespace caelestia::services { - -@@ -57,7 +60,7 @@ void CavaProcessor::process() { - - void CavaProcessor::setBars(int bars) { - if (bars < 0) { -- qWarning() << "CavaProcessor::setBars: bars must be greater than 0. Setting to 0."; -+ qCWarning(lcCavaProcessor) << "setBars: bars must be greater than 0. Setting to 0."; - bars = 0; - } - -@@ -109,7 +112,7 @@ int CavaProvider::bars() const { - - void CavaProvider::setBars(int bars) { - if (bars < 0) { -- qWarning() << "CavaProvider::setBars: bars must be greater than 0. Setting to 0."; -+ qCWarning(lcCava) << "setBars: bars must be greater than 0. Setting to 0."; - bars = 0; - } - -diff --git a/plugin/src/Caelestia/Services/service.cpp b/plugin/src/Caelestia/Services/service.cpp -index bc215671..4e1921e8 100644 ---- a/plugin/src/Caelestia/Services/service.cpp -+++ b/plugin/src/Caelestia/Services/service.cpp -@@ -1,6 +1,5 @@ - #include "service.hpp" - --#include - #include - - namespace caelestia::services { -diff --git a/plugin/src/Caelestia/appdb.cpp b/plugin/src/Caelestia/appdb.cpp -index 6e37e16f..8d80ced9 100644 ---- a/plugin/src/Caelestia/appdb.cpp -+++ b/plugin/src/Caelestia/appdb.cpp -@@ -1,9 +1,12 @@ - #include "appdb.hpp" - -+#include - #include - #include - #include - -+Q_LOGGING_CATEGORY(lcAppDb, "caelestia.appdb", QtInfoMsg) -+ - namespace caelestia { - - AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) -@@ -11,7 +14,7 @@ AppEntry::AppEntry(QObject* entry, unsigned int frequency, QObject* parent) - , m_entry(entry) - , m_frequency(frequency) { - const auto mo = m_entry->metaObject(); -- const auto tmo = metaObject(); -+ const auto tmo = &AppEntry::staticMetaObject; - - for (const auto& prop : - { "name", "comment", "execString", "startupClass", "genericName", "categories", "keywords" }) { -@@ -162,6 +165,39 @@ void AppDb::setEntries(const QObjectList& entries) { - m_timer->start(); - } - -+QStringList AppDb::favouriteApps() const { -+ return m_favouriteApps; -+} -+ -+void AppDb::setFavouriteApps(const QStringList& favApps) { -+ if (m_favouriteApps == favApps) { -+ return; -+ } -+ -+ m_favouriteApps = favApps; -+ emit favouriteAppsChanged(); -+ m_favouriteAppsRegex.clear(); -+ m_favouriteAppsRegex.reserve(m_favouriteApps.size()); -+ for (const QString& item : std::as_const(m_favouriteApps)) { -+ const QRegularExpression re(regexifyString(item)); -+ if (re.isValid()) { -+ m_favouriteAppsRegex << re; -+ } else { -+ qCWarning(lcAppDb) << "setFavouriteApps: regular expression is not valid:" << re.pattern(); -+ } -+ } -+ -+ emit appsChanged(); -+} -+ -+QString AppDb::regexifyString(const QString& original) const { -+ if (original.startsWith('^') && original.endsWith('$')) -+ return original; -+ -+ const QString escaped = QRegularExpression::escape(original); -+ return QStringLiteral("^%1$").arg(escaped); -+} -+ - QQmlListProperty AppDb::apps() { - return QQmlListProperty(this, &getSortedApps()); - } -@@ -179,28 +215,48 @@ void AppDb::incrementFrequency(const QString& id) { - auto* app = m_apps.value(id); - if (app) { - const auto before = getSortedApps(); -- - app->incrementFrequency(); -- -- if (before != getSortedApps()) { -+ getSortedApps(); -+ if (before != m_sortedApps) { - emit appsChanged(); - } - } else { -- qWarning() << "AppDb::incrementFrequency: could not find app with id" << id; -+ qCWarning(lcAppDb) << "incrementFrequency: could not find app with id" << id; - } - } - - QList& AppDb::getSortedApps() const { - m_sortedApps = m_apps.values(); -- std::sort(m_sortedApps.begin(), m_sortedApps.end(), [](AppEntry* a, AppEntry* b) { -- if (a->frequency() != b->frequency()) { -+ -+ // Pre-compute favourite status to avoid repeated regex matching during sort -+ QSet favSet; -+ favSet.reserve(m_sortedApps.size()); -+ for (const auto* app : std::as_const(m_sortedApps)) { -+ if (isFavourite(app)) -+ favSet.insert(app->id()); -+ } -+ -+ std::sort(m_sortedApps.begin(), m_sortedApps.end(), [&favSet](AppEntry* a, AppEntry* b) { -+ const bool aIsFav = favSet.contains(a->id()); -+ const bool bIsFav = favSet.contains(b->id()); -+ if (aIsFav != bIsFav) -+ return aIsFav; -+ if (a->frequency() != b->frequency()) - return a->frequency() > b->frequency(); -- } - return a->name().localeAwareCompare(b->name()) < 0; - }); - return m_sortedApps; - } - -+bool AppDb::isFavourite(const AppEntry* app) const { -+ for (const QRegularExpression& re : m_favouriteAppsRegex) { -+ if (re.match(app->id()).hasMatch()) { -+ return true; -+ } -+ } -+ return false; -+} -+ - quint32 AppDb::getFrequency(const QString& id) const { - auto db = QSqlDatabase::database(m_uuid); - QSqlQuery query(db); -@@ -222,7 +278,8 @@ void AppDb::updateAppFrequencies() { - app->setFrequency(getFrequency(app->id())); - } - -- if (before != getSortedApps()) { -+ getSortedApps(); -+ if (before != m_sortedApps) { - emit appsChanged(); - } - } -@@ -249,11 +306,13 @@ void AppDb::updateApps() { - newIds.insert(entry->property("id").toString()); - } - -- for (auto it = m_apps.keyBegin(); it != m_apps.keyEnd(); ++it) { -- const auto& id = *it; -- if (!newIds.contains(id)) { -+ for (auto it = m_apps.begin(); it != m_apps.end();) { -+ if (!newIds.contains(it.key())) { - dirty = true; -- m_apps.take(id)->deleteLater(); -+ it.value()->deleteLater(); -+ it = m_apps.erase(it); -+ } else { -+ ++it; - } - } - -diff --git a/plugin/src/Caelestia/appdb.hpp b/plugin/src/Caelestia/appdb.hpp -index 5f9b9604..ce5f2706 100644 ---- a/plugin/src/Caelestia/appdb.hpp -+++ b/plugin/src/Caelestia/appdb.hpp -@@ -4,6 +4,7 @@ - #include - #include - #include -+#include - #include - - namespace caelestia { -@@ -66,6 +67,7 @@ class AppDb : public QObject { - Q_PROPERTY(QString uuid READ uuid CONSTANT) - Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged REQUIRED) - Q_PROPERTY(QObjectList entries READ entries WRITE setEntries NOTIFY entriesChanged REQUIRED) -+ Q_PROPERTY(QStringList favouriteApps READ favouriteApps WRITE setFavouriteApps NOTIFY favouriteAppsChanged REQUIRED) - Q_PROPERTY(QQmlListProperty apps READ apps NOTIFY appsChanged) - - public: -@@ -79,6 +81,9 @@ public: - [[nodiscard]] QObjectList entries() const; - void setEntries(const QObjectList& entries); - -+ [[nodiscard]] QStringList favouriteApps() const; -+ void setFavouriteApps(const QStringList& favApps); -+ - [[nodiscard]] QQmlListProperty apps(); - - Q_INVOKABLE void incrementFrequency(const QString& id); -@@ -86,6 +91,7 @@ public: - signals: - void pathChanged(); - void entriesChanged(); -+ void favouriteAppsChanged(); - void appsChanged(); - - private: -@@ -94,10 +100,14 @@ private: - const QString m_uuid; - QString m_path; - QObjectList m_entries; -+ QStringList m_favouriteApps; // unedited string list from qml -+ QList m_favouriteAppsRegex; // pre-regexified m_favouriteApps list - QHash m_apps; - mutable QList m_sortedApps; - -+ QString regexifyString(const QString& original) const; - QList& getSortedApps() const; -+ bool isFavourite(const AppEntry* app) const; - quint32 getFrequency(const QString& id) const; - void updateAppFrequencies(); - void updateApps(); -diff --git a/plugin/src/Caelestia/cutils.cpp b/plugin/src/Caelestia/cutils.cpp -index 6e3bfa99..28a0e178 100644 ---- a/plugin/src/Caelestia/cutils.cpp -+++ b/plugin/src/Caelestia/cutils.cpp -@@ -6,8 +6,11 @@ - #include - #include - #include -+#include - #include - -+Q_LOGGING_CATEGORY(lcCUtils, "caelestia.cutils", QtInfoMsg) -+ - namespace caelestia { - - void CUtils::saveItem(QQuickItem* target, const QUrl& path) { -@@ -32,17 +35,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q - - void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, QJSValue onSaved, QJSValue onFailed) { - if (!target) { -- qWarning() << "CUtils::saveItem: a target is required"; -+ qCWarning(lcCUtils) << "saveItem: a target is required"; - return; - } - - if (!path.isLocalFile()) { -- qWarning() << "CUtils::saveItem:" << path << "is not a local file"; -+ qCWarning(lcCUtils) << "saveItem:" << path << "is not a local file"; - return; - } - - if (!target->window()) { -- qWarning() << "CUtils::saveItem: unable to save target" << target << "without a window"; -+ qCWarning(lcCUtils) << "saveItem: unable to save target" << target << "without a window"; - return; - } - -@@ -75,13 +78,20 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q - QObject::connect(watcher, &QFutureWatcher::finished, this, [=]() { - if (watcher->result()) { - if (onSaved.isCallable()) { -- onSaved.call( -- { QJSValue(path.toLocalFile()), engine->toScriptValue(QVariant::fromValue(path)) }); -+ QJSValueList args = { QJSValue(path.toLocalFile()) }; -+ if (engine) { -+ args << engine->toScriptValue(QVariant::fromValue(path)); -+ } -+ onSaved.call(args); - } - } else { -- qWarning() << "CUtils::saveItem: failed to save" << path; -+ qCWarning(lcCUtils) << "saveItem: failed to save" << path; - if (onFailed.isCallable()) { -- onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); -+ if (engine) { -+ onFailed.call({ engine->toScriptValue(QVariant::fromValue(path)) }); -+ } else { -+ onFailed.call(); -+ } - } - } - watcher->deleteLater(); -@@ -92,17 +102,17 @@ void CUtils::saveItem(QQuickItem* target, const QUrl& path, const QRect& rect, Q - - bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) const { - if (!source.isLocalFile()) { -- qWarning() << "CUtils::copyFile: source" << source << "is not a local file"; -+ qCWarning(lcCUtils) << "copyFile: source" << source << "is not a local file"; - return false; - } - if (!target.isLocalFile()) { -- qWarning() << "CUtils::copyFile: target" << target << "is not a local file"; -+ qCWarning(lcCUtils) << "copyFile: target" << target << "is not a local file"; - return false; - } - - if (overwrite && QFile::exists(target.toLocalFile())) { - if (!QFile::remove(target.toLocalFile())) { -- qWarning() << "CUtils::copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); -+ qCWarning(lcCUtils) << "copyFile: overwrite was specified but failed to remove" << target.toLocalFile(); - return false; - } - } -@@ -112,7 +122,7 @@ bool CUtils::copyFile(const QUrl& source, const QUrl& target, bool overwrite) co - - bool CUtils::deleteFile(const QUrl& path) const { - if (!path.isLocalFile()) { -- qWarning() << "CUtils::deleteFile: path" << path << "is not a local file"; -+ qCWarning(lcCUtils) << "deleteFile: path" << path << "is not a local file"; - return false; - } - -@@ -121,7 +131,7 @@ bool CUtils::deleteFile(const QUrl& path) const { - - QString CUtils::toLocalFile(const QUrl& url) const { - if (!url.isLocalFile()) { -- qWarning() << "CUtils::toLocalFile: given url is not a local file" << url; -+ qCWarning(lcCUtils) << "toLocalFile: given url is not a local file" << url; - return QString(); - } - -diff --git a/plugin/src/Caelestia/imageanalyser.cpp b/plugin/src/Caelestia/imageanalyser.cpp -index 880b0785..7f3ba526 100644 ---- a/plugin/src/Caelestia/imageanalyser.cpp -+++ b/plugin/src/Caelestia/imageanalyser.cpp -@@ -4,8 +4,11 @@ - #include - #include - #include -+#include - #include - -+Q_LOGGING_CATEGORY(lcImageAnalyser, "caelestia.imageanalyser", QtInfoMsg) -+ - namespace caelestia { - - ImageAnalyser::ImageAnalyser(QObject* parent) -@@ -134,6 +137,11 @@ void ImageAnalyser::update() { - - if (m_sourceItem) { - const QSharedPointer grabResult = m_sourceItem->grabToImage(); -+ if (!grabResult) { -+ QObject::connect(m_sourceItem, &QQuickItem::windowChanged, this, &ImageAnalyser::requestUpdate, -+ Qt::SingleShotConnection); -+ return; -+ } - QObject::connect(grabResult.data(), &QQuickItemGrabResult::ready, this, [grabResult, this]() { - m_futureWatcher->setFuture(QtConcurrent::run(&ImageAnalyser::analyse, grabResult->image(), m_rescaleSize)); - }); -@@ -147,7 +155,7 @@ void ImageAnalyser::update() { - - void ImageAnalyser::analyse(QPromise& promise, const QImage& image, int rescaleSize) { - if (image.isNull()) { -- qWarning() << "ImageAnalyser::analyse: image is null"; -+ qCWarning(lcImageAnalyser) << "analyse: image is null"; - return; - } - -@@ -191,14 +199,14 @@ void ImageAnalyser::analyse(QPromise& promise, const QImage& imag - continue; - } - -- const quint32 mr = static_cast(pixel[0] & 0xF8); -+ const quint32 mr = static_cast(pixel[2] & 0xF8); - const quint32 mg = static_cast(pixel[1] & 0xF8); -- const quint32 mb = static_cast(pixel[2] & 0xF8); -+ const quint32 mb = static_cast(pixel[0] & 0xF8); - ++colours[(mr << 16) | (mg << 8) | mb]; - -- const qreal r = pixel[0] / 255.0; -+ const qreal r = pixel[2] / 255.0; - const qreal g = pixel[1] / 255.0; -- const qreal b = pixel[2] / 255.0; -+ const qreal b = pixel[0] / 255.0; - totalLuminance += std::sqrt(0.299 * r * r + 0.587 * g * g + 0.114 * b * b); - ++count; - } -diff --git a/plugin/src/Caelestia/imageanalyser.hpp b/plugin/src/Caelestia/imageanalyser.hpp -index bbea2b32..63fbf969 100644 ---- a/plugin/src/Caelestia/imageanalyser.hpp -+++ b/plugin/src/Caelestia/imageanalyser.hpp -@@ -4,6 +4,7 @@ - #include - #include - #include -+#include - #include - - namespace caelestia { -@@ -48,7 +49,7 @@ private: - QFutureWatcher* const m_futureWatcher; - - QString m_source; -- QQuickItem* m_sourceItem; -+ QPointer m_sourceItem; - int m_rescaleSize; - - QColor m_dominantColour; -diff --git a/plugin/src/Caelestia/qalculator.cpp b/plugin/src/Caelestia/qalculator.cpp -index 44e8d21e..c7242179 100644 ---- a/plugin/src/Caelestia/qalculator.cpp -+++ b/plugin/src/Caelestia/qalculator.cpp -@@ -1,13 +1,19 @@ - #include "qalculator.hpp" - - #include -+#include - - namespace caelestia { - -+QMutex Qalculator::s_calculatorMutex; -+ - Qalculator::Qalculator(QObject* parent) - : QObject(parent) { - if (!CALCULATOR) { -- new Calculator(); -+ // Calculator constructor sets the global `calculator` pointer (CALCULATOR macro), -+ // but we need to assign it to a var so compiler doesn't flag it as a leak -+ static const auto* const instance = new Calculator(); -+ Q_UNUSED(instance) - CALCULATOR->loadExchangeRates(); - CALCULATOR->loadGlobalDefinitions(); - CALCULATOR->loadLocalDefinitions(); -@@ -19,6 +25,8 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { - return QString(); - } - -+ QMutexLocker locker(&s_calculatorMutex); -+ - EvaluationOptions eo; - PrintOptions po; - -@@ -49,4 +57,92 @@ QString Qalculator::eval(const QString& expr, bool printExpr) const { - return QString::fromStdString(result); - } - -+void Qalculator::evalAsync(const QString& expr) { -+ const quint64 gen = ++m_generation; -+ -+ if (expr.isEmpty()) { -+ if (!m_result.isEmpty()) { -+ m_result.clear(); -+ emit resultChanged(); -+ } -+ if (!m_rawResult.isEmpty()) { -+ m_rawResult.clear(); -+ emit rawResultChanged(); -+ } -+ if (m_busy) { -+ m_busy = false; -+ emit busyChanged(); -+ } -+ return; -+ } -+ -+ if (!m_busy) { -+ m_busy = true; -+ emit busyChanged(); -+ } -+ -+ QtConcurrent::run([expr]() -> QPair { -+ QMutexLocker locker(&s_calculatorMutex); -+ -+ EvaluationOptions eo; -+ PrintOptions po; -+ -+ std::string parsed; -+ std::string result = CALCULATOR->calculateAndPrint( -+ CALCULATOR->unlocalizeExpression(expr.toStdString(), eo.parse_options), 100, eo, po, &parsed); -+ -+ std::string error; -+ while (CALCULATOR->message()) { -+ if (!CALCULATOR->message()->message().empty()) { -+ if (CALCULATOR->message()->type() == MESSAGE_ERROR) { -+ error += "error: "; -+ } else if (CALCULATOR->message()->type() == MESSAGE_WARNING) { -+ error += "warning: "; -+ } -+ error += CALCULATOR->message()->message(); -+ } -+ CALCULATOR->nextMessage(); -+ } -+ -+ if (!error.empty()) { -+ const QString errorStr = QString::fromStdString(error); -+ return { errorStr, errorStr }; -+ } -+ -+ const QString rawStr = QString::fromStdString(result); -+ return { QString("%1 = %2").arg(parsed).arg(result), rawStr }; -+ }).then(this, [this, gen](QPair result) { -+ if (gen != m_generation) { -+ return; -+ } -+ -+ const auto& [formatted, raw] = result; -+ -+ if (m_result != formatted) { -+ m_result = formatted; -+ emit resultChanged(); -+ } -+ if (m_rawResult != raw) { -+ m_rawResult = raw; -+ emit rawResultChanged(); -+ } -+ if (m_busy) { -+ m_busy = false; -+ emit busyChanged(); -+ } -+ }); -+} -+ -+QString Qalculator::result() const { -+ return m_result; -+} -+ -+QString Qalculator::rawResult() const { -+ return m_rawResult; -+} -+ -+bool Qalculator::busy() const { -+ return m_busy; -+} -+ - } // namespace caelestia -diff --git a/plugin/src/Caelestia/qalculator.hpp b/plugin/src/Caelestia/qalculator.hpp -index a07a8a2f..b2f5517f 100644 ---- a/plugin/src/Caelestia/qalculator.hpp -+++ b/plugin/src/Caelestia/qalculator.hpp -@@ -1,5 +1,6 @@ - #pragma once - -+#include - #include - #include - -@@ -10,10 +11,32 @@ class Qalculator : public QObject { - QML_ELEMENT - QML_SINGLETON - -+ Q_PROPERTY(QString result READ result NOTIFY resultChanged) -+ Q_PROPERTY(QString rawResult READ rawResult NOTIFY rawResultChanged) -+ Q_PROPERTY(bool busy READ busy NOTIFY busyChanged) -+ - public: - explicit Qalculator(QObject* parent = nullptr); - - Q_INVOKABLE QString eval(const QString& expr, bool printExpr = true) const; -+ Q_INVOKABLE void evalAsync(const QString& expr); -+ -+ [[nodiscard]] QString result() const; -+ [[nodiscard]] QString rawResult() const; -+ [[nodiscard]] bool busy() const; -+ -+signals: -+ void resultChanged(); -+ void rawResultChanged(); -+ void busyChanged(); -+ -+private: -+ static QMutex s_calculatorMutex; -+ -+ QString m_result; -+ QString m_rawResult; -+ bool m_busy = false; -+ quint64 m_generation = 0; - }; - - } // namespace caelestia -diff --git a/plugin/src/Caelestia/requests.cpp b/plugin/src/Caelestia/requests.cpp -index 2ceddb35..862dea7f 100644 ---- a/plugin/src/Caelestia/requests.cpp -+++ b/plugin/src/Caelestia/requests.cpp -@@ -1,22 +1,41 @@ - #include "requests.hpp" - -+#include -+#include - #include -+#include - #include - #include - -+Q_LOGGING_CATEGORY(lcRequests, "caelestia.requests", QtInfoMsg) -+ - namespace caelestia { - - Requests::Requests(QObject* parent) - : QObject(parent) - , m_manager(new QNetworkAccessManager(this)) {} - --void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const { -+void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError, QJSValue headers) const { - if (!onSuccess.isCallable()) { -- qWarning() << "Requests::get: onSuccess is not callable"; -+ qCWarning(lcRequests) << "get: onSuccess is not callable"; - return; - } - - QNetworkRequest request(url); -+ request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork); -+ request.setAttribute(QNetworkRequest::CookieSaveControlAttribute, QNetworkRequest::Manual); -+ request.setRawHeader("Cache-Control", "no-cache, no-store"); -+ request.setRawHeader("Pragma", "no-cache"); -+ request.setRawHeader("Connection", "close"); -+ -+ if (headers.isObject()) { -+ QJSValueIterator it(headers); -+ while (it.hasNext()) { -+ it.next(); -+ request.setRawHeader(it.name().toUtf8(), it.value().toString().toUtf8()); -+ } -+ } -+ - auto reply = m_manager->get(request); - - QObject::connect(reply, &QNetworkReply::finished, [reply, onSuccess, onError]() { -@@ -25,11 +44,15 @@ void Requests::get(const QUrl& url, QJSValue onSuccess, QJSValue onError) const - } else if (onError.isCallable()) { - onError.call({ reply->errorString() }); - } else { -- qWarning() << "Requests::get: request failed with error" << reply->errorString(); -+ qCWarning(lcRequests) << "get: request failed with error" << reply->errorString(); - } - - reply->deleteLater(); - }); - } - -+void Requests::resetCookies() const { -+ m_manager->setCookieJar(new QNetworkCookieJar(m_manager)); -+} -+ - } // namespace caelestia -diff --git a/plugin/src/Caelestia/requests.hpp b/plugin/src/Caelestia/requests.hpp -index 1db2f4cf..d07d7e8f 100644 ---- a/plugin/src/Caelestia/requests.hpp -+++ b/plugin/src/Caelestia/requests.hpp -@@ -14,7 +14,9 @@ class Requests : public QObject { - public: - explicit Requests(QObject* parent = nullptr); - -- Q_INVOKABLE void get(const QUrl& url, QJSValue callback, QJSValue onError = QJSValue()) const; -+ Q_INVOKABLE void get( -+ const QUrl& url, QJSValue callback, QJSValue onError = QJSValue(), QJSValue headers = QJSValue()) const; -+ Q_INVOKABLE void resetCookies() const; - - private: - QNetworkAccessManager* m_manager; -diff --git a/plugin/src/Caelestia/toaster.cpp b/plugin/src/Caelestia/toaster.cpp -index 978805de..b51c77f8 100644 ---- a/plugin/src/Caelestia/toaster.cpp -+++ b/plugin/src/Caelestia/toaster.cpp -@@ -1,6 +1,5 @@ - #include "toaster.hpp" - --#include - #include - #include - -diff --git a/scripts/qml-lint-conventions.py b/scripts/qml-lint-conventions.py -new file mode 100755 -index 00000000..cef46657 ---- /dev/null -+++ b/scripts/qml-lint-conventions.py -@@ -0,0 +1,648 @@ -+#!/usr/bin/env python3 -+"""Checks QML files for Qt coding convention violations. -+ -+https://doc.qt.io/qt-6/qml-codingconventions.html -+ -+Required ordering within each QML object (with blank line between sections): -+ 1. id -+ 2. property declarations -+ 3. signal declarations -+ 4. JavaScript functions -+ 5. object properties (bindings) -+ 6. child objects -+ 7. component definitions -+""" -+ -+import re -+import sys -+from enum import IntEnum -+from pathlib import Path -+ -+RED = "\033[0;31m" -+YELLOW = "\033[0;33m" -+CYAN = "\033[0;36m" -+GREEN = "\033[0;32m" -+MAGENTA = "\033[0;35m" -+BOLD = "\033[1m" -+RESET = "\033[0m" -+ -+REPO_ROOT = Path(__file__).resolve().parent.parent -+ -+ -+class Section(IntEnum): -+ ID = 0 -+ PROPERTY = 1 -+ SIGNAL = 2 -+ FUNCTION = 3 -+ BINDING = 4 -+ CHILD = 5 -+ COMPONENT_DEF = 6 -+ -+ -+SECTION_NAMES = { -+ Section.ID: "id", -+ Section.PROPERTY: "property declarations", -+ Section.SIGNAL: "signal declarations", -+ Section.FUNCTION: "functions", -+ Section.BINDING: "bindings", -+ Section.CHILD: "child objects", -+ Section.COMPONENT_DEF: "component definitions", -+} -+ -+RULE_COLOURS = { -+ "file-structure": RED, -+ "import-order": GREEN, -+ "section-order": YELLOW, -+ "missing-section-separator": CYAN, -+ "blank-after-open-brace": MAGENTA, -+ "blank-before-close-brace": MAGENTA, -+} -+ -+IMPORT_RE = re.compile(r"^import\s+(\S+)") -+ -+ -+def import_group(module: str) -> tuple[int, int] | None: -+ """Return (group, depth) for a module import, or None to skip.""" -+ if module.startswith('"'): -+ return None -+ depth = module.count(".") + 1 -+ if module == "QtQuick" or module.startswith("QtQuick."): -+ return (1, depth) -+ if module.startswith("Qt"): -+ return (2, depth) -+ if module == "Quickshell" or module.startswith("Quickshell."): -+ return (3, depth) -+ if module == "M3Shapes": -+ return (4, depth) -+ if module == "Caelestia" or module.startswith("Caelestia."): -+ return (5, depth) -+ if module == "qs.components" or module.startswith("qs.components."): -+ return (6, depth) -+ if module == "qs.services": -+ return (7, depth) -+ if module == "qs.config": -+ return (8, depth) -+ if module == "qs.utils": -+ return (9, depth) -+ if module == "qs.modules" or module.startswith("qs.modules."): -+ return (10, depth) -+ return None -+ -+ -+def parse_imports(lines: list[str]) -> tuple[int | None, int | None, list[str], list[tuple[str, int, int, str]]]: -+ """Parse the import block, returning (first_idx, last_idx, relative_imports, module_imports). -+ -+ module_imports entries are (line_text, group, depth, module_name). -+ """ -+ first_import = None -+ last_import = None -+ relative_imports: list[str] = [] -+ module_imports: list[tuple[str, int, int, str]] = [] -+ -+ for i, line in enumerate(lines): -+ stripped = line.strip() -+ if not stripped or stripped.startswith("//") or stripped.startswith("pragma "): -+ continue -+ m = IMPORT_RE.match(stripped) -+ if m: -+ if first_import is None: -+ first_import = i -+ last_import = i -+ module = m.group(1) -+ result = import_group(module) -+ if result is None: -+ relative_imports.append(line) -+ else: -+ group, depth = result -+ module_imports.append((line, group, depth, module)) -+ continue -+ break -+ -+ return first_import, last_import, relative_imports, module_imports -+ -+ -+def check_imports(filepath: Path, lines: list[str], rel: str) -> list[Violation]: -+ """Check that module imports are in the required order.""" -+ violations = [] -+ _, _, _, module_imports = parse_imports(lines) -+ imports = [(i, *entry) for i, entry in enumerate(module_imports)] -+ -+ for j in range(1, len(imports)): -+ _, prev_line, prev_group, prev_depth, prev_mod = imports[j - 1] -+ _, curr_line, curr_group, curr_depth, curr_mod = imports[j] -+ -+ # Find actual line number for the current import -+ lineno = 0 -+ count = 0 -+ for li, line in enumerate(lines): -+ stripped = line.strip() -+ m = IMPORT_RE.match(stripped) -+ if m and import_group(m.group(1)) is not None: -+ if count == j: -+ lineno = li + 1 -+ break -+ count += 1 -+ -+ if curr_group < prev_group: -+ violations.append( -+ Violation( -+ rel, -+ lineno, -+ "import-order", -+ f"'{curr_mod}' should appear before '{prev_mod}'", -+ ) -+ ) -+ elif curr_group == prev_group and curr_depth < prev_depth: -+ violations.append( -+ Violation( -+ rel, -+ lineno, -+ "import-order", -+ f"'{curr_mod}' should appear before '{prev_mod}' (less nested first)", -+ ) -+ ) -+ -+ return violations -+ -+ -+def fix_imports(lines: list[str]) -> list[str]: -+ """Sort imports and return the modified lines.""" -+ first, last, relative, module = parse_imports(lines) -+ if first is None: -+ return lines -+ -+ module.sort(key=lambda x: (x[1], x[2], x[3])) -+ sorted_imports = relative + [entry[0] for entry in module] -+ return lines[:first] + sorted_imports + lines[last + 1 :] -+ -+ -+def check_file_structure(lines: list[str], rel: str) -> list[Violation]: -+ """Check file-level structure: pragmas, then imports, then content.""" -+ violations = [] -+ pragma_indices: list[int] = [] -+ import_indices: list[int] = [] -+ content_start: int | None = None -+ -+ for i, line in enumerate(lines): -+ stripped = line.strip() -+ if not stripped: -+ continue -+ if stripped.startswith("pragma "): -+ pragma_indices.append(i) -+ elif IMPORT_RE.match(stripped): -+ import_indices.append(i) -+ else: -+ content_start = i -+ break -+ -+ # Pragmas must come before imports -+ if pragma_indices and import_indices: -+ if pragma_indices[-1] > import_indices[0]: -+ violations.append( -+ Violation(rel, pragma_indices[-1] + 1, "file-structure", "pragmas should appear before imports") -+ ) -+ -+ # Separator between pragmas and imports -+ if pragma_indices and import_indices: -+ gap = import_indices[0] - pragma_indices[-1] - 1 -+ if gap == 0: -+ violations.append( -+ Violation( -+ rel, import_indices[0] + 1, "file-structure", "blank line expected between pragmas and imports" -+ ) -+ ) -+ elif gap > 1: -+ violations.append( -+ Violation( -+ rel, -+ pragma_indices[-1] + 3, -+ "file-structure", -+ "only one blank line expected between pragmas and imports", -+ ) -+ ) -+ -+ # No blank lines within imports -+ for j in range(1, len(import_indices)): -+ if import_indices[j] != import_indices[j - 1] + 1: -+ for gap_line in range(import_indices[j - 1] + 1, import_indices[j]): -+ if not lines[gap_line].strip(): -+ violations.append( -+ Violation(rel, gap_line + 1, "file-structure", "no blank lines expected within imports") -+ ) -+ -+ # Separator between imports/pragmas and content -+ last_header = import_indices[-1] if import_indices else (pragma_indices[-1] if pragma_indices else None) -+ if last_header is not None and content_start is not None: -+ gap = content_start - last_header - 1 -+ label = "imports" if import_indices else "pragmas" -+ if gap == 0: -+ violations.append( -+ Violation( -+ rel, content_start + 1, "file-structure", f"blank line expected between {label} and content" -+ ) -+ ) -+ elif gap > 1: -+ violations.append( -+ Violation( -+ rel, -+ last_header + 3, -+ "file-structure", -+ f"only one blank line expected between {label} and content", -+ ) -+ ) -+ -+ return violations -+ -+ -+def fix_file_structure(lines: list[str]) -> list[str]: -+ """Ensure correct file structure: pragmas, blank, imports (no gaps), blank, content.""" -+ pragmas: list[str] = [] -+ imports: list[str] = [] -+ content_start: int | None = None -+ -+ for i, line in enumerate(lines): -+ stripped = line.strip() -+ if not stripped: -+ continue -+ if stripped.startswith("pragma "): -+ pragmas.append(line) -+ elif IMPORT_RE.match(stripped): -+ imports.append(line) -+ else: -+ content_start = i -+ break -+ -+ if content_start is None: -+ content_start = len(lines) -+ -+ # Skip any blank lines at the start of content -+ while content_start < len(lines) and not lines[content_start].strip(): -+ content_start += 1 -+ -+ result: list[str] = [] -+ has_content = content_start < len(lines) -+ if pragmas: -+ result.extend(pragmas) -+ if imports or has_content: -+ result.append("") -+ if imports: -+ result.extend(imports) -+ if has_content: -+ result.append("") -+ result.extend(lines[content_start:]) -+ return result -+ -+ -+def fix_section_separators(lines: list[str]) -> list[str]: -+ """Insert blank lines between different sections and return modified lines.""" -+ insertions: list[int] = [] -+ scopes: dict[str, ScopeTracker] = {} -+ in_block_comment = False -+ func_skip_depth = 0 -+ prev_blank: dict[str, bool] = {} -+ -+ for i, line in enumerate(lines): -+ stripped = line.strip() -+ indent = get_indent(line) -+ -+ if in_block_comment: -+ if BLOCK_COMMENT_END.search(stripped): -+ in_block_comment = False -+ continue -+ if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): -+ in_block_comment = True -+ continue -+ -+ if not stripped: -+ for key in prev_blank: -+ prev_blank[key] = True -+ continue -+ -+ if COMMENT_LINE_RE.match(stripped): -+ continue -+ -+ if func_skip_depth > 0: -+ func_skip_depth += stripped.count("{") - stripped.count("}") -+ if func_skip_depth <= 0: -+ func_skip_depth = 0 -+ continue -+ -+ if stripped == "}": -+ to_remove = [k for k in scopes if len(k) > len(indent)] -+ for k in to_remove: -+ del scopes[k] -+ prev_blank.pop(k, None) -+ continue -+ -+ section = classify_line(stripped) -+ if section is None: -+ continue -+ -+ if indent not in scopes: -+ scopes[indent] = ScopeTracker() -+ prev_blank[indent] = True -+ -+ tracker = scopes[indent] -+ had_blank = prev_blank.get(indent, True) -+ -+ if tracker.last_section is not None and section != tracker.last_section and not had_blank: -+ insertions.append(i) -+ -+ if tracker.last_section is None or section >= tracker.last_section: -+ tracker.last_section = section -+ tracker.last_section_line = i + 1 -+ -+ prev_blank[indent] = False -+ -+ brace_count = stripped.count("{") - stripped.count("}") -+ if brace_count > 0 and section == Section.FUNCTION: -+ func_skip_depth = brace_count -+ if brace_count > 0 and section == Section.BINDING: -+ colon_idx = stripped.index(":") -+ after_colon = stripped[colon_idx + 1 :].strip() -+ if not re.match(r"^[A-Z]", after_colon): -+ func_skip_depth = brace_count -+ if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): -+ to_remove = [k for k in scopes if len(k) > len(indent)] -+ for k in to_remove: -+ del scopes[k] -+ prev_blank.pop(k, None) -+ -+ result = list(lines) -+ for idx in reversed(insertions): -+ result.insert(idx, "") -+ return result -+ -+ -+def fix_file(filepath: Path) -> bool: -+ """Fix auto-fixable violations. Returns True if file was modified.""" -+ try: -+ text = filepath.read_text() -+ except (OSError, UnicodeDecodeError): -+ return False -+ -+ lines = text.splitlines() -+ lines = fix_imports(lines) -+ lines = fix_file_structure(lines) -+ lines = fix_section_separators(lines) -+ new_text = "\n".join(lines) -+ if text.endswith("\n"): -+ new_text += "\n" -+ if new_text != text: -+ filepath.write_text(new_text) -+ return True -+ return False -+ -+ -+# Regexes -+PROPERTY_DECL_RE = re.compile(r"^(?:required\s+|readonly\s+|default\s+)*property\s") -+SIGNAL_RE = re.compile(r"^signal\s") -+FUNCTION_RE = re.compile(r"^function\s") -+ID_RE = re.compile(r"^id\s*:\s*[a-zA-Z_]\w*\s*$") -+ENUM_RE = re.compile(r"^enum\s") -+COMPONENT_DEF_RE = re.compile(r"^component\s+\w+\s*:") -+COMMENT_LINE_RE = re.compile(r"^//") -+BLOCK_COMMENT_START = re.compile(r"/\*") -+BLOCK_COMMENT_END = re.compile(r"\*/") -+BINDING_RE = re.compile(r"^[a-z][a-zA-Z0-9_.]*\s*:") -+SIGNAL_HANDLER_RE = re.compile(r"^on[A-Z][a-zA-Z]*\s*:") -+# Child object: starts with uppercase or is a known child-like pattern -+CHILD_OBJECT_RE = re.compile(r"^[A-Z][a-zA-Z0-9_.]*\s*\{") -+# Inline component: Component { ... } -+INLINE_COMPONENT_RE = re.compile(r"^Component\s*\{") -+# Behavior on {, NumberAnimation on {, etc. -+BEHAVIOR_ON_RE = re.compile(r"^[A-Z]\w+\s+on\s+\w[\w.]*\s*\{") -+# Attached signal handler: Component.onCompleted:, Drag.onDragStarted:, etc. -+ATTACHED_HANDLER_RE = re.compile(r"^[A-Z]\w+\.on[A-Z]\w*\s*:") -+ -+ -+class Violation: -+ def __init__(self, file: str, line: int, rule: str, msg: str): -+ self.file = file -+ self.line = line -+ self.rule = rule -+ self.msg = msg -+ -+ def __str__(self): -+ c = RULE_COLOURS.get(self.rule, "") -+ return f"{c}[{self.rule}]{RESET} {self.file}:{self.line}: {self.msg}" -+ -+ -+class ScopeTracker: -+ """Tracks the current section and last-seen state for one indent level.""" -+ -+ def __init__(self): -+ self.last_section: Section | None = None -+ self.last_section_line: int = 0 -+ self.had_blank_before_current: bool = True # no separator needed at start -+ -+ -+def get_indent(line: str) -> str: -+ return line[: len(line) - len(line.lstrip())] -+ -+ -+def classify_line(stripped: str) -> Section | None: -+ """Classify a stripped QML line into a section category.""" -+ if ID_RE.match(stripped): -+ return Section.ID -+ if PROPERTY_DECL_RE.match(stripped): -+ return Section.PROPERTY -+ if SIGNAL_RE.match(stripped): -+ return Section.SIGNAL -+ if FUNCTION_RE.match(stripped): -+ return Section.FUNCTION -+ if ENUM_RE.match(stripped): -+ return Section.PROPERTY # enums go with declarations -+ if COMPONENT_DEF_RE.match(stripped): -+ return Section.COMPONENT_DEF -+ if BEHAVIOR_ON_RE.match(stripped): -+ return Section.CHILD -+ if CHILD_OBJECT_RE.match(stripped): -+ return Section.CHILD -+ if INLINE_COMPONENT_RE.match(stripped): -+ return Section.CHILD -+ if BINDING_RE.match(stripped) or SIGNAL_HANDLER_RE.match(stripped): -+ return Section.BINDING -+ if ATTACHED_HANDLER_RE.match(stripped): -+ return Section.BINDING -+ return None -+ -+ -+def check_file(filepath: Path) -> list[Violation]: -+ violations = [] -+ rel = str(filepath.relative_to(REPO_ROOT)) -+ -+ try: -+ lines = filepath.read_text().splitlines() -+ except (OSError, UnicodeDecodeError): -+ return violations -+ -+ violations.extend(check_file_structure(lines, rel)) -+ violations.extend(check_imports(filepath, lines, rel)) -+ -+ scopes: dict[str, ScopeTracker] = {} # indent -> tracker -+ in_block_comment = False -+ func_skip_depth = 0 # brace depth for skipping function bodies only -+ prev_blank: dict[str, bool] = {} # indent -> was previous relevant line a blank? -+ -+ for i, line in enumerate(lines): -+ lineno = i + 1 -+ stripped = line.strip() -+ indent = get_indent(line) -+ -+ # Handle block comments -+ if in_block_comment: -+ if BLOCK_COMMENT_END.search(stripped): -+ in_block_comment = False -+ continue -+ if BLOCK_COMMENT_START.search(stripped) and not BLOCK_COMMENT_END.search(stripped): -+ in_block_comment = True -+ continue -+ -+ # Track blank lines per indent -+ if not stripped: -+ # Check: blank line right after opening brace of a QML object -+ if i > 0 and func_skip_depth == 0 and not in_block_comment and lines[i - 1].strip().endswith("{"): -+ violations.append( -+ Violation( -+ rel, -+ lineno, -+ "blank-after-open-brace", -+ "no blank line expected after opening brace", -+ ) -+ ) -+ for key in prev_blank: -+ prev_blank[key] = True -+ continue -+ -+ # Skip line comments -+ if COMMENT_LINE_RE.match(stripped): -+ continue -+ -+ # Skip inside function bodies (JS code, not QML structure) -+ if func_skip_depth > 0: -+ func_skip_depth += stripped.count("{") - stripped.count("}") -+ if func_skip_depth <= 0: -+ func_skip_depth = 0 -+ continue -+ -+ # Closing brace: pop all scopes deeper than this indent -+ # (the scope at this indent belongs to the parent object and must persist) -+ if stripped == "}": -+ # Check: blank line right before closing brace -+ if i > 0 and not lines[i - 1].strip(): -+ violations.append( -+ Violation( -+ rel, -+ lineno, -+ "blank-before-close-brace", -+ "no blank line expected before closing brace", -+ ) -+ ) -+ to_remove = [k for k in scopes if len(k) > len(indent)] -+ for k in to_remove: -+ del scopes[k] -+ prev_blank.pop(k, None) -+ continue -+ -+ section = classify_line(stripped) -+ if section is None: -+ continue -+ -+ # Get or create scope tracker for this indent -+ if indent not in scopes: -+ scopes[indent] = ScopeTracker() -+ prev_blank[indent] = True # treat start of object as having separator -+ -+ tracker = scopes[indent] -+ had_blank = prev_blank.get(indent, True) -+ -+ # --- Check 1: Section ordering --- -+ if tracker.last_section is not None and section < tracker.last_section: -+ violations.append( -+ Violation( -+ rel, -+ lineno, -+ "section-order", -+ f"{SECTION_NAMES[section]} should appear before " -+ f"{SECTION_NAMES[tracker.last_section]} " -+ f"(seen at line {tracker.last_section_line})", -+ ) -+ ) -+ -+ # --- Check 2: Missing blank line between different sections --- -+ if tracker.last_section is not None and section != tracker.last_section and not had_blank: -+ violations.append( -+ Violation( -+ rel, -+ lineno, -+ "missing-section-separator", -+ f"blank line expected between {SECTION_NAMES[tracker.last_section]} and {SECTION_NAMES[section]}", -+ ) -+ ) -+ -+ # Update tracker -+ if tracker.last_section is None or section >= tracker.last_section: -+ tracker.last_section = section -+ tracker.last_section_line = lineno -+ -+ prev_blank[indent] = False -+ -+ # Skip function bodies (they contain JS, not QML structure) -+ brace_count = stripped.count("{") - stripped.count("}") -+ if brace_count > 0 and section == Section.FUNCTION: -+ func_skip_depth = brace_count -+ -+ # Skip JS blocks in bindings (signal handlers, attached handlers, -+ # and expression blocks like `color: { ... }`) -+ if brace_count > 0 and section == Section.BINDING: -+ colon_idx = stripped.index(":") -+ after_colon = stripped[colon_idx + 1 :].strip() -+ # If content after : doesn't start with an uppercase type name, -+ # it's a JS block (not an inline QML object like `contentItem: Rect {`) -+ if not re.match(r"^[A-Z]", after_colon): -+ func_skip_depth = brace_count -+ -+ # Child object/component opening resets deeper scopes -+ if brace_count > 0 and section in (Section.CHILD, Section.COMPONENT_DEF): -+ to_remove = [k for k in scopes if len(k) > len(indent)] -+ for k in to_remove: -+ del scopes[k] -+ prev_blank.pop(k, None) -+ -+ return violations -+ -+ -+def main(): -+ fix_mode = "--fix" in sys.argv -+ qml_files = sorted(p for p in REPO_ROOT.rglob("*.qml") if "build" not in p.parts) -+ -+ if fix_mode: -+ fixed = sum(1 for f in qml_files if fix_file(f)) -+ print(f"{BOLD}Fixed {fixed} file(s).{RESET}\n") -+ -+ print(f"{BOLD}Checking {len(qml_files)} QML files for convention violations...{RESET}\n") -+ -+ all_violations: list[Violation] = [] -+ for f in qml_files: -+ all_violations.extend(check_file(f)) -+ -+ for v in all_violations: -+ print(v) -+ -+ print() -+ if all_violations: -+ by_rule: dict[str, int] = {} -+ for v in all_violations: -+ by_rule[v.rule] = by_rule.get(v.rule, 0) + 1 -+ for rule, count in sorted(by_rule.items()): -+ print(f" {RULE_COLOURS.get(rule, '')}{rule}{RESET}: {count}") -+ print(f"\n{BOLD}Found {len(all_violations)} violation(s).{RESET}") -+ return 1 -+ else: -+ print(f"{BOLD}No violations found.{RESET}") -+ return 0 -+ -+ -+if __name__ == "__main__": -+ sys.exit(main()) -diff --git a/services/Audio.qml b/services/Audio.qml -index 908d1563..6268b92e 100644 ---- a/services/Audio.qml -+++ b/services/Audio.qml -@@ -1,11 +1,12 @@ - pragma Singleton - --import qs.config --import Caelestia.Services --import Caelestia -+import QtQuick - import Quickshell -+import Quickshell.Io - import Quickshell.Services.Pipewire --import QtQuick -+import Caelestia -+import Caelestia.Config -+import Caelestia.Services - - Singleton { - id: root -@@ -13,26 +14,9 @@ Singleton { - property string previousSinkName: "" - property string previousSourceName: "" - -- readonly property var nodes: Pipewire.nodes.values.reduce((acc, node) => { -- if (!node.isStream) { -- if (node.isSink) -- acc.sinks.push(node); -- else if (node.audio) -- acc.sources.push(node); -- } else if (node.isStream && node.audio) { -- // Application streams (output streams) -- acc.streams.push(node); -- } -- return acc; -- }, { -- sources: [], -- sinks: [], -- streams: [] -- }) -- -- readonly property list sinks: nodes.sinks -- readonly property list sources: nodes.sources -- readonly property list streams: nodes.streams -+ property list sinks: [] -+ property list sources: [] -+ property list streams: [] - - readonly property PwNode sink: Pipewire.defaultAudioSink - readonly property PwNode source: Pipewire.defaultAudioSource -@@ -49,31 +33,31 @@ Singleton { - function setVolume(newVolume: real): void { - if (sink?.ready && sink?.audio) { - sink.audio.muted = false; -- sink.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); -+ sink.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); - } - } - - function incrementVolume(amount: real): void { -- setVolume(volume + (amount || Config.services.audioIncrement)); -+ setVolume(volume + (amount || GlobalConfig.services.audioIncrement)); - } - - function decrementVolume(amount: real): void { -- setVolume(volume - (amount || Config.services.audioIncrement)); -+ setVolume(volume - (amount || GlobalConfig.services.audioIncrement)); - } - - function setSourceVolume(newVolume: real): void { - if (source?.ready && source?.audio) { - source.audio.muted = false; -- source.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); -+ source.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); - } - } - - function incrementSourceVolume(amount: real): void { -- setSourceVolume(sourceVolume + (amount || Config.services.audioIncrement)); -+ setSourceVolume(sourceVolume + (amount || GlobalConfig.services.audioIncrement)); - } - - function decrementSourceVolume(amount: real): void { -- setSourceVolume(sourceVolume - (amount || Config.services.audioIncrement)); -+ setSourceVolume(sourceVolume - (amount || GlobalConfig.services.audioIncrement)); - } - - function setAudioSink(newSink: PwNode): void { -@@ -84,10 +68,19 @@ Singleton { - Pipewire.preferredDefaultAudioSource = newSource; - } - -+ function cycleNextAudioOutput(): void { -+ if (sinks.length === 0) -+ return; -+ -+ const currentIndex = sinks.findIndex(s => s === sink); -+ const nextIndex = (currentIndex + 1) % sinks.length; -+ setAudioSink(sinks[nextIndex]); -+ } -+ - function setStreamVolume(stream: PwNode, newVolume: real): void { - if (stream?.ready && stream?.audio) { - stream.audio.muted = false; -- stream.audio.volume = Math.max(0, Math.min(Config.services.maxVolume, newVolume)); -+ stream.audio.volume = Math.max(0, Math.min(GlobalConfig.services.maxVolume, newVolume)); - } - } - -@@ -109,7 +102,7 @@ Singleton { - if (!stream) - return qsTr("Unknown"); - // Try application name first, then description, then name -- return stream.applicationName || stream.description || stream.name || qsTr("Unknown Application"); -+ return stream.properties["application.name"] || stream.description || stream.name || qsTr("Unknown Application"); - } - - onSinkChanged: { -@@ -118,7 +111,7 @@ Singleton { - - const newSinkName = sink.description || sink.name || qsTr("Unknown Device"); - -- if (previousSinkName && previousSinkName !== newSinkName && Config.utilities.toasts.audioOutputChanged) -+ if (previousSinkName && previousSinkName !== newSinkName && GlobalConfig.utilities.toasts.audioOutputChanged) - Toaster.toast(qsTr("Audio output changed"), qsTr("Now using: %1").arg(newSinkName), "volume_up"); - - previousSinkName = newSinkName; -@@ -130,7 +123,7 @@ Singleton { - - const newSourceName = source.description || source.name || qsTr("Unknown Device"); - -- if (previousSourceName && previousSourceName !== newSourceName && Config.utilities.toasts.audioInputChanged) -+ if (previousSourceName && previousSourceName !== newSourceName && GlobalConfig.utilities.toasts.audioInputChanged) - Toaster.toast(qsTr("Audio input changed"), qsTr("Now using: %1").arg(newSourceName), "mic"); - - previousSourceName = newSourceName; -@@ -141,6 +134,31 @@ Singleton { - previousSourceName = source?.description || source?.name || qsTr("Unknown Device"); - } - -+ Connections { -+ function onValuesChanged(): void { -+ const newSinks = []; -+ const newSources = []; -+ const newStreams = []; -+ -+ for (const node of Pipewire.nodes.values) { -+ if (!node.isStream) { -+ if (node.isSink) -+ newSinks.push(node); -+ else if (node.audio) -+ newSources.push(node); -+ } else if (node.audio) { -+ newStreams.push(node); -+ } -+ } -+ -+ root.sinks = newSinks; -+ root.sources = newSources; -+ root.streams = newStreams; -+ } -+ -+ target: Pipewire.nodes -+ } -+ - PwObjectTracker { - objects: [...root.sinks, ...root.sources, ...root.streams] - } -@@ -148,10 +166,18 @@ Singleton { - CavaProvider { - id: cava - -- bars: Config.services.visualiserBars -+ bars: GlobalConfig.services.visualiserBars - } - - BeatTracker { - id: beatTracker - } -+ -+ IpcHandler { -+ function cycleOutput(): void { -+ root.cycleNextAudioOutput(); -+ } -+ -+ target: "audio" -+ } - } -diff --git a/services/Brightness.qml b/services/Brightness.qml -index 12920eed..ac34cbc1 100644 ---- a/services/Brightness.qml -+++ b/services/Brightness.qml -@@ -1,56 +1,62 @@ - pragma Singleton - pragma ComponentBehavior: Bound - --import qs.config --import qs.components.misc -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick -+import Caelestia.Config -+import qs.components.misc - - Singleton { - id: root - - property list ddcMonitors: [] -- readonly property list monitors: variants.instances -+ readonly property var ddcMonitorMap: { -+ const map = {}; -+ for (const m of ddcMonitors) -+ map[m.connector] = m; -+ return map; -+ } -+ readonly property list monitors: variants.instances // qmllint disable incompatible-type - property bool appleDisplayPresent: false - - function getMonitorForScreen(screen: ShellScreen): var { -- return monitors.find(m => m.modelData === screen); -+ return monitors.find(m => m.modelData === screen); // qmllint disable missing-property - } - - function getMonitor(query: string): var { - if (query === "active") { -- return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); -+ return monitors.find(m => Hypr.monitorFor(m.modelData)?.focused); // qmllint disable missing-property - } - - if (query.startsWith("model:")) { - const model = query.slice(6); -- return monitors.find(m => m.modelData.model === model); -+ return monitors.find(m => m.modelData.model === model); // qmllint disable missing-property - } - - if (query.startsWith("serial:")) { - const serial = query.slice(7); -- return monitors.find(m => m.modelData.serialNumber === serial); -+ return monitors.find(m => m.modelData.serialNumber === serial); // qmllint disable missing-property - } - - if (query.startsWith("id:")) { - const id = parseInt(query.slice(3), 10); -- return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); -+ return monitors.find(m => Hypr.monitorFor(m.modelData)?.id === id); // qmllint disable missing-property - } - -- return monitors.find(m => m.modelData.name === query); -+ return monitors.find(m => m.modelData.name === query); // qmllint disable missing-property - } - - function increaseBrightness(): void { - const monitor = getMonitor("active"); - if (monitor) -- monitor.setBrightness(monitor.brightness + Config.services.brightnessIncrement); -+ monitor.setBrightness(monitor.brightness + GlobalConfig.services.brightnessIncrement); - } - - function decreaseBrightness(): void { - const monitor = getMonitor("active"); - if (monitor) -- monitor.setBrightness(monitor.brightness - Config.services.brightnessIncrement); -+ monitor.setBrightness(monitor.brightness - GlobalConfig.services.brightnessIncrement); - } - - onMonitorsChanged: { -@@ -61,7 +67,7 @@ Singleton { - Variants { - id: variants - -- model: Quickshell.screens -+ model: Quickshell.screens // Don't respect excluded screens cause ipc - - Monitor {} - } -@@ -86,21 +92,23 @@ Singleton { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "brightnessUp" - description: "Increase brightness" - onPressed: root.increaseBrightness() - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "brightnessDown" - description: "Decrease brightness" - onPressed: root.decreaseBrightness() - } - - IpcHandler { -- target: "brightness" -- - function get(): real { - return getFor("active"); - } -@@ -149,14 +157,17 @@ Singleton { - - return `Set monitor ${monitor.modelData.name} brightness to ${+monitor.brightness.toFixed(2)}`; - } -+ -+ target: "brightness" - } - - component Monitor: QtObject { - id: monitor - - required property ShellScreen modelData -- readonly property bool isDdc: root.ddcMonitors.some(m => m.connector === modelData.name) -- readonly property string busNum: root.ddcMonitors.find(m => m.connector === modelData.name)?.busNum ?? "" -+ readonly property var ddcInfo: root.ddcMonitorMap[modelData.name] ?? null -+ readonly property bool isDdc: ddcInfo !== null -+ readonly property string busNum: ddcInfo?.busNum ?? "" - readonly property bool isAppleDisplay: root.appleDisplayPresent && modelData.model.startsWith("StudioDisplay") - property real brightness - property real queuedBrightness: NaN -diff --git a/services/Colours.qml b/services/Colours.qml -index cd86c8fb..922b51db 100644 ---- a/services/Colours.qml -+++ b/services/Colours.qml -@@ -1,12 +1,13 @@ - pragma Singleton - pragma ComponentBehavior: Bound - --import qs.config --import qs.utils --import Caelestia -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick -+import Caelestia -+import Caelestia.Config -+import qs.services -+import qs.utils - - Singleton { - id: root -@@ -78,6 +79,21 @@ Singleton { - Quickshell.execDetached(["caelestia", "scheme", "set", "--notify", "-m", mode]); - } - -+ function reloadHyprRules(): void { -+ const str = "keyword layerrule %1 %2, match:namespace caelestia-drawers"; -+ Hypr.extras.batchMessage([str.arg("blur").arg(transparency.enabled ? 1 : 0), str.arg("ignore_alpha").arg(transparency.base - 0.03)]); -+ } -+ -+ Component.onCompleted: debounceTimer.triggered() -+ -+ Connections { -+ function onConfigReloaded(): void { -+ root.reloadHyprRules(); -+ } -+ -+ target: Hypr -+ } -+ - FileView { - path: `${Paths.state}/scheme.json` - watchChanges: true -@@ -91,10 +107,20 @@ Singleton { - source: Wallpapers.current - } - -+ Timer { -+ id: debounceTimer -+ -+ interval: 300 -+ onTriggered: root.reloadHyprRules() -+ } -+ - component Transparency: QtObject { -- readonly property bool enabled: Appearance.transparency.enabled -- readonly property real base: Appearance.transparency.base - (root.light ? 0.1 : 0) -- readonly property real layers: Appearance.transparency.layers -+ readonly property bool enabled: Tokens.transparency.enabled -+ readonly property real base: Math.max(0, Math.min(1, Tokens.transparency.base - (root.light ? 0.1 : 0))) -+ readonly property real layers: Tokens.transparency.layers -+ -+ onEnabledChanged: debounceTimer.restart() -+ onBaseChanged: debounceTimer.restart() - } - - component M3TPalette: QtObject { -diff --git a/services/GameMode.qml b/services/GameMode.qml -index 83770b79..6dfc791c 100644 ---- a/services/GameMode.qml -+++ b/services/GameMode.qml -@@ -1,11 +1,11 @@ - pragma Singleton - --import qs.services --import qs.config --import Caelestia -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick -+import Caelestia -+import Caelestia.Config -+import qs.services - - Singleton { - id: root -@@ -28,11 +28,11 @@ Singleton { - onEnabledChanged: { - if (enabled) { - setDynamicConfs(); -- if (Config.utilities.toasts.gameModeChanged) -+ if (GlobalConfig.utilities.toasts.gameModeChanged) - Toaster.toast(qsTr("Game mode enabled"), qsTr("Disabled Hyprland animations, blur, gaps and shadows"), "gamepad"); - } else { - Hypr.extras.message("reload"); -- if (Config.utilities.toasts.gameModeChanged) -+ if (GlobalConfig.utilities.toasts.gameModeChanged) - Toaster.toast(qsTr("Game mode disabled"), qsTr("Hyprland settings restored"), "gamepad"); - } - } -@@ -40,23 +40,21 @@ Singleton { - PersistentProperties { - id: props - -- property bool enabled: Hypr.options["animations:enabled"] === 0 -+ property bool enabled: Hypr.options["animations:enabled"] === 0 // qmllint disable missing-property - - reloadableId: "gameMode" - } - - Connections { -- target: Hypr -- - function onConfigReloaded(): void { - if (props.enabled) - root.setDynamicConfs(); - } -+ -+ target: Hypr - } - - IpcHandler { -- target: "gameMode" -- - function isEnabled(): bool { - return props.enabled; - } -@@ -72,5 +70,7 @@ Singleton { - function disable(): void { - props.enabled = false; - } -+ -+ target: "gameMode" - } - } -diff --git a/services/Hypr.qml b/services/Hypr.qml -index a654fdd3..181967e7 100644 ---- a/services/Hypr.qml -+++ b/services/Hypr.qml -@@ -1,13 +1,13 @@ - pragma Singleton - --import qs.components.misc --import qs.config --import Caelestia --import Caelestia.Internal -+import QtQuick - import Quickshell - import Quickshell.Hyprland - import Quickshell.Io --import QtQuick -+import Caelestia -+import Caelestia.Config -+import Caelestia.Internal -+import qs.components.misc - - Singleton { - id: root -@@ -16,7 +16,10 @@ Singleton { - readonly property var workspaces: Hyprland.workspaces - readonly property var monitors: Hyprland.monitors - -- readonly property HyprlandToplevel activeToplevel: Hyprland.activeToplevel?.wayland?.activated ? Hyprland.activeToplevel : null -+ readonly property HyprlandToplevel activeToplevel: { -+ const t = Hyprland.activeToplevel; -+ return t?.workspace?.name.startsWith("special:") || Hyprland.focusedWorkspace?.toplevels.values.length > 0 ? t : null; -+ } - readonly property HyprlandWorkspace focusedWorkspace: Hyprland.focusedWorkspace - readonly property HyprlandMonitor focusedMonitor: Hyprland.focusedMonitor - readonly property int activeWsId: focusedWorkspace?.id ?? 1 -@@ -34,6 +37,7 @@ Singleton { - readonly property alias devices: extras.devices - - property bool hadKeyboard -+ property string lastSpecialWorkspace: "" - - signal configReloaded - -@@ -41,6 +45,43 @@ Singleton { - Hyprland.dispatch(request); - } - -+ function cycleSpecialWorkspace(direction: string): void { -+ const openSpecials = workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0); -+ -+ if (openSpecials.length === 0) -+ return; -+ -+ const activeSpecial = focusedMonitor.lastIpcObject.specialWorkspace.name ?? ""; -+ -+ if (!activeSpecial) { -+ if (lastSpecialWorkspace) { -+ const workspace = workspaces.values.find(w => w.name === lastSpecialWorkspace); -+ if (workspace && workspace.lastIpcObject.windows > 0) { -+ dispatch(`workspace ${lastSpecialWorkspace}`); -+ return; -+ } -+ } -+ dispatch(`workspace ${openSpecials[0].name}`); -+ return; -+ } -+ -+ const currentIndex = openSpecials.findIndex(w => w.name === activeSpecial); -+ let nextIndex = 0; -+ -+ if (currentIndex !== -1) { -+ if (direction === "next") -+ nextIndex = (currentIndex + 1) % openSpecials.length; -+ else -+ nextIndex = (currentIndex - 1 + openSpecials.length) % openSpecials.length; -+ } -+ -+ dispatch(`workspace ${openSpecials[nextIndex].name}`); -+ } -+ -+ function monitorNames(): list { -+ return monitors.values.map(e => e.name); -+ } -+ - function monitorFor(screen: ShellScreen): HyprlandMonitor { - return Hyprland.monitorFor(screen); - } -@@ -52,7 +93,7 @@ Singleton { - Component.onCompleted: reloadDynamicConfs() - - onCapsLockChanged: { -- if (!Config.utilities.toasts.capsLockChanged) -+ if (!GlobalConfig.utilities.toasts.capsLockChanged) - return; - - if (capsLock) -@@ -62,7 +103,7 @@ Singleton { - } - - onNumLockChanged: { -- if (!Config.utilities.toasts.numLockChanged) -+ if (!GlobalConfig.utilities.toasts.numLockChanged) - return; - - if (numLock) -@@ -72,15 +113,13 @@ Singleton { - } - - onKbLayoutFullChanged: { -- if (hadKeyboard && Config.utilities.toasts.kbLayoutChanged) -+ if (hadKeyboard && GlobalConfig.utilities.toasts.kbLayoutChanged) - Toaster.toast(qsTr("Keyboard layout changed"), qsTr("Layout changed to: %1").arg(kbLayoutFull), "keyboard"); - - hadKeyboard = !!keyboard; - } - - Connections { -- target: Hyprland -- - function onRawEvent(event: HyprlandEvent): void { - const n = event.name; - if (n.endsWith("v2")) -@@ -103,6 +142,20 @@ Singleton { - Hyprland.refreshToplevels(); - } - } -+ -+ target: Hyprland -+ } -+ -+ Connections { -+ function onLastIpcObjectChanged(): void { -+ const specialName = root.focusedMonitor.lastIpcObject.specialWorkspace.name; -+ -+ if (specialName && specialName.startsWith("special:")) { -+ root.lastSpecialWorkspace = specialName; -+ } -+ } -+ -+ target: root.focusedMonitor - } - - FileView { -@@ -139,14 +192,24 @@ Singleton { - } - - IpcHandler { -- target: "hypr" -- - function refreshDevices(): void { - extras.refreshDevices(); - } -+ -+ function cycleSpecialWorkspace(direction: string): void { -+ root.cycleSpecialWorkspace(direction); -+ } -+ -+ function listSpecialWorkspaces(): string { -+ return root.workspaces.values.filter(w => w.name.startsWith("special:") && w.lastIpcObject.windows > 0).map(w => w.name).join("\n"); -+ } -+ -+ target: "hypr" - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "refreshDevices" - description: "Reload devices" - onPressed: extras.refreshDevices() -diff --git a/services/IdleInhibitor.qml b/services/IdleInhibitor.qml -index 29409abc..9f556b3a 100644 ---- a/services/IdleInhibitor.qml -+++ b/services/IdleInhibitor.qml -@@ -35,8 +35,6 @@ Singleton { - } - - IpcHandler { -- target: "idleInhibitor" -- - function isEnabled(): bool { - return props.enabled; - } -@@ -52,5 +50,7 @@ Singleton { - function disable(): void { - props.enabled = false; - } -+ -+ target: "idleInhibitor" - } - } -diff --git a/services/LyricsService.qml b/services/LyricsService.qml -new file mode 100644 -index 00000000..26a75e9b ---- /dev/null -+++ b/services/LyricsService.qml -@@ -0,0 +1,403 @@ -+pragma Singleton -+ -+import "../utils/scripts/lrcparser.js" as Lrc -+import QtQuick -+import Quickshell -+import Quickshell.Io -+import Caelestia -+import Caelestia.Config -+import qs.utils -+ -+Singleton { -+ id: root -+ -+ property var player: Players.active -+ property int currentIndex: -1 -+ property bool loading: false -+ property bool isManualSeeking: false -+ property bool lyricsVisible: GlobalConfig.services.showLyrics -+ property string backend: "Local" -+ property string preferredBackend: GlobalConfig.services.lyricsBackend -+ property real currentSongId: 0 -+ property string loadedLocalFile: "" -+ property real offset -+ property int currentRequestId: 0 -+ property var lyricsMap: ({}) -+ -+ readonly property string lyricsDir: Paths.absolutePath(GlobalConfig.paths.lyricsDir) -+ readonly property string lyricsMapFile: lyricsDir + "/lyrics_map.json" -+ readonly property alias model: lyricsModel -+ readonly property alias candidatesModel: fetchedCandidatesModel -+ readonly property var _netEaseHeaders: ({ -+ "User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:120.0) Gecko/20100101 Firefox/120.0", -+ "Referer": "https://music.163.com/" -+ }) -+ -+ function getMetadata() { -+ if (!player || !player.metadata) -+ return null; -+ let artist = player.metadata["xesam:artist"]; -+ const title = player.metadata["xesam:title"]; -+ if (Array.isArray(artist)) -+ artist = artist.join(", "); -+ return { -+ artist: artist || "Unknown", -+ title: title || "Unknown" -+ }; -+ } -+ -+ function _metaKey(meta) { -+ return `${meta.artist} - ${meta.title}`; -+ } -+ -+ function savePrefs() { -+ let meta = getMetadata(); -+ if (!meta) -+ return; -+ let key = _metaKey(meta); -+ let existing = root.lyricsMap[key] ?? {}; -+ root.lyricsMap[key] = { -+ offset: root.offset, -+ backend: root.backend, -+ neteaseId: existing.neteaseId ?? null -+ }; -+ // reassign to notify QML bindings of the map change -+ root.lyricsMap = root.lyricsMap; -+ saveLyricsMap.command = ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap).replace(/'/g, "'\\''")}' > "${root.lyricsMapFile}"`]; -+ saveLyricsMap.running = true; -+ } -+ -+ function toggleVisibility() { -+ GlobalConfig.services.showLyrics = !GlobalConfig.services.showLyrics; -+ } -+ -+ function loadLyrics() { -+ loadDebounce.restart(); -+ } -+ -+ function _doLoadLyrics() { -+ const meta = getMetadata(); -+ if (!meta) { -+ lyricsModel.clear(); -+ root.currentIndex = -1; -+ return; -+ } -+ -+ loading = true; -+ lyricsModel.clear(); -+ currentIndex = -1; -+ root.currentSongId = 0; -+ -+ root.currentRequestId++; -+ let requestId = root.currentRequestId; -+ -+ let key = _metaKey(meta); -+ let saved = root.lyricsMap[key]; -+ root.offset = saved?.offset ?? 0.0; -+ -+ if (root.preferredBackend === "NetEase") { -+ root.backend = "NetEase"; -+ fetchNetEase(meta.title, meta.artist, requestId); -+ return; -+ } -+ -+ if (root.preferredBackend === "Local") { -+ root.backend = "Local"; -+ let cleanDir = lyricsDir.replace(/\/$/, ""); -+ let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; -+ -+ // Search for files matching "Artist - Title.lrc" pattern -+ const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); -+ const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); -+ const escapedTitle = titleStr.replace(/'/g, "'\\''"); -+ const escapedArtist = artistStr.replace(/'/g, "'\\''"); -+ findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; -+ findLyricsInSubdirs.requestId = requestId; -+ findLyricsInSubdirs.running = true; -+ -+ lrcFile.path = ""; -+ lrcFile.path = flatPath; -+ return; -+ } -+ -+ // Auto mode: try local first -+ root.backend = "Local"; -+ let cleanDir = lyricsDir.replace(/\/$/, ""); -+ let flatPath = `${cleanDir}/${meta.artist} - ${meta.title}.lrc`; -+ -+ const artistStr = Array.isArray(meta.artist) ? meta.artist.join(", ") : String(meta.artist || ""); -+ const titleStr = Array.isArray(meta.title) ? meta.title.join(", ") : String(meta.title || ""); -+ const escapedTitle = titleStr.replace(/'/g, "'\\''"); -+ const escapedArtist = artistStr.replace(/'/g, "'\\''"); -+ findLyricsInSubdirs.command = ["sh", "-c", `find "${cleanDir}" -type f -iname "*${escapedArtist}*${escapedTitle}*.lrc" | head -n 1`]; -+ findLyricsInSubdirs.requestId = requestId; -+ findLyricsInSubdirs.running = true; -+ -+ lrcFile.path = ""; -+ lrcFile.path = flatPath; -+ fetchNetEaseCandidates(meta.title, meta.artist, requestId); -+ } -+ -+ function updateModel(parsedArray) { -+ root.currentIndex = -1; -+ lyricsModel.clear(); -+ for (let line of parsedArray) { -+ lyricsModel.append({ -+ time: line.time, -+ lyricLine: line.text -+ }); -+ } -+ } -+ -+ function fallbackToOnline() { -+ let meta = getMetadata(); -+ if (!meta) -+ return; -+ fetchNetEase(meta.title, meta.artist, root.currentRequestId); -+ } -+ -+ // NetEase -+ -+ // searches NetEase and populates the candidates model. returns the result array via the onResults callback -+ function _searchNetEase(title, artist, reqId, onResults) { -+ Requests.resetCookies(); -+ const query = encodeURIComponent(`${title} ${artist}`); -+ const url = `https://music.163.com/api/search/get?s=${query}&type=1&limit=5`; -+ -+ Requests.get(url, text => { -+ if (reqId !== root.currentRequestId) -+ return; -+ const res = JSON.parse(text); -+ const songs = res.result?.songs || []; -+ -+ fetchedCandidatesModel.clear(); -+ for (let s of songs) { -+ fetchedCandidatesModel.append({ -+ id: s.id, -+ title: s.name || "Unknown Title", -+ artist: s.artists?.map(a => a.name).join(", ") || "Unknown Artist" -+ }); -+ } -+ -+ onResults(songs); -+ }, err => {}, root._netEaseHeaders); -+ } -+ -+ // populates the candidates model only. used when a saved NetEase ID already exists and we just want to refresh the picker list. -+ function fetchNetEaseCandidates(title, artist, reqId) { -+ _searchNetEase(title, artist, reqId, _songs => {}); -+ } -+ -+ // searches NetEase, populates candidates, then auto-selects the best match and fetches its lyrics. -+ function fetchNetEase(title, artist, reqId) { -+ _searchNetEase(title, artist, reqId, songs => { -+ const bestMatch = songs.find(s => { -+ const inputArtist = String(artist || "").toLowerCase(); -+ const sArtist = String(s.artists?.[0]?.name || "").toLowerCase(); -+ return inputArtist.includes(sArtist) || sArtist.includes(inputArtist); -+ }); -+ -+ if (!bestMatch) { -+ return; // No reliable lyrics found -+ } -+ -+ let key = `${artist} - ${title}`; -+ root.lyricsMap[key] = { -+ offset: root.lyricsMap[key]?.offset ?? 0.0, -+ backend: "NetEase", -+ neteaseId: bestMatch.id -+ }; -+ root.currentSongId = bestMatch.id; -+ savePrefs(); -+ fetchNetEaseLyrics(bestMatch.id, reqId); -+ }); -+ } -+ -+ function fetchNetEaseLyrics(id, reqId) { -+ const url = `https://music.163.com/api/song/lyric?id=${id}&lv=1&kv=1&tv=-1`; -+ Requests.get(url, text => { -+ if (reqId !== root.currentRequestId) -+ return; -+ const res = JSON.parse(text); -+ if (res.lrc?.lyric) { -+ updateModel(Lrc.parseLrc(res.lrc.lyric)); -+ loading = false; -+ } -+ }); -+ } -+ -+ function selectCandidate(songId) { -+ let meta = getMetadata(); -+ if (!meta) -+ return; -+ root.backend = "NetEase"; -+ root.currentSongId = songId; -+ let key = _metaKey(meta); -+ root.lyricsMap[key] = { -+ offset: root.lyricsMap[key]?.offset ?? 0.0, -+ neteaseId: songId -+ }; -+ savePrefs(); -+ fetchNetEaseLyrics(songId, currentRequestId); -+ } -+ -+ function updatePosition() { -+ if (isManualSeeking || loading || !player || lyricsModel.count === 0) -+ return; -+ -+ let pos = player.position - root.offset; -+ let newIdx = -1; -+ for (let i = lyricsModel.count - 1; i >= 0; i--) { -+ if (pos >= lyricsModel.get(i).time - 0.1) { // 100ms fudge factor -+ newIdx = i; -+ break; -+ } -+ } -+ -+ if (newIdx !== currentIndex) { -+ root.currentIndex = newIdx; -+ } -+ } -+ -+ function jumpTo(index, time) { -+ root.isManualSeeking = true; -+ root.currentIndex = index; -+ -+ if (player) { -+ player.position = time + root.offset + 0.01; // compensate for rounding -+ } -+ -+ seekTimer.restart(); -+ } -+ -+ onPreferredBackendChanged: { -+ if (GlobalConfig.services.lyricsBackend !== preferredBackend) { -+ GlobalConfig.services.lyricsBackend = preferredBackend; -+ } -+ } -+ -+ ListModel { -+ id: lyricsModel -+ } -+ -+ ListModel { -+ id: fetchedCandidatesModel -+ } -+ -+ Timer { -+ id: seekTimer -+ -+ interval: 500 -+ onTriggered: root.isManualSeeking = false -+ } -+ -+ // If no local lyrics were loaded within the interval, fall back to NetEase -+ Timer { -+ id: fallbackTimer -+ -+ interval: 200 -+ onTriggered: { -+ if (lyricsModel.count === 0) { -+ root.backend = "NetEase"; -+ fallbackToOnline(); -+ } -+ } -+ } -+ -+ Timer { -+ id: loadDebounce -+ -+ interval: 50 -+ onTriggered: root._doLoadLyrics() -+ } -+ -+ FileView { -+ id: lyricsMapFileView -+ -+ path: root.lyricsMapFile -+ printErrors: false -+ onLoaded: { -+ try { -+ root.lyricsMap = JSON.parse(text()); -+ } catch (e) { -+ root.lyricsMap = {}; -+ } -+ } -+ } -+ -+ FileView { -+ id: lrcFile -+ -+ printErrors: false -+ onLoaded: { -+ fallbackTimer.stop(); -+ let parsed = Lrc.parseLrc(text()); -+ if (parsed.length > 0) { -+ root.backend = "Local"; -+ root.loadedLocalFile = path; -+ updateModel(parsed); -+ loading = false; -+ } else if (root.preferredBackend === "Local") { -+ // Local mode only - fail immediately -+ root.backend = "NetEase"; -+ fallbackToOnline(); -+ } -+ // In Auto mode, let the Process onExited handle fallback -+ } -+ } -+ -+ Connections { -+ function onActiveChanged() { -+ root.player = Players.active; -+ loadLyrics(); -+ } -+ -+ target: Players -+ } -+ -+ Connections { -+ function onMetadataChanged() { -+ loadLyrics(); -+ } -+ -+ target: root.player -+ ignoreUnknownSignals: true -+ } -+ -+ Process { -+ id: saveLyricsMap -+ -+ command: ["sh", "-c", `mkdir -p "${root.lyricsDir}" && echo '${JSON.stringify(root.lyricsMap)}' > "${root.lyricsMapFile}"`] -+ } -+ -+ Process { -+ id: findLyricsInSubdirs -+ -+ property int requestId: -1 -+ property bool foundFile: false -+ -+ stdout: SplitParser { -+ onRead: data => { -+ if (findLyricsInSubdirs.requestId === root.currentRequestId) { -+ const foundPath = data.trim(); -+ if (foundPath && foundPath.length > 0) { -+ findLyricsInSubdirs.foundFile = true; -+ fallbackTimer.stop(); -+ root.loadedLocalFile = foundPath; -+ lrcFile.path = ""; -+ lrcFile.path = foundPath; -+ } -+ } -+ } -+ } -+ -+ onExited: (exitCode, exitStatus) => { // qmllint disable signal-handler-parameters -+ if (requestId === root.currentRequestId && !foundFile && root.preferredBackend === "Auto") { -+ if (lyricsModel.count === 0) { -+ fallbackTimer.restart(); -+ } -+ } -+ foundFile = false; -+ } -+ } -+} -diff --git a/services/Monitors.qml b/services/Monitors.qml -index e64d14e5..c880ada5 100644 ---- a/services/Monitors.qml -+++ b/services/Monitors.qml -@@ -29,29 +29,37 @@ Singleton { - identifyTimer.stop(); - } - -- // Safely iterate UntypedObjectModel — .find() doesn't work on it -+ function sourceMonitors(): var { -+ if ((Hyprctl.monitors?.length ?? 0) > 0) -+ return Hyprctl.monitors; -+ return Hypr.monitors.values ?? []; -+ } -+ -+ // Safely iterate monitor data — .find() doesn't work on UntypedObjectModel - function findMonitorByName(name: string): var { -- for (let i = 0; i < Hypr.monitors.length; i++) { -- if (Hypr.monitors[i].name === name) -- return Hypr.monitors[i]; -+ const monitors = sourceMonitors(); -+ for (let i = 0; i < monitors.length; i++) { -+ if (monitors[i].name === name) -+ return monitors[i]; - } - return null; - } - - function findMonitorById(id: int): var { -- for (let i = 0; i < Hypr.monitors.length; i++) { -- if (Hypr.monitors[i].id === id) -- return Hypr.monitors[i]; -+ const monitors = sourceMonitors(); -+ for (let i = 0; i < monitors.length; i++) { -+ if (monitors[i].id === id) -+ return monitors[i]; - } - return null; - } - - // Build the monitor string Hyprland expects: - // NAME,WIDTHxHEIGHT@RATE,XxY,SCALE[,transform,N] -- function monitorStr(mon: var, overrideScale: real, overrideTransform: int): string { -- const scale = overrideScale >= 0 ? overrideScale : (mon.scale || 1); -+ function monitorStr(mon: var, overrideScale: real, overrideTransform: int, overrideRefreshRate: real): string { -+ const scale = overrideScale >= 0 ? overrideScale : (mon.scale || 1); - const transform = overrideTransform >= 0 ? overrideTransform : (mon.transform || 0); -- const rr = (mon.refreshRate || 60).toFixed(3); -+ const rr = (overrideRefreshRate > 0 ? overrideRefreshRate : (mon.refreshRate || 60)).toFixed(3); - let s = `${mon.name},${mon.width}x${mon.height}@${rr},${mon.x}x${mon.y},${scale}`; - if (transform !== 0) - s += `,transform,${transform}`; -@@ -62,6 +70,7 @@ Singleton { - // "keyword" is a config command, not a dispatcher action. - function sendKeyword(monStr: string): void { - Hypr.extras.batchMessage([`keyword monitor ${monStr}`]); -+ Hyprctl.update(); - } - - function arrange(monitorName: string, pos: string, relativeToId: int): void { -@@ -82,7 +91,7 @@ Singleton { - else if (pos === "top") y -= movingH; - else if (pos === "bottom") y += targetH; - -- sendKeyword(monitorStr(moving, moving.scale || 1, moving.transform || 0) -+ sendKeyword(monitorStr(moving, moving.scale || 1, moving.transform || 0, moving.refreshRate || 60) - .replace(`${moving.x}x${moving.y}`, `${Math.round(x)}x${Math.round(y)}`)); - } - -@@ -95,13 +104,19 @@ Singleton { - else if (angle === 180) transform = 2; - else if (angle === 270) transform = 3; - -- sendKeyword(monitorStr(mon, mon.scale || 1, transform)); -+ sendKeyword(monitorStr(mon, mon.scale || 1, transform, mon.refreshRate || 60)); - } - - function setScale(monitorName: string, scale: real): void { - const mon = findMonitorByName(monitorName); - if (!mon) return; - const s = Math.max(0.5, Math.min(3.0, scale)); -- sendKeyword(monitorStr(mon, s, mon.transform || 0)); -+ sendKeyword(monitorStr(mon, s, mon.transform || 0, mon.refreshRate || 60)); -+ } -+ -+ function setRefreshRate(monitorName: string, refreshRate: real): void { -+ const mon = findMonitorByName(monitorName); -+ if (!mon) return; -+ sendKeyword(monitorStr(mon, mon.scale || 1, mon.transform || 0, Math.max(1, refreshRate))); - } - } -diff --git a/services/Network.qml b/services/Network.qml -index f3dfc3ea..4e0b809c 100644 ---- a/services/Network.qml -+++ b/services/Network.qml -@@ -1,44 +1,28 @@ - pragma Singleton - -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick - import qs.services - - Singleton { - id: root - -- Component.onCompleted: { -- // Trigger ethernet device detection after initialization -- Qt.callLater(() => { -- getEthernetDevices(); -- }); -- // Load saved connections on startup -- Nmcli.loadSavedConnections(() => { -- root.savedConnections = Nmcli.savedConnections; -- root.savedConnectionSsids = Nmcli.savedConnectionSsids; -- }); -- // Get initial WiFi status -- Nmcli.getWifiStatus(enabled => { -- root.wifiEnabled = enabled; -- }); -- // Sync networks from Nmcli on startup -- Qt.callLater(() => { -- syncNetworksFromNmcli(); -- }, 100); -- } -- - readonly property list networks: [] - readonly property AccessPoint active: networks.find(n => n.active) ?? null - property bool wifiEnabled: true - readonly property bool scanning: Nmcli.scanning -- - property list ethernetDevices: [] - readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null - property int ethernetDeviceCount: 0 - property bool ethernetProcessRunning: false - property var ethernetDeviceDetails: null - property var wirelessDeviceDetails: null -+ property var pendingConnection: null -+ property list savedConnections: [] -+ property list savedConnectionSsids: [] -+ -+ signal connectionFailed(string ssid) - - function enableWifi(enabled: bool): void { - Nmcli.enableWifi(enabled, result => { -@@ -66,9 +50,6 @@ Singleton { - Nmcli.rescanWifi(); - } - -- property var pendingConnection: null -- signal connectionFailed(string ssid) -- - function connectToNetwork(ssid: string, password: string, bssid: string, callback: var): void { - // Set up pending connection tracking if callback provided - if (callback) { -@@ -159,20 +140,6 @@ Singleton { - }); - } - -- property list savedConnections: [] -- property list savedConnectionSsids: [] -- -- // Sync saved connections from Nmcli when they're updated -- Connections { -- target: Nmcli -- function onSavedConnectionsChanged() { -- root.savedConnections = Nmcli.savedConnections; -- } -- function onSavedConnectionSsidsChanged() { -- root.savedConnectionSsids = Nmcli.savedConnectionSsids; -- } -- } -- - function syncNetworksFromNmcli(): void { - const rNetworks = root.networks; - const nNetworks = Nmcli.networks; -@@ -217,22 +184,6 @@ Singleton { - } - } - -- component AccessPoint: QtObject { -- required property var lastIpcObject -- readonly property string ssid: lastIpcObject.ssid -- readonly property string bssid: lastIpcObject.bssid -- readonly property int strength: lastIpcObject.strength -- readonly property int frequency: lastIpcObject.frequency -- readonly property bool active: lastIpcObject.active -- readonly property string security: lastIpcObject.security -- readonly property bool isSecure: security.length > 0 -- } -- -- Component { -- id: apComp -- AccessPoint {} -- } -- - function hasSavedProfile(ssid: string): bool { - // Use Nmcli's hasSavedProfile which has the same logic - return Nmcli.hasSavedProfile(ssid); -@@ -309,16 +260,73 @@ Singleton { - return octets.join("."); - } - -+ Component.onCompleted: { -+ // Trigger ethernet device detection after initialization -+ Qt.callLater(() => { -+ getEthernetDevices(); -+ }); -+ // Load saved connections on startup -+ Nmcli.loadSavedConnections(() => { -+ root.savedConnections = Nmcli.savedConnections; -+ root.savedConnectionSsids = Nmcli.savedConnectionSsids; -+ }); -+ // Get initial WiFi status -+ Nmcli.getWifiStatus(enabled => { -+ root.wifiEnabled = enabled; -+ }); -+ // Sync networks from Nmcli on startup -+ Qt.callLater(() => { -+ syncNetworksFromNmcli(); -+ }, 100); -+ } -+ -+ // Sync saved connections from Nmcli when they're updated -+ Connections { -+ function onSavedConnectionsChanged() { -+ root.savedConnections = Nmcli.savedConnections; -+ } -+ -+ function onSavedConnectionSsidsChanged() { -+ root.savedConnectionSsids = Nmcli.savedConnectionSsids; -+ } -+ -+ target: Nmcli -+ } -+ -+ Timer { -+ id: monitorDebounce -+ -+ interval: 200 -+ onTriggered: { -+ Nmcli.getNetworks(() => { -+ syncNetworksFromNmcli(); -+ }); -+ getEthernetDevices(); -+ } -+ } -+ - Process { - running: true - command: ["nmcli", "m"] - stdout: SplitParser { -- onRead: { -- Nmcli.getNetworks(() => { -- syncNetworksFromNmcli(); -- }); -- getEthernetDevices(); -- } -+ onRead: monitorDebounce.start() - } - } -+ -+ Component { -+ id: apComp -+ -+ AccessPoint {} -+ } -+ -+ component AccessPoint: QtObject { -+ required property var lastIpcObject -+ readonly property string ssid: lastIpcObject.ssid -+ readonly property string bssid: lastIpcObject.bssid -+ readonly property int strength: lastIpcObject.strength -+ readonly property int frequency: lastIpcObject.frequency -+ readonly property bool active: lastIpcObject.active -+ readonly property string security: lastIpcObject.security -+ readonly property bool isSecure: security.length > 0 -+ } - } -diff --git a/services/NetworkUsage.qml b/services/NetworkUsage.qml -new file mode 100644 -index 00000000..6c4dc8d2 ---- /dev/null -+++ b/services/NetworkUsage.qml -@@ -0,0 +1,229 @@ -+pragma Singleton -+ -+import QtQuick -+import Quickshell -+import Quickshell.Io -+import Caelestia.Config -+import Caelestia.Internal -+ -+Singleton { -+ id: root -+ -+ property int refCount: 0 -+ -+ // Current speeds in bytes per second -+ readonly property real downloadSpeed: _downloadSpeed -+ readonly property real uploadSpeed: _uploadSpeed -+ -+ // Total bytes transferred since tracking started -+ readonly property real downloadTotal: _downloadTotal -+ readonly property real uploadTotal: _uploadTotal -+ -+ // History buffers for sparkline -+ readonly property CircularBuffer downloadBuffer: _downloadBuffer -+ readonly property CircularBuffer uploadBuffer: _uploadBuffer -+ readonly property int historyLength: 30 -+ -+ // Private properties -+ property real _downloadSpeed: 0 -+ property real _uploadSpeed: 0 -+ property real _downloadTotal: 0 -+ property real _uploadTotal: 0 -+ -+ // Previous readings for calculating speed -+ property real _prevRxBytes: 0 -+ property real _prevTxBytes: 0 -+ property real _prevTimestamp: 0 -+ -+ // Initial readings for calculating totals -+ property real _initialRxBytes: 0 -+ property real _initialTxBytes: 0 -+ property bool _initialized: false -+ -+ function formatBytes(bytes: real): var { -+ // Handle negative or invalid values -+ if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { -+ return { -+ value: 0, -+ unit: "B/s" -+ }; -+ } -+ -+ if (bytes < 1024) { -+ return { -+ value: bytes, -+ unit: "B/s" -+ }; -+ } else if (bytes < 1024 * 1024) { -+ return { -+ value: bytes / 1024, -+ unit: "KB/s" -+ }; -+ } else if (bytes < 1024 * 1024 * 1024) { -+ return { -+ value: bytes / (1024 * 1024), -+ unit: "MB/s" -+ }; -+ } else { -+ return { -+ value: bytes / (1024 * 1024 * 1024), -+ unit: "GB/s" -+ }; -+ } -+ } -+ -+ function formatBytesTotal(bytes: real): var { -+ // Handle negative or invalid values -+ if (bytes < 0 || isNaN(bytes) || !isFinite(bytes)) { -+ return { -+ value: 0, -+ unit: "B" -+ }; -+ } -+ -+ if (bytes < 1024) { -+ return { -+ value: bytes, -+ unit: "B" -+ }; -+ } else if (bytes < 1024 * 1024) { -+ return { -+ value: bytes / 1024, -+ unit: "KB" -+ }; -+ } else if (bytes < 1024 * 1024 * 1024) { -+ return { -+ value: bytes / (1024 * 1024), -+ unit: "MB" -+ }; -+ } else { -+ return { -+ value: bytes / (1024 * 1024 * 1024), -+ unit: "GB" -+ }; -+ } -+ } -+ -+ function parseNetDev(content: string): var { -+ const lines = content.split("\n"); -+ let totalRx = 0; -+ let totalTx = 0; -+ -+ for (let i = 2; i < lines.length; i++) { -+ const line = lines[i].trim(); -+ if (!line) -+ continue; -+ -+ const parts = line.split(/\s+/); -+ if (parts.length < 10) -+ continue; -+ -+ const iface = parts[0].replace(":", ""); -+ // Skip loopback interface -+ if (iface === "lo") -+ continue; -+ -+ const rxBytes = parseFloat(parts[1]) || 0; -+ const txBytes = parseFloat(parts[9]) || 0; -+ -+ totalRx += rxBytes; -+ totalTx += txBytes; -+ } -+ -+ return { -+ rx: totalRx, -+ tx: totalTx -+ }; -+ } -+ -+ CircularBuffer { -+ id: _downloadBuffer -+ -+ capacity: root.historyLength + 1 -+ } -+ -+ CircularBuffer { -+ id: _uploadBuffer -+ -+ capacity: root.historyLength + 1 -+ } -+ -+ FileView { -+ id: netDevFile -+ -+ path: "/proc/net/dev" -+ } -+ -+ Timer { -+ interval: GlobalConfig.dashboard.resourceUpdateInterval -+ running: root.refCount > 0 -+ repeat: true -+ triggeredOnStart: true -+ -+ onTriggered: { -+ netDevFile.reload(); -+ const content = netDevFile.text(); -+ if (!content) -+ return; -+ -+ const data = root.parseNetDev(content); -+ const now = Date.now(); -+ -+ if (!root._initialized) { -+ root._initialRxBytes = data.rx; -+ root._initialTxBytes = data.tx; -+ root._prevRxBytes = data.rx; -+ root._prevTxBytes = data.tx; -+ root._prevTimestamp = now; -+ root._initialized = true; -+ return; -+ } -+ -+ const timeDelta = (now - root._prevTimestamp) / 1000; // seconds -+ if (timeDelta > 0) { -+ // Calculate byte deltas -+ let rxDelta = data.rx - root._prevRxBytes; -+ let txDelta = data.tx - root._prevTxBytes; -+ -+ // Handle counter overflow (when counters wrap around from max to 0) -+ // This happens when counters exceed 32-bit or 64-bit limits -+ if (rxDelta < 0) { -+ // Counter wrapped around - assume 64-bit counter -+ rxDelta += Math.pow(2, 64); -+ } -+ if (txDelta < 0) { -+ txDelta += Math.pow(2, 64); -+ } -+ -+ // Calculate speeds -+ root._downloadSpeed = rxDelta / timeDelta; -+ root._uploadSpeed = txDelta / timeDelta; -+ -+ if (root._downloadSpeed >= 0 && isFinite(root._downloadSpeed)) -+ _downloadBuffer.push(root._downloadSpeed); -+ -+ if (root._uploadSpeed >= 0 && isFinite(root._uploadSpeed)) -+ _uploadBuffer.push(root._uploadSpeed); -+ } -+ -+ // Calculate totals with overflow handling -+ let downTotal = data.rx - root._initialRxBytes; -+ let upTotal = data.tx - root._initialTxBytes; -+ -+ // Handle counter overflow for totals -+ if (downTotal < 0) { -+ downTotal += Math.pow(2, 64); -+ } -+ if (upTotal < 0) { -+ upTotal += Math.pow(2, 64); -+ } -+ -+ root._downloadTotal = downTotal; -+ root._uploadTotal = upTotal; -+ -+ root._prevRxBytes = data.rx; -+ root._prevTxBytes = data.tx; -+ root._prevTimestamp = now; -+ } -+ } -+} -diff --git a/services/Nmcli.qml b/services/Nmcli.qml -index 36bd3e6d..ea5d7eb3 100644 ---- a/services/Nmcli.qml -+++ b/services/Nmcli.qml -@@ -1,9 +1,9 @@ - pragma Singleton - pragma ComponentBehavior: Bound - -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick - - Singleton { - id: root -@@ -24,14 +24,15 @@ Singleton { - property var wifiConnectionQueue: [] - property int currentSsidQueryIndex: 0 - property var pendingConnection: null -- signal connectionFailed(string ssid) - property var wirelessDeviceDetails: null - property var ethernetDeviceDetails: null - property list ethernetDevices: [] - readonly property var activeEthernet: ethernetDevices.find(d => d.connected) ?? null -- - property list activeProcesses: [] - -+ readonly property alias connectionCheckTimer: connectionCheckTimer -+ readonly property alias immediateCheckTimer: immediateCheckTimer -+ - // Constants - readonly property string deviceTypeWifi: "wifi" - readonly property string deviceTypeEthernet: "ethernet" -@@ -55,6 +56,8 @@ Singleton { - readonly property string connectionParamPassword: "password" - readonly property string connectionParamBssid: "802-11-wireless.bssid" - -+ signal connectionFailed(string ssid) -+ - function detectPasswordRequired(error: string): bool { - if (!error || error.length === 0) { - return false; -@@ -165,7 +168,7 @@ Singleton { - - function executeCommand(args: list, callback: var): void { - const proc = commandProc.createObject(root); -- proc.command = ["nmcli", ...args]; -+ proc.cmdArgs = ["nmcli", ...args]; - proc.callback = callback; - - activeProcesses.push(proc); -@@ -178,7 +181,7 @@ Singleton { - }); - - Qt.callLater(() => { -- proc.exec(proc.command); -+ proc.exec(proc.cmdArgs); - }); - } - -@@ -388,7 +391,7 @@ Singleton { - } - - if (!result.success && root.pendingConnection && retries < maxRetries) { -- console.warn("[NMCLI] Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); -+ console.warn(lc, "Connection failed, retrying... (attempt " + (retries + 1) + "/" + maxRetries + ")"); - Qt.callLater(() => { - connectWireless(ssid, password, bssid, callback, retries + 1); - }, 1000); -@@ -414,7 +417,7 @@ Singleton { - loadSavedConnections(() => {}); - activateConnection(ssid, callback); - } else { -- console.warn("[NMCLI] Connection profile creation failed, trying fallback..."); -+ console.warn(lc, "Connection profile creation failed, trying fallback..."); - let fallbackCmd = [root.nmcliCommandDevice, root.nmcliCommandWifi, "connect", ssid, root.connectionParamPassword, password]; - executeCommand(fallbackCmd, fallbackResult => { - if (callback) -@@ -751,17 +754,25 @@ Singleton { - const networks = deduplicateNetworks(allNetworks); - const rNetworks = root.networks; - -- const destroyed = rNetworks.filter(rn => !networks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid)); -- for (const network of destroyed) { -- const index = rNetworks.indexOf(network); -- if (index >= 0) { -- rNetworks.splice(index, 1); -- network.destroy(); -+ const newMap = new Map(); -+ for (const n of networks) -+ newMap.set(`${n.frequency}:${n.ssid}:${n.bssid}`, n); -+ -+ for (let i = rNetworks.length - 1; i >= 0; i--) { -+ const rn = rNetworks[i]; -+ const key = `${rn.frequency}:${rn.ssid}:${rn.bssid}`; -+ if (!newMap.has(key)) { -+ rNetworks.splice(i, 1); -+ rn.destroy(); - } - } - -- for (const network of networks) { -- const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid); -+ const existingMap = new Map(); -+ for (const rn of rNetworks) -+ existingMap.set(`${rn.frequency}:${rn.ssid}:${rn.bssid}`, rn); -+ -+ for (const [key, network] of newMap) { -+ const match = existingMap.get(key); - if (match) { - match.lastIpcObject = network; - } else { -@@ -829,7 +840,7 @@ Singleton { - return false; - } - -- if (!isConnectionCommand(proc.command) || !root.pendingConnection || !root.pendingConnection.callback) { -+ if (!isConnectionCommand(proc.cmdArgs) || !root.pendingConnection || !root.pendingConnection.callback) { - return false; - } - -@@ -861,247 +872,6 @@ Singleton { - return false; - } - -- component CommandProcess: Process { -- id: proc -- -- property var callback: null -- property list command: [] -- property bool callbackCalled: false -- property int exitCode: 0 -- -- signal processFinished -- -- environment: ({ -- LANG: "C.UTF-8", -- LC_ALL: "C.UTF-8" -- }) -- -- stdout: StdioCollector { -- id: stdoutCollector -- } -- -- stderr: StdioCollector { -- id: stderrCollector -- -- onStreamFinished: { -- const error = text.trim(); -- if (error && error.length > 0) { -- const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; -- root.handlePasswordRequired(proc, error, output, -1); -- } -- } -- } -- -- onExited: code => { -- exitCode = code; -- -- Qt.callLater(() => { -- if (callbackCalled) { -- processFinished(); -- return; -- } -- -- if (proc.callback) { -- const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; -- const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; -- const success = exitCode === 0; -- const cmdIsConnection = isConnectionCommand(proc.command); -- -- if (root.handlePasswordRequired(proc, error, output, exitCode)) { -- processFinished(); -- return; -- } -- -- const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); -- -- if (!success && cmdIsConnection && root.pendingConnection) { -- const failedSsid = root.pendingConnection.ssid; -- root.connectionFailed(failedSsid); -- } -- -- callbackCalled = true; -- callback({ -- success: success, -- output: output, -- error: error, -- exitCode: proc.exitCode, -- needsPassword: needsPassword || false -- }); -- processFinished(); -- } else { -- processFinished(); -- } -- }); -- } -- } -- -- Component { -- id: commandProc -- -- CommandProcess {} -- } -- -- component AccessPoint: QtObject { -- required property var lastIpcObject -- readonly property string ssid: lastIpcObject.ssid -- readonly property string bssid: lastIpcObject.bssid -- readonly property int strength: lastIpcObject.strength -- readonly property int frequency: lastIpcObject.frequency -- readonly property bool active: lastIpcObject.active -- readonly property string security: lastIpcObject.security -- readonly property bool isSecure: security.length > 0 -- } -- -- Component { -- id: apComp -- -- AccessPoint {} -- } -- -- Timer { -- id: connectionCheckTimer -- -- interval: 4000 -- onTriggered: { -- if (root.pendingConnection) { -- const connected = root.active && root.active.ssid === root.pendingConnection.ssid; -- -- if (!connected && root.pendingConnection.callback) { -- let foundPasswordError = false; -- for (let i = 0; i < root.activeProcesses.length; i++) { -- const proc = root.activeProcesses[i]; -- if (proc && proc.stderr && proc.stderr.text) { -- const error = proc.stderr.text.trim(); -- if (error && error.length > 0) { -- if (root.isConnectionCommand(proc.command)) { -- const needsPassword = root.detectPasswordRequired(error); -- -- if (needsPassword && !proc.callbackCalled && root.pendingConnection) { -- const pending = root.pendingConnection; -- root.pendingConnection = null; -- immediateCheckTimer.stop(); -- immediateCheckTimer.checkCount = 0; -- proc.callbackCalled = true; -- const result = { -- success: false, -- output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", -- error: error, -- exitCode: -1, -- needsPassword: true -- }; -- if (pending.callback) { -- pending.callback(result); -- } -- if (proc.callback && proc.callback !== pending.callback) { -- proc.callback(result); -- } -- foundPasswordError = true; -- break; -- } -- } -- } -- } -- } -- -- if (!foundPasswordError) { -- const pending = root.pendingConnection; -- const failedSsid = pending.ssid; -- root.pendingConnection = null; -- immediateCheckTimer.stop(); -- immediateCheckTimer.checkCount = 0; -- root.connectionFailed(failedSsid); -- pending.callback({ -- success: false, -- output: "", -- error: "Connection timeout", -- exitCode: -1, -- needsPassword: false -- }); -- } -- } else if (connected) { -- root.pendingConnection = null; -- immediateCheckTimer.stop(); -- immediateCheckTimer.checkCount = 0; -- } -- } -- } -- } -- -- Timer { -- id: immediateCheckTimer -- -- property int checkCount: 0 -- -- interval: 500 -- repeat: true -- triggeredOnStart: false -- -- onTriggered: { -- if (root.pendingConnection) { -- checkCount++; -- const connected = root.active && root.active.ssid === root.pendingConnection.ssid; -- -- if (connected) { -- connectionCheckTimer.stop(); -- immediateCheckTimer.stop(); -- immediateCheckTimer.checkCount = 0; -- if (root.pendingConnection.callback) { -- root.pendingConnection.callback({ -- success: true, -- output: "Connected", -- error: "", -- exitCode: 0 -- }); -- } -- root.pendingConnection = null; -- } else { -- for (let i = 0; i < root.activeProcesses.length; i++) { -- const proc = root.activeProcesses[i]; -- if (proc && proc.stderr && proc.stderr.text) { -- const error = proc.stderr.text.trim(); -- if (error && error.length > 0) { -- if (root.isConnectionCommand(proc.command)) { -- const needsPassword = root.detectPasswordRequired(error); -- -- if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { -- connectionCheckTimer.stop(); -- immediateCheckTimer.stop(); -- immediateCheckTimer.checkCount = 0; -- const pending = root.pendingConnection; -- root.pendingConnection = null; -- proc.callbackCalled = true; -- const result = { -- success: false, -- output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", -- error: error, -- exitCode: -1, -- needsPassword: true -- }; -- if (pending.callback) { -- pending.callback(result); -- } -- if (proc.callback && proc.callback !== pending.callback) { -- proc.callback(result); -- } -- return; -- } -- } -- } -- } -- } -- -- if (checkCount >= 6) { -- immediateCheckTimer.stop(); -- immediateCheckTimer.checkCount = 0; -- } -- } -- } else { -- immediateCheckTimer.stop(); -- immediateCheckTimer.checkCount = 0; -- } -- } -- } -- - function checkPendingConnection(): void { - if (root.pendingConnection) { - Qt.callLater(() => { -@@ -1253,39 +1023,9 @@ Singleton { - return details; - } - -- Process { -- id: rescanProc -- -- command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] -- onExited: root.getNetworks() -- } -- -- Process { -- id: monitorProc -- -- running: true -- command: ["nmcli", "monitor"] -- environment: ({ -- LANG: "C.UTF-8", -- LC_ALL: "C.UTF-8" -- }) -- stdout: SplitParser { -- onRead: root.refreshOnConnectionChange() -- } -- onExited: monitorRestartTimer.start() -- } -- -- Timer { -- id: monitorRestartTimer -- interval: 2000 -- onTriggered: { -- monitorProc.running = true; -- } -- } -- -- function refreshOnConnectionChange(): void { -- getNetworks(networks => { -- const newActive = root.active; -+ function refreshOnConnectionChange(): void { -+ getNetworks(networks => { -+ const newActive = root.active; - - if (newActive && newActive.active) { - Qt.callLater(() => { -@@ -1349,4 +1089,283 @@ Singleton { - } - }, 2000); - } -+ -+ Component { -+ id: commandProc -+ -+ CommandProcess {} -+ } -+ -+ Component { -+ id: apComp -+ -+ AccessPoint {} -+ } -+ -+ Timer { -+ id: connectionCheckTimer -+ -+ interval: 4000 -+ onTriggered: { -+ if (root.pendingConnection) { -+ const connected = root.active && root.active.ssid === root.pendingConnection.ssid; -+ -+ if (!connected && root.pendingConnection.callback) { -+ let foundPasswordError = false; -+ for (let i = 0; i < root.activeProcesses.length; i++) { -+ const proc = root.activeProcesses[i]; -+ if (proc && proc.stderr && proc.stderr.text) { -+ const error = proc.stderr.text.trim(); -+ if (error && error.length > 0) { -+ if (root.isConnectionCommand(proc.cmdArgs)) { -+ const needsPassword = root.detectPasswordRequired(error); -+ -+ if (needsPassword && !proc.callbackCalled && root.pendingConnection) { -+ const pending = root.pendingConnection; -+ root.pendingConnection = null; -+ immediateCheckTimer.stop(); -+ immediateCheckTimer.checkCount = 0; -+ proc.callbackCalled = true; -+ const result = { -+ success: false, -+ output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", -+ error: error, -+ exitCode: -1, -+ needsPassword: true -+ }; -+ if (pending.callback) { -+ pending.callback(result); -+ } -+ if (proc.callback && proc.callback !== pending.callback) { -+ proc.callback(result); -+ } -+ foundPasswordError = true; -+ break; -+ } -+ } -+ } -+ } -+ } -+ -+ if (!foundPasswordError) { -+ const pending = root.pendingConnection; -+ const failedSsid = pending.ssid; -+ root.pendingConnection = null; -+ immediateCheckTimer.stop(); -+ immediateCheckTimer.checkCount = 0; -+ root.connectionFailed(failedSsid); -+ pending.callback({ -+ success: false, -+ output: "", -+ error: "Connection timeout", -+ exitCode: -1, -+ needsPassword: false -+ }); -+ } -+ } else if (connected) { -+ root.pendingConnection = null; -+ immediateCheckTimer.stop(); -+ immediateCheckTimer.checkCount = 0; -+ } -+ } -+ } -+ } -+ -+ Timer { -+ id: immediateCheckTimer -+ -+ property int checkCount: 0 -+ -+ interval: 500 -+ repeat: true -+ triggeredOnStart: false -+ -+ onTriggered: { -+ if (root.pendingConnection) { -+ checkCount++; -+ const connected = root.active && root.active.ssid === root.pendingConnection.ssid; -+ -+ if (connected) { -+ connectionCheckTimer.stop(); -+ immediateCheckTimer.stop(); -+ immediateCheckTimer.checkCount = 0; -+ if (root.pendingConnection.callback) { -+ root.pendingConnection.callback({ -+ success: true, -+ output: "Connected", -+ error: "", -+ exitCode: 0 -+ }); -+ } -+ root.pendingConnection = null; -+ } else { -+ for (let i = 0; i < root.activeProcesses.length; i++) { -+ const proc = root.activeProcesses[i]; -+ if (proc && proc.stderr && proc.stderr.text) { -+ const error = proc.stderr.text.trim(); -+ if (error && error.length > 0) { -+ if (root.isConnectionCommand(proc.cmdArgs)) { -+ const needsPassword = root.detectPasswordRequired(error); -+ -+ if (needsPassword && !proc.callbackCalled && root.pendingConnection && root.pendingConnection.callback) { -+ connectionCheckTimer.stop(); -+ immediateCheckTimer.stop(); -+ immediateCheckTimer.checkCount = 0; -+ const pending = root.pendingConnection; -+ root.pendingConnection = null; -+ proc.callbackCalled = true; -+ const result = { -+ success: false, -+ output: (proc.stdout && proc.stdout.text) ? proc.stdout.text : "", -+ error: error, -+ exitCode: -1, -+ needsPassword: true -+ }; -+ if (pending.callback) { -+ pending.callback(result); -+ } -+ if (proc.callback && proc.callback !== pending.callback) { -+ proc.callback(result); -+ } -+ return; -+ } -+ } -+ } -+ } -+ } -+ -+ if (checkCount >= 6) { -+ immediateCheckTimer.stop(); -+ immediateCheckTimer.checkCount = 0; -+ } -+ } -+ } else { -+ immediateCheckTimer.stop(); -+ immediateCheckTimer.checkCount = 0; -+ } -+ } -+ } -+ -+ Process { -+ id: rescanProc -+ -+ command: ["nmcli", "dev", root.nmcliCommandWifi, "list", "--rescan", "yes"] -+ onExited: root.getNetworks() // qmllint disable signal-handler-parameters -+ } -+ -+ Process { -+ id: monitorProc -+ -+ running: true -+ command: ["nmcli", "monitor"] -+ environment: ({ -+ LANG: "C.UTF-8", -+ LC_ALL: "C.UTF-8" -+ }) -+ stdout: SplitParser { -+ onRead: root.refreshOnConnectionChange() -+ } -+ onExited: monitorRestartTimer.start() // qmllint disable signal-handler-parameters -+ } -+ -+ Timer { -+ id: monitorRestartTimer -+ -+ interval: 2000 -+ onTriggered: { -+ monitorProc.running = true; -+ } -+ } -+ -+ LoggingCategory { -+ id: lc -+ -+ name: "caelestia.qml.services.nmcli" -+ defaultLogLevel: LoggingCategory.Info -+ } -+ -+ component CommandProcess: Process { -+ id: proc -+ -+ property var callback: null -+ property list cmdArgs: [] -+ property bool callbackCalled: false -+ property int exitCode: 0 -+ -+ signal processFinished -+ -+ environment: ({ -+ LANG: "C.UTF-8", -+ LC_ALL: "C.UTF-8" -+ }) -+ -+ stdout: StdioCollector { -+ id: stdoutCollector -+ } -+ -+ stderr: StdioCollector { -+ id: stderrCollector -+ -+ onStreamFinished: { -+ const error = text.trim(); -+ if (error && error.length > 0) { -+ const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; -+ root.handlePasswordRequired(proc, error, output, -1); -+ } -+ } -+ } -+ -+ onExited: code => { // qmllint disable signal-handler-parameters -+ exitCode = code; -+ -+ Qt.callLater(() => { -+ if (callbackCalled) { -+ processFinished(); -+ return; -+ } -+ -+ if (proc.callback) { -+ const output = (stdoutCollector && stdoutCollector.text) ? stdoutCollector.text : ""; -+ const error = (stderrCollector && stderrCollector.text) ? stderrCollector.text : ""; -+ const success = exitCode === 0; -+ const cmdIsConnection = isConnectionCommand(proc.cmdArgs); -+ -+ if (root.handlePasswordRequired(proc, error, output, exitCode)) { -+ processFinished(); -+ return; -+ } -+ -+ const needsPassword = cmdIsConnection && root.detectPasswordRequired(error); -+ -+ if (!success && cmdIsConnection && root.pendingConnection) { -+ const failedSsid = root.pendingConnection.ssid; -+ root.connectionFailed(failedSsid); -+ } -+ -+ callbackCalled = true; -+ callback({ -+ success: success, -+ output: output, -+ error: error, -+ exitCode: proc.exitCode, -+ needsPassword: needsPassword || false -+ }); -+ processFinished(); -+ } else { -+ processFinished(); -+ } -+ }); -+ } -+ } -+ -+ component AccessPoint: QtObject { -+ required property var lastIpcObject -+ readonly property string ssid: lastIpcObject.ssid -+ readonly property string bssid: lastIpcObject.bssid -+ readonly property int strength: lastIpcObject.strength -+ readonly property int frequency: lastIpcObject.frequency -+ readonly property bool active: lastIpcObject.active -+ readonly property string security: lastIpcObject.security -+ readonly property bool isSecure: security.length > 0 -+ } - } -diff --git a/services/NotifData.qml b/services/NotifData.qml -new file mode 100644 -index 00000000..3c6ae23b ---- /dev/null -+++ b/services/NotifData.qml -@@ -0,0 +1,243 @@ -+pragma ComponentBehavior: Bound -+ -+import QtQuick -+import Quickshell -+import Quickshell.Services.Notifications -+import Caelestia -+import Caelestia.Config -+import qs.services -+import qs.utils -+ -+QtObject { -+ id: notif -+ -+ property bool popup -+ property bool closed -+ property var locks: new Set() -+ -+ property date time: new Date() -+ property string timeStr: qsTr("now") -+ -+ readonly property Timer timeStrTimer: Timer { -+ running: !notif.closed -+ repeat: true -+ interval: 5000 -+ onTriggered: notif.updateTimeStr() -+ } -+ -+ property Notification notification -+ property string id -+ property string summary -+ property string body -+ property string appIcon -+ property string appName -+ property string image -+ property var hints // Hints are not persisted across restarts -+ property real expireTimeout: GlobalConfig.notifs.defaultExpireTimeout -+ property int urgency: NotificationUrgency.Normal -+ property bool resident -+ property bool hasActionIcons -+ property list actions -+ -+ readonly property bool hasFullscreen: { -+ const monitor = Hypr.focusedMonitor; -+ const specialName = monitor?.lastIpcObject.specialWorkspace?.name; -+ if (specialName) { -+ const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); -+ return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; -+ } -+ return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; -+ } -+ -+ readonly property Timer timer: Timer { -+ running: true -+ interval: notif.expireTimeout > 0 ? notif.expireTimeout : notif.hasFullscreen ? GlobalConfig.notifs.fullscreenExpireTimeout : GlobalConfig.notifs.defaultExpireTimeout -+ onTriggered: { -+ // Always expire if the active workspace has a fullscreen window -+ if (GlobalConfig.notifs.expire || notif.hasFullscreen) -+ notif.popup = false; -+ } -+ } -+ -+ readonly property LazyLoader dummyImageLoader: LazyLoader { -+ active: false -+ -+ // qmllint disable uncreatable-type -+ PanelWindow { -+ // qmllint enable uncreatable-type -+ implicitWidth: TokenConfig.sizes.notifs.image -+ implicitHeight: TokenConfig.sizes.notifs.image -+ color: "transparent" -+ mask: Region {} -+ -+ Image { -+ function tryCache(): void { -+ if (status !== Image.Ready || width != TokenConfig.sizes.notifs.image || height != TokenConfig.sizes.notifs.image) -+ return; -+ -+ const cacheKey = notif.appName + notif.summary + notif.id + notif.image; -+ let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; -+ for (let i = 0; i < cacheKey.length; i++) { -+ ch = cacheKey.charCodeAt(i); -+ h1 = Math.imul(h1 ^ ch, 2654435761); -+ h2 = Math.imul(h2 ^ ch, 1597334677); -+ } -+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); -+ h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); -+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); -+ h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); -+ const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); -+ -+ const cache = `${Paths.notifimagecache}/${hash}.png`; -+ CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { -+ notif.image = cache; -+ notif.dummyImageLoader.active = false; -+ }); -+ } -+ -+ anchors.fill: parent -+ source: Qt.resolvedUrl(notif.image) -+ fillMode: Image.PreserveAspectCrop -+ cache: false -+ asynchronous: true -+ opacity: 0 -+ -+ onStatusChanged: tryCache() -+ onWidthChanged: tryCache() -+ onHeightChanged: tryCache() -+ } -+ } -+ } -+ -+ readonly property Connections conn: Connections { -+ function onClosed(): void { -+ notif.close(); -+ } -+ -+ function onSummaryChanged(): void { -+ notif.summary = notif.notification.summary; -+ } -+ -+ function onBodyChanged(): void { -+ notif.body = notif.notification.body; -+ } -+ -+ function onAppIconChanged(): void { -+ notif.appIcon = notif.notification.appIcon; -+ } -+ -+ function onAppNameChanged(): void { -+ notif.appName = notif.notification.appName; -+ } -+ -+ function onImageChanged(): void { -+ notif.image = notif.notification.image; -+ notif.maybeTriggerDummyImageLoader(); -+ } -+ -+ function onExpireTimeoutChanged(): void { -+ notif.expireTimeout = notif.notification.expireTimeout; -+ } -+ -+ function onUrgencyChanged(): void { -+ notif.urgency = notif.notification.urgency; -+ } -+ -+ function onResidentChanged(): void { -+ notif.resident = notif.notification.resident; -+ } -+ -+ function onHasActionIconsChanged(): void { -+ notif.hasActionIcons = notif.notification.hasActionIcons; -+ } -+ -+ function onActionsChanged(): void { -+ // qmllint disable unresolved-type -+ notif.actions = notif.notification.actions.map(a => ({ -+ // qmllint enable unresolved-type -+ identifier: a.identifier, -+ text: a.text, -+ invoke: () => a.invoke() -+ })); -+ } -+ -+ function onHintsChanged(): void { -+ notif.hints = notif.notification.hints; -+ } -+ -+ target: notif.notification -+ } -+ -+ function updateTimeStr(): void { -+ const diff = Date.now() - time.getTime(); -+ const m = Math.floor(diff / 60000); -+ -+ if (m < 1) { -+ timeStr = qsTr("now"); -+ timeStrTimer.interval = 5000; -+ } else { -+ const h = Math.floor(m / 60); -+ const d = Math.floor(h / 24); -+ -+ if (d > 0) { -+ timeStr = `${d}d`; -+ timeStrTimer.interval = 3600000; -+ } else if (h > 0) { -+ timeStr = `${h}h`; -+ timeStrTimer.interval = 300000; -+ } else { -+ timeStr = `${m}m`; -+ timeStrTimer.interval = m < 10 ? 30000 : 60000; -+ } -+ } -+ } -+ -+ function maybeTriggerDummyImageLoader(): void { -+ if (image && !image.startsWith("image://icon/") && !image.startsWith(Paths.notifimagecache)) -+ dummyImageLoader.active = true; -+ } -+ -+ function lock(item: Item): void { -+ locks.add(item); -+ } -+ -+ function unlock(item: Item): void { -+ locks.delete(item); -+ if (closed) -+ close(); -+ } -+ -+ function close(): void { -+ closed = true; -+ if (locks.size === 0 && Notifs.list.includes(this)) { -+ Notifs.list = Notifs.list.filter(n => n !== this); -+ notification?.dismiss(); -+ destroy(); -+ } -+ } -+ -+ Component.onCompleted: { -+ if (!notification) -+ return; -+ -+ id = notification.id; -+ summary = notification.summary; -+ body = notification.body; -+ appIcon = notification.appIcon; -+ appName = notification.appName; -+ image = notification.image; -+ maybeTriggerDummyImageLoader(); -+ expireTimeout = notification.expireTimeout; -+ hints = notification.hints; -+ urgency = notification.urgency; -+ resident = notification.resident; -+ hasActionIcons = notification.hasActionIcons; -+ // qmllint disable unresolved-type -+ actions = notification.actions.map(a => ({ -+ // qmllint enable unresolved-type -+ identifier: a.identifier, -+ text: a.text, -+ invoke: () => a.invoke() -+ })); -+ } -+} -diff --git a/services/Notifs.qml b/services/Notifs.qml -index 2ebc32db..d539d0a2 100644 ---- a/services/Notifs.qml -+++ b/services/Notifs.qml -@@ -1,27 +1,44 @@ - pragma Singleton - pragma ComponentBehavior: Bound - --import qs.components.misc --import qs.config --import qs.utils --import Caelestia -+import QtQuick - import Quickshell - import Quickshell.Io - import Quickshell.Services.Notifications --import QtQuick -+import Caelestia -+import Caelestia.Config -+import qs.components.misc -+import qs.services -+import qs.utils - - Singleton { - id: root - -- property list list: [] -- readonly property list notClosed: list.filter(n => !n.closed) -- readonly property list popups: list.filter(n => n.popup) -+ property list list: [] -+ readonly property list notClosed: list.filter(n => !n.closed) -+ readonly property list popups: list.filter(n => n.popup) - property alias dnd: props.dnd - - property bool loaded - -+ function hasFullscreen(): bool { -+ for (const monitor of Hypr.monitors.values) { -+ if (monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1)) -+ return true; -+ } -+ return false; -+ } -+ -+ function shouldShowPopup(): bool { -+ if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) -+ return false; -+ if (GlobalConfig.notifs.fullscreen === "off" && hasFullscreen()) -+ return false; -+ return true; -+ } -+ - onDndChanged: { -- if (!Config.utilities.toasts.dndChanged) -+ if (!GlobalConfig.utilities.toasts.dndChanged) - return; - - if (dnd) -@@ -78,7 +95,7 @@ Singleton { - notif.tracked = true; - - const comp = notifComp.createObject(root, { -- popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), -+ popup: root.shouldShowPopup(), - notification: notif - }); - root.list = [comp, ...root.list]; -@@ -88,6 +105,7 @@ Singleton { - FileView { - id: storage - -+ printErrors: false - path: `${Paths.state}/notifs.json` - onLoaded: { - const data = JSON.parse(text()); -@@ -99,12 +117,14 @@ Singleton { - onLoadFailed: err => { - if (err === FileViewError.FileNotFound) { - root.loaded = true; -- setText("[]"); -+ Qt.callLater(() => setText("[]")); - } - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "clearNotifs" - description: "Clear all notifications" - onPressed: { -@@ -114,8 +134,6 @@ Singleton { - } - - IpcHandler { -- target: "notifs" -- - function clear(): void { - for (const notif of root.list.slice()) - notif.close(); -@@ -136,203 +154,13 @@ Singleton { - function disableDnd(): void { - props.dnd = false; - } -- } -- -- component Notif: QtObject { -- id: notif -- -- property bool popup -- property bool closed -- property var locks: new Set() -- -- property date time: new Date() -- readonly property string timeStr: { -- const diff = Time.date.getTime() - time.getTime(); -- const m = Math.floor(diff / 60000); -- -- if (m < 1) -- return qsTr("now"); -- -- const h = Math.floor(m / 60); -- const d = Math.floor(h / 24); -- -- if (d > 0) -- return `${d}d`; -- if (h > 0) -- return `${h}h`; -- return `${m}m`; -- } -- -- property Notification notification -- property string id -- property string summary -- property string body -- property string appIcon -- property string appName -- property string image -- property real expireTimeout: Config.notifs.defaultExpireTimeout -- property int urgency: NotificationUrgency.Normal -- property bool resident -- property bool hasActionIcons -- property list actions -- -- readonly property Timer timer: Timer { -- running: true -- interval: notif.expireTimeout > 0 ? notif.expireTimeout : Config.notifs.defaultExpireTimeout -- onTriggered: { -- if (Config.notifs.expire) -- notif.popup = false; -- } -- } -- -- readonly property LazyLoader dummyImageLoader: LazyLoader { -- active: false -- -- PanelWindow { -- implicitWidth: Config.notifs.sizes.image -- implicitHeight: Config.notifs.sizes.image -- color: "transparent" -- mask: Region {} -- -- Image { -- function tryCache(): void { -- if (status !== Image.Ready || width != Config.notifs.sizes.image || height != Config.notifs.sizes.image) -- return; -- -- const cacheKey = notif.appName + notif.summary + notif.id; -- let h1 = 0xdeadbeef, h2 = 0x41c6ce57, ch; -- for (let i = 0; i < cacheKey.length; i++) { -- ch = cacheKey.charCodeAt(i); -- h1 = Math.imul(h1 ^ ch, 2654435761); -- h2 = Math.imul(h2 ^ ch, 1597334677); -- } -- h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507); -- h1 ^= Math.imul(h2 ^ (h2 >>> 13), 3266489909); -- h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507); -- h2 ^= Math.imul(h1 ^ (h1 >>> 13), 3266489909); -- const hash = (h2 >>> 0).toString(16).padStart(8, 0) + (h1 >>> 0).toString(16).padStart(8, 0); -- -- const cache = `${Paths.notifimagecache}/${hash}.png`; -- CUtils.saveItem(this, Qt.resolvedUrl(cache), () => { -- notif.image = cache; -- notif.dummyImageLoader.active = false; -- }); -- } -- -- anchors.fill: parent -- source: Qt.resolvedUrl(notif.image) -- fillMode: Image.PreserveAspectCrop -- cache: false -- asynchronous: true -- opacity: 0 -- -- onStatusChanged: tryCache() -- onWidthChanged: tryCache() -- onHeightChanged: tryCache() -- } -- } -- } - -- readonly property Connections conn: Connections { -- target: notif.notification -- -- function onClosed(): void { -- notif.close(); -- } -- -- function onSummaryChanged(): void { -- notif.summary = notif.notification.summary; -- } -- -- function onBodyChanged(): void { -- notif.body = notif.notification.body; -- } -- -- function onAppIconChanged(): void { -- notif.appIcon = notif.notification.appIcon; -- } -- -- function onAppNameChanged(): void { -- notif.appName = notif.notification.appName; -- } -- -- function onImageChanged(): void { -- notif.image = notif.notification.image; -- if (notif.notification?.image) -- notif.dummyImageLoader.active = true; -- } -- -- function onExpireTimeoutChanged(): void { -- notif.expireTimeout = notif.notification.expireTimeout; -- } -- -- function onUrgencyChanged(): void { -- notif.urgency = notif.notification.urgency; -- } -- -- function onResidentChanged(): void { -- notif.resident = notif.notification.resident; -- } -- -- function onHasActionIconsChanged(): void { -- notif.hasActionIcons = notif.notification.hasActionIcons; -- } -- -- function onActionsChanged(): void { -- notif.actions = notif.notification.actions.map(a => ({ -- identifier: a.identifier, -- text: a.text, -- invoke: () => a.invoke() -- })); -- } -- } -- -- function lock(item: Item): void { -- locks.add(item); -- } -- -- function unlock(item: Item): void { -- locks.delete(item); -- if (closed) -- close(); -- } -- -- function close(): void { -- closed = true; -- if (locks.size === 0 && root.list.includes(this)) { -- root.list = root.list.filter(n => n !== this); -- notification?.dismiss(); -- destroy(); -- } -- } -- -- Component.onCompleted: { -- if (!notification) -- return; -- -- id = notification.id; -- summary = notification.summary; -- body = notification.body; -- appIcon = notification.appIcon; -- appName = notification.appName; -- image = notification.image; -- if (notification?.image) -- dummyImageLoader.active = true; -- expireTimeout = notification.expireTimeout; -- urgency = notification.urgency; -- resident = notification.resident; -- hasActionIcons = notification.hasActionIcons; -- actions = notification.actions.map(a => ({ -- identifier: a.identifier, -- text: a.text, -- invoke: () => a.invoke() -- })); -- } -+ target: "notifs" - } - - Component { - id: notifComp - -- Notif {} -+ NotifData {} - } - } -diff --git a/services/Players.qml b/services/Players.qml -index 1191696a..41507839 100644 ---- a/services/Players.qml -+++ b/services/Players.qml -@@ -1,36 +1,51 @@ - pragma Singleton - --import qs.components.misc --import qs.config -+import QtQml - import Quickshell - import Quickshell.Io - import Quickshell.Services.Mpris --import QtQml - import Caelestia -+import Caelestia.Config -+import qs.components.misc - - Singleton { - id: root - - readonly property list list: Mpris.players.values -- readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === Config.services.defaultPlayer) ?? list[0] ?? null -+ readonly property MprisPlayer active: props.manualActive ?? list.find(p => getIdentity(p) === GlobalConfig.services.defaultPlayer) ?? list[0] ?? null - property alias manualActive: props.manualActive - - function getIdentity(player: MprisPlayer): string { -- const alias = Config.services.playerAliases.find(a => a.from === player.identity); -+ const alias = GlobalConfig.services.playerAliases.find(a => a.from === player.identity); - return alias?.to ?? player.identity; - } - -- Connections { -- target: active -+ function getArtUrl(player: MprisPlayer): string { -+ if (!player) -+ return ""; -+ if (player.trackArtUrl) -+ return player.trackArtUrl; -+ -+ const url = player.metadata["xesam:url"] ?? ""; -+ if (url.startsWith("https://www.youtube.com/watch")) { -+ // Fallback for youtube -+ const id = url.match(/[?&]v=([\w-]{11})/)?.[1]; -+ return id ? `https://img.youtube.com/vi/${id}/hqdefault.jpg` : ""; -+ } -+ return ""; -+ } - -+ Connections { - function onPostTrackChanged() { -- if (!Config.utilities.toasts.nowPlaying) { -+ if (!GlobalConfig.utilities.toasts.nowPlaying) { - return; - } -- if (active.trackArtist != "" && active.trackTitle != "") { -- Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(active.trackArtist).arg(active.trackTitle), "music_note"); -+ if (root.active.trackArtist != "" && root.active.trackTitle != "") { -+ Toaster.toast(qsTr("Now Playing"), qsTr("%1 - %2").arg(root.active.trackArtist).arg(root.active.trackTitle), "music_note"); - } - } -+ -+ target: root.active - } - - PersistentProperties { -@@ -41,7 +56,9 @@ Singleton { - reloadableId: "players" - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "mediaToggle" - description: "Toggle media playback" - onPressed: { -@@ -51,7 +68,9 @@ Singleton { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "mediaPrev" - description: "Previous track" - onPressed: { -@@ -61,7 +80,9 @@ Singleton { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "mediaNext" - description: "Next track" - onPressed: { -@@ -71,15 +92,15 @@ Singleton { - } - } - -+ // qmllint disable unresolved-type - CustomShortcut { -+ // qmllint enable unresolved-type - name: "mediaStop" - description: "Stop media playback" - onPressed: root.active?.stop() - } - - IpcHandler { -- target: "mpris" -- - function getActive(prop: string): string { - const active = root.active; - return active ? active[prop] ?? "Invalid property" : "No active player"; -@@ -122,5 +143,7 @@ Singleton { - function stop(): void { - root.active?.stop(); - } -+ -+ target: "mpris" - } - } -diff --git a/services/Recorder.qml b/services/Recorder.qml -index be83629c..253bac17 100644 ---- a/services/Recorder.qml -+++ b/services/Recorder.qml -@@ -1,54 +1,66 @@ - pragma Singleton - -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick - - Singleton { - id: root - - readonly property alias running: props.running -+ readonly property alias starting: props.starting - readonly property alias paused: props.paused - readonly property alias elapsed: props.elapsed -+ readonly property alias videoMode: props.videoMode -+ readonly property alias audioMode: props.audioMode -+ property int startChecks: 0 -+ readonly property int maxStartChecks: 30 - - signal errorOccurred(string errorMsg) - signal recordingStarted() - signal recordingStopped() - - function start(videoMode: string, audioMode: string): bool { -- if (props.running) { -+ if (props.running || props.starting) { - console.warn("Recording already running"); - errorOccurred("Recording already in progress"); - return false; - } - -+ const requestedVideoMode = videoMode || "fullscreen"; -+ const requestedAudioMode = audioMode || "none"; -+ - // Build command array -- const args = ["caelestia", "record", "--mode", videoMode]; -+ const args = ["caelestia", "record", "--mode", requestedVideoMode]; - -- if (audioMode) { -- args.push("--audio", audioMode); -+ if (requestedAudioMode) { -+ args.push("--audio", requestedAudioMode); - } - - console.log("Executing:", args.join(" ")); - - try { - Quickshell.execDetached(args); -- props.running = true; -+ props.starting = true; -+ props.running = false; - props.paused = false; - props.elapsed = 0; -+ props.videoMode = requestedVideoMode; -+ props.audioMode = requestedAudioMode; -+ root.startChecks = 0; - verifyTimer.restart(); -- recordingStarted(); - return true; - } catch (error) { - console.error("Failed to start recording:", error); - errorOccurred("Failed to execute recording command: " + error); -+ props.starting = false; - props.running = false; - return false; - } - } - - function stop(): void { -- if (!props.running) { -+ if (!props.running && !props.starting) { - console.warn("No recording to stop"); - return; - } -@@ -57,12 +69,21 @@ Singleton { - - try { - Quickshell.execDetached(["caelestia", "record", "--stop"]); -+ if (props.starting) { -+ props.starting = false; -+ props.running = false; -+ props.paused = false; -+ props.elapsed = 0; -+ recordingStopped(); -+ return; -+ } - // Don't immediately set running to false - wait for process to confirm - stopVerifyTimer.restart(); - } catch (error) { - console.error("Failed to stop recording:", error); - errorOccurred("Failed to stop recording: " + error); - // Force state reset on error -+ props.starting = false; - props.running = false; - props.paused = false; - props.elapsed = 0; -@@ -71,7 +92,7 @@ Singleton { - } - - function togglePause(): void { -- if (!props.running) { -+ if (!props.running || props.starting) { - console.warn("No recording to pause"); - return; - } -@@ -96,8 +117,11 @@ Singleton { - id: props - - property bool running: false -+ property bool starting: false - property bool paused: false - property real elapsed: 0 -+ property string videoMode: "fullscreen" -+ property string audioMode: "none" - - reloadableId: "recorder" - } -@@ -116,6 +140,7 @@ Singleton { - // Detect unexpected stop - if (wasRunning && !isRunning) { - console.warn("Recording process stopped unexpectedly"); -+ props.starting = false; - props.running = false; - props.paused = false; - props.elapsed = 0; -@@ -132,7 +157,7 @@ Singleton { - // Verification timer after start - Timer { - id: verifyTimer -- interval: 1500 -+ interval: 1000 - repeat: false - onTriggered: { - console.log("Verifying recording started"); -@@ -161,9 +186,26 @@ Singleton { - onExited: code => { - const isRunning = code === 0; - -- if (!isRunning && props.running) { -+ if (isRunning && props.starting) { -+ console.log("Recording verified running"); -+ props.starting = false; -+ props.running = true; -+ props.paused = false; -+ recordingStarted(); -+ statusCheckTimer.restart(); -+ return; -+ } -+ -+ if (!isRunning && props.starting) { -+ root.startChecks++; -+ if (root.startChecks < root.maxStartChecks) { -+ verifyTimer.restart(); -+ return; -+ } -+ - console.error("Recording process failed to start"); -- errorOccurred("Recording process failed to start"); -+ errorOccurred("Recording did not start"); -+ props.starting = false; - props.running = false; - props.paused = false; - props.elapsed = 0; -@@ -186,6 +228,7 @@ Singleton { - - if (!isRunning) { - console.log("Recording stopped successfully"); -+ props.starting = false; - props.running = false; - props.paused = false; - props.elapsed = 0; -@@ -240,10 +283,12 @@ Singleton { - onExited: code => { - if (code === 0) { - console.log("Found existing recording process"); -+ props.starting = false; - props.running = true; - statusCheckTimer.restart(); - } else { - console.log("No existing recording process"); -+ props.starting = false; - props.running = false; - props.paused = false; - props.elapsed = 0; -diff --git a/services/Screens.qml b/services/Screens.qml -new file mode 100644 -index 00000000..ac26d27d ---- /dev/null -+++ b/services/Screens.qml -@@ -0,0 +1,14 @@ -+pragma Singleton -+ -+import Quickshell -+import Caelestia.Config -+ -+Singleton { -+ id: root -+ -+ readonly property list screens: Quickshell.screens.filter(s => GlobalConfig.forScreen(s.name).enabled) -+ -+ function isExcluded(screen: ShellScreen): bool { -+ return !GlobalConfig.forScreen(screen.name).enabled; -+ } -+} -diff --git a/services/SystemUsage.qml b/services/SystemUsage.qml -index bd02da36..15dda611 100644 ---- a/services/SystemUsage.qml -+++ b/services/SystemUsage.qml -@@ -1,31 +1,57 @@ - pragma Singleton - --import qs.config -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick -+import Caelestia.Config - - Singleton { - id: root - -+ // CPU properties -+ property string cpuName: "" - property real cpuPerc - property real cpuTemp -- readonly property string gpuType: Config.services.gpuType.toUpperCase() || autoGpuType -+ -+ // GPU properties -+ readonly property string gpuType: GlobalConfig.services.gpuType.toUpperCase() || autoGpuType - property string autoGpuType: "NONE" -+ property string gpuName: "" - property real gpuPerc - property real gpuTemp -+ -+ // Memory properties - property real memUsed - property real memTotal - readonly property real memPerc: memTotal > 0 ? memUsed / memTotal : 0 -- property real storageUsed -- property real storageTotal -- property real storagePerc: storageTotal > 0 ? storageUsed / storageTotal : 0 -+ -+ // Storage properties (aggregated) -+ readonly property real storagePerc: { -+ let totalUsed = 0; -+ let totalSize = 0; -+ for (const disk of disks) { -+ totalUsed += disk.used; -+ totalSize += disk.total; -+ } -+ return totalSize > 0 ? totalUsed / totalSize : 0; -+ } -+ -+ // Individual disks: Array of { mount, used, total, free, perc } -+ property var disks: [] - - property real lastCpuIdle - property real lastCpuTotal - - property int refCount - -+ function cleanCpuName(name: string): string { -+ return name.replace(/\(R\)|\(TM\)|CPU|\d+(?:th|nd|rd|st) Gen |Core |Processor/gi, "").replace(/\s+/g, " ").trim(); -+ } -+ -+ function cleanGpuName(name: string): string { -+ return name.replace(/\(R\)|\(TM\)|Graphics/gi, "").replace(/\s+/g, " ").trim(); -+ } -+ - function formatKib(kib: real): var { - const mib = 1024; - const gib = 1024 ** 2; -@@ -54,7 +80,7 @@ Singleton { - - Timer { - running: root.refCount > 0 -- interval: 3000 -+ interval: GlobalConfig.dashboard.resourceUpdateInterval - repeat: true - triggeredOnStart: true - onTriggered: { -@@ -66,6 +92,18 @@ Singleton { - } - } - -+ // One-time CPU info detection (name) -+ FileView { -+ id: cpuinfoInit -+ -+ path: "/proc/cpuinfo" -+ onLoaded: { -+ const nameMatch = text().match(/model name\s*:\s*(.+)/); -+ if (nameMatch) -+ root.cpuName = root.cleanCpuName(nameMatch[1]); -+ } -+ } -+ - FileView { - id: stat - -@@ -101,41 +139,111 @@ Singleton { - Process { - id: storage - -- command: ["sh", "-c", "df | grep '^/dev/' | awk '{print $1, $3, $4}'"] -+ // Get physical disks with aggregated usage from their partitions -+ // -J triggers JSON output. -b triggers bytes. -+ command: ["lsblk", "-J", "-b", "-o", "NAME,SIZE,TYPE,FSUSED,FSSIZE,MOUNTPOINT"] -+ - stdout: StdioCollector { - onStreamFinished: { -- const deviceMap = new Map(); -+ const data = JSON.parse(text); -+ const diskList = []; -+ const seenDevices = new Set(); -+ -+ // Helper to recursively sum usage from children (partitions, crypt, lvm) -+ const aggregateUsage = dev => { -+ let used = 0; -+ let size = 0; -+ let isRoot = dev.mountpoint === "/" || (dev.mountpoints && dev.mountpoints.includes("/")); -+ -+ if (!seenDevices.has(dev.name)) { -+ // lsblk returns null for empty/unformatted partitions, which parses to 0 here -+ used = parseInt(dev.fsused) || 0; -+ size = parseInt(dev.fssize) || 0; -+ seenDevices.add(dev.name); -+ } - -- for (const line of text.trim().split("\n")) { -- if (line.trim() === "") -- continue; -- -- const parts = line.trim().split(/\s+/); -- if (parts.length >= 3) { -- const device = parts[0]; -- const used = parseInt(parts[1], 10) || 0; -- const avail = parseInt(parts[2], 10) || 0; -- -- // Only keep the entry with the largest total space for each device -- if (!deviceMap.has(device) || (used + avail) > (deviceMap.get(device).used + deviceMap.get(device).avail)) { -- deviceMap.set(device, { -- used: used, -- avail: avail -- }); -+ if (dev.children) { -+ for (const child of dev.children) { -+ const stats = aggregateUsage(child); -+ used += stats.used; -+ size += stats.size; -+ if (stats.isRoot) -+ isRoot = true; - } - } -+ return { -+ used, -+ size, -+ isRoot -+ }; -+ }; -+ -+ for (const dev of data.blockdevices) { -+ // Only process physical disks at the top level -+ if (dev.type === "disk" && !dev.name.startsWith("zram")) { -+ const stats = aggregateUsage(dev); -+ -+ if (stats.size === 0) { -+ continue; -+ } -+ -+ const total = stats.size; -+ const used = stats.used; -+ -+ diskList.push({ -+ mount: dev.name, -+ used: used / 1024 // KiB -+ , -+ total: total / 1024 // KiB -+ , -+ free: (total - used) / 1024, -+ perc: total > 0 ? used / total : 0, -+ hasRoot: stats.isRoot -+ }); -+ } - } - -- let totalUsed = 0; -- let totalAvail = 0; -+ // Sort by putting the disk with root first, then sort the rest alphabetically -+ root.disks = diskList.sort((a, b) => { -+ if (a.hasRoot && !b.hasRoot) -+ return -1; -+ if (!a.hasRoot && b.hasRoot) -+ return 1; -+ return a.mount.localeCompare(b.mount); -+ }); -+ } -+ } -+ } -+ -+ // GPU name detection (one-time) -+ Process { -+ id: gpuNameDetect - -- for (const [device, stats] of deviceMap) { -- totalUsed += stats.used; -- totalAvail += stats.avail; -- } -+ running: true -+ command: ["sh", "-c", "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null || glxinfo -B 2>/dev/null | grep 'Device:' | cut -d':' -f2 | cut -d'(' -f1 || lspci 2>/dev/null | grep -i 'vga\\|3d controller\\|display' | head -1"] -+ stdout: StdioCollector { -+ onStreamFinished: { -+ const output = text.trim(); -+ if (!output) -+ return; - -- root.storageUsed = totalUsed; -- root.storageTotal = totalUsed + totalAvail; -+ // Check if it's from nvidia-smi (clean GPU name) -+ if (output.toLowerCase().includes("nvidia") || output.toLowerCase().includes("geforce") || output.toLowerCase().includes("rtx") || output.toLowerCase().includes("gtx")) { -+ root.gpuName = root.cleanGpuName(output); -+ } else if (output.toLowerCase().includes("rx")) { -+ root.gpuName = root.cleanGpuName(output); -+ } else { -+ // Parse lspci output: extract name from brackets or after colon -+ // Handles cases like [AMD/ATI] Navi 21 [Radeon RX 6800/6800 XT / 6900 XT] (rev c0) -+ const bracketMatch = output.match(/\[([^\]]+)\][^\[]*$/); -+ if (bracketMatch) { -+ root.gpuName = root.cleanGpuName(bracketMatch[1]); -+ } else { -+ const colonMatch = output.match(/:\s*(.+)/); -+ if (colonMatch) -+ root.gpuName = root.cleanGpuName(colonMatch[1]); -+ } -+ } - } - } - } -@@ -143,7 +251,7 @@ Singleton { - Process { - id: gpuTypeCheck - -- running: !Config.services.gpuType -+ running: !GlobalConfig.services.gpuType - command: ["sh", "-c", "if command -v nvidia-smi &>/dev/null && nvidia-smi -L &>/dev/null; then echo NVIDIA; elif ls /sys/class/drm/card*/device/gpu_busy_percent 2>/dev/null | grep -q .; then echo GENERIC; else echo NONE; fi"] - stdout: StdioCollector { - onStreamFinished: root.autoGpuType = text.trim() -diff --git a/services/Time.qml b/services/Time.qml -index a07d9ef8..0db520d0 100644 ---- a/services/Time.qml -+++ b/services/Time.qml -@@ -1,8 +1,8 @@ - pragma Singleton - --import qs.config --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia.Config - - Singleton { - property alias enabled: clock.enabled -@@ -11,7 +11,7 @@ Singleton { - readonly property int minutes: clock.minutes - readonly property int seconds: clock.seconds - -- readonly property string timeStr: format(Config.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") -+ readonly property string timeStr: format(GlobalConfig.services.useTwelveHourClock ? "hh:mm:A" : "hh:mm") - readonly property list timeComponents: timeStr.split(":") - readonly property string hourStr: timeComponents[0] ?? "" - readonly property string minuteStr: timeComponents[1] ?? "" -@@ -23,6 +23,7 @@ Singleton { - - SystemClock { - id: clock -+ - precision: SystemClock.Seconds - } - } -diff --git a/services/VPN.qml b/services/VPN.qml -index 2d08631a..61e91d0b 100644 ---- a/services/VPN.qml -+++ b/services/VPN.qml -@@ -1,20 +1,26 @@ - pragma Singleton - -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick --import qs.config - import Caelestia -+import Caelestia.Config - - Singleton { - id: root - - property bool connected: false -+ property var status: ({ -+ connected: false, -+ state: "disconnected", -+ reason: "", -+ authUrl: "" -+ }) - - readonly property bool connecting: connectProc.running || disconnectProc.running -- readonly property bool enabled: Config.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) -+ readonly property bool enabled: GlobalConfig.utilities.vpn.provider.some(p => typeof p === "object" ? (p.enabled === true) : false) - readonly property var providerInput: { -- const enabledProvider = Config.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); -+ const enabledProvider = GlobalConfig.utilities.vpn.provider.find(p => typeof p === "object" ? (p.enabled === true) : false); - return enabledProvider || "wireguard"; - } - readonly property bool isCustomProvider: typeof providerInput === "object" -@@ -53,7 +59,7 @@ Singleton { - displayName: "Warp" - }, - "netbird": { -- connectCmd: ["netbird", "up"], -+ connectCmd: ["netbird", "up", "--no-browser"], - disconnectCmd: ["netbird", "down"], - interface: "wt0", - displayName: "NetBird" -@@ -75,6 +81,10 @@ Singleton { - } - - function connect(): void { -+ if (status.state === "needs-auth" && status.authUrl) { -+ emitStatusToast(status); -+ return; -+ } - if (!connected && !connecting && root.currentConfig && root.currentConfig.connectCmd) { - connectProc.exec(root.currentConfig.connectCmd); - } -@@ -87,11 +97,7 @@ Singleton { - } - - function toggle(): void { -- if (connected) { -- disconnect(); -- } else { -- connect(); -- } -+ connected ? disconnect() : connect(); - } - - function checkStatus(): void { -@@ -100,18 +106,223 @@ Singleton { - } - } - -- onConnectedChanged: { -- if (!Config.utilities.toasts.vpnChanged) -+ function getStatusCommand(): var { -+ switch (providerName) { -+ case "tailscale": -+ return ["tailscale", "status", "--json"]; -+ case "netbird": -+ return ["netbird", "status", "--json"]; -+ case "warp": -+ return ["warp-cli", "status"]; -+ case "wireguard": -+ return ["ip", "link", "show"]; -+ default: -+ return ["ip", "link", "show"]; -+ } -+ } -+ -+ function parseTailscaleStatus(output: string): var { -+ const status = { -+ connected: false, -+ state: "disconnected", -+ reason: "", -+ authUrl: "" -+ }; -+ -+ // Handle empty or whitespace-only output -+ if (!output || output.trim().length === 0) { -+ return status; -+ } -+ -+ // Check for common non-JSON states first -+ if (output.includes("Logged out") || output.includes("Stopped") || output.includes("not running") || output.includes("Tailscale is not running")) { -+ status.state = "disconnected"; -+ return status; -+ } -+ -+ // Try to parse as JSON -+ try { -+ const data = JSON.parse(output); -+ const backendState = data.BackendState || ""; -+ -+ if (backendState === "Running") { -+ status.connected = true; -+ status.state = "connected"; -+ } else if (backendState === "Starting") { -+ status.state = "connecting"; -+ } else if (backendState === "NeedsLogin" || backendState === "NeedsMachineAuth") { -+ status.state = "needs-auth"; -+ status.reason = backendState === "NeedsLogin" ? "Login required" : "Machine authorization required"; -+ status.authUrl = data.AuthURL || ""; -+ } -+ } catch (e) { -+ // JSON parsing failed - treat as disconnected unless it looks like an error -+ if (output.includes("error") || output.includes("Error") || output.includes("failed")) { -+ status.state = "disconnected"; -+ status.reason = "Tailscale may not be running"; -+ } else { -+ status.state = "disconnected"; -+ } -+ } -+ return status; -+ } -+ -+ function parseNetBirdStatus(output: string): var { -+ const status = { -+ connected: false, -+ state: "disconnected", -+ reason: "", -+ authUrl: "" -+ }; -+ try { -+ const data = JSON.parse(output); -+ const mgmtConnected = data.management?.connected; -+ const signalConnected = data.signal?.connected; -+ -+ if (mgmtConnected && signalConnected) { -+ status.connected = true; -+ status.state = "connected"; -+ } else if (data.management?.error) { -+ const error = data.management.error; -+ if (error.includes("auth") || error.includes("login")) { -+ status.state = "needs-auth"; -+ status.reason = "Authentication required"; -+ } else { -+ status.reason = error; -+ } -+ } -+ } catch (e) { -+ status.state = "error"; -+ status.reason = "Failed to parse status"; -+ } -+ return status; -+ } -+ -+ function parseWarpStatus(output: string): var { -+ const status = { -+ connected: false, -+ state: "disconnected", -+ reason: "", -+ authUrl: "" -+ }; -+ -+ if (output.includes("Connected")) { -+ status.connected = true; -+ status.state = "connected"; -+ } else if (output.includes("Connecting")) { -+ status.state = "connecting"; -+ } else if (output.includes("Unable") || output.includes("Registration Missing") || output.includes("registration") || output.includes("register")) { -+ status.state = "needs-auth"; -+ status.reason = "WARP registration required"; -+ } else if (!output.includes("Disconnected")) { -+ status.state = "error"; -+ status.reason = "Unknown WARP status"; -+ } -+ return status; -+ } -+ -+ function parseWireGuardStatus(output: string): var { -+ const status = { -+ connected: false, -+ state: "disconnected", -+ reason: "", -+ authUrl: "" -+ }; -+ const iface = root.currentConfig?.interface || ""; -+ -+ if (iface && output.includes(iface + ":")) { -+ status.connected = true; -+ status.state = "connected"; -+ } -+ return status; -+ } -+ -+ function parseStatusOutput(output: string): var { -+ switch (providerName) { -+ case "tailscale": -+ return parseTailscaleStatus(output); -+ case "netbird": -+ return parseNetBirdStatus(output); -+ case "warp": -+ return parseWarpStatus(output); -+ case "wireguard": -+ default: -+ return parseWireGuardStatus(output); -+ } -+ } -+ -+ function extractAuthUrl(text: string): string { -+ const urlMatch = text.match(/(https?:\/\/[^\s]+)/); -+ return urlMatch ? urlMatch[1] : ""; -+ } -+ -+ function createAuthStatus(authUrl: string): var { -+ return { -+ connected: false, -+ state: "needs-auth", -+ reason: "Authentication required", -+ authUrl: authUrl -+ }; -+ } -+ -+ function updateStatus(newStatus: var): void { -+ const oldState = status.state; -+ if (newStatus.state === "needs-auth" && !newStatus.authUrl && status.authUrl) { -+ newStatus.authUrl = status.authUrl; -+ } -+ status = newStatus; -+ root.connected = newStatus.connected; -+ -+ if (oldState !== newStatus.state) { -+ emitStatusToast(newStatus); -+ } -+ } -+ -+ function emitStatusToast(statusObj: var): void { -+ if (!GlobalConfig.utilities.toasts.vpnChanged) - return; - - const displayName = root.currentConfig ? (root.currentConfig.displayName || "VPN") : "VPN"; -- if (connected) { -+ -+ switch (statusObj.state) { -+ case "connected": - Toaster.toast(qsTr("VPN connected"), qsTr("Connected to %1").arg(displayName), "vpn_key"); -- } else { -- Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); -+ break; -+ case "disconnected": -+ if (status.connected) { -+ Toaster.toast(qsTr("VPN disconnected"), qsTr("Disconnected from %1").arg(displayName), "vpn_key_off"); -+ } -+ break; -+ case "needs-auth": -+ const authMsg = statusObj.reason || "Authentication required"; -+ Toaster.toast(qsTr("VPN authentication required"), qsTr("%1: %2").arg(displayName).arg(authMsg), "vpn_lock"); -+ break; -+ case "error": -+ if (status.state === "connected" || status.state === "connecting" || status.state === "needs-auth") { -+ const errMsg = statusObj.reason || "Unknown error"; -+ Toaster.toast(qsTr("VPN error"), qsTr("%1: %2").arg(displayName).arg(errMsg), "error"); -+ } -+ break; -+ } -+ } -+ -+ onStatusChanged: { -+ if (providerName === "warp" && status.state === "needs-auth" && status.reason.includes("registration")) { -+ warpRegisterProc.exec(["warp-cli", "registration", "new"]); - } - } - -+ onProviderNameChanged: { -+ status = { -+ connected: false, -+ state: "disconnected", -+ reason: "", -+ authUrl: "" -+ }; -+ root.connected = false; -+ statusCheckTimer.start(); -+ } -+ - Component.onCompleted: root.enabled && statusCheckTimer.start() - - Process { -@@ -127,15 +338,47 @@ Singleton { - Process { - id: statusProc - -- command: ["ip", "link", "show"] -+ command: root.getStatusCommand() -+ // qmllint disable incompatible-type - environment: ({ -+ // qmllint enable incompatible-type - LANG: "C.UTF-8", - LC_ALL: "C.UTF-8" - }) - stdout: StdioCollector { - onStreamFinished: { -- const iface = root.currentConfig ? root.currentConfig.interface : ""; -- root.connected = iface && text.includes(iface + ":"); -+ const newStatus = root.parseStatusOutput(text); -+ root.updateStatus(newStatus); -+ } -+ } -+ stderr: StdioCollector { -+ onStreamFinished: { -+ if (text.trim().length > 0) { -+ if (text.includes("doesn't appear to be running") || text.includes("failed to connect to local tailscaled") || text.includes("daemon is not running") || text.includes("not running") && (text.includes("netbird") || text.includes("warp"))) { -+ let cmd = "sudo systemctl start "; -+ switch (root.providerName) { -+ case "tailscale": -+ cmd += "tailscaled"; -+ break; -+ case "netbird": -+ cmd += "netbird"; -+ break; -+ case "warp": -+ cmd += "warp-svc"; -+ break; -+ default: -+ cmd += root.providerName + "d"; -+ break; -+ } -+ const errorStatus = { -+ connected: false, -+ state: "disconnected", -+ reason: `Service not running (run: ${cmd})`, -+ authUrl: "" -+ }; -+ root.updateStatus(errorStatus); -+ } -+ } - } - } - } -@@ -143,12 +386,59 @@ Singleton { - Process { - id: connectProc - -- onExited: statusCheckTimer.start() -+ onExited: exitCode => { // qmllint disable signal-handler-parameters -+ if (exitCode !== 0) { -+ return; -+ } -+ -+ if (root.providerName === "tailscale") { -+ Qt.callLater(() => { -+ if (root.status.state !== "needs-auth") { -+ statusCheckTimer.start(); -+ } -+ }); -+ } else if (root.status.state !== "needs-auth") { -+ statusCheckTimer.start(); -+ } -+ } -+ stdout: SplitParser { -+ onRead: data => { -+ const authUrl = root.extractAuthUrl(data); -+ if (authUrl) { -+ root.updateStatus(root.createAuthStatus(authUrl)); -+ } -+ } -+ } - stderr: StdioCollector { - onStreamFinished: { - const error = text.trim(); -- if (error && !error.includes("[#]") && !error.includes("already exists")) { -- console.warn("VPN connection error:", error); -+ -+ if (error.includes("Access denied") || error.includes("checkprefs access denied")) { -+ const errorStatus = { -+ connected: false, -+ state: "disconnected", -+ reason: "Permission denied. Run in terminal: sudo tailscale set --operator=$USER", -+ authUrl: "" -+ }; -+ root.updateStatus(errorStatus); -+ return; -+ } -+ -+ if (error.includes("Unknown device type") || error.includes("Protocol not supported")) { -+ const errorStatus = { -+ connected: false, -+ state: "disconnected", -+ reason: "WireGuard module not loaded. Run: sudo modprobe wireguard", -+ authUrl: "" -+ }; -+ root.updateStatus(errorStatus); -+ return; -+ } -+ -+ const authUrl = root.extractAuthUrl(error); -+ -+ if (authUrl) { -+ root.updateStatus(root.createAuthStatus(authUrl)); - } else if (error.includes("already exists")) { - root.connected = true; - } -@@ -159,21 +449,38 @@ Singleton { - Process { - id: disconnectProc - -- onExited: statusCheckTimer.start() -+ onExited: statusCheckTimer.start() // qmllint disable signal-handler-parameters - stderr: StdioCollector { - onStreamFinished: { - const error = text.trim(); - if (error && !error.includes("[#]")) { -- console.warn("VPN disconnection error:", error); -+ console.warn(lc, "Disconnection error:", error); - } - } - } - } - -+ Process { -+ id: warpRegisterProc -+ -+ onExited: exitCode => { // qmllint disable signal-handler-parameters -+ if (exitCode === 0) { -+ statusCheckTimer.start(); -+ } -+ } -+ } -+ - Timer { - id: statusCheckTimer - - interval: 500 - onTriggered: root.checkStatus() - } -+ -+ LoggingCategory { -+ id: lc -+ -+ name: "caelestia.qml.services.vpn" -+ defaultLogLevel: LoggingCategory.Info -+ } - } -diff --git a/services/Visibilities.qml b/services/Visibilities.qml -index 5ddde0c9..39187050 100644 ---- a/services/Visibilities.qml -+++ b/services/Visibilities.qml -@@ -1,16 +1,18 @@ - pragma Singleton - - import Quickshell -+import qs.components -+import qs.services - - Singleton { - property var screens: new Map() - property var bars: new Map() - -- function load(screen: ShellScreen, visibilities: var): void { -+ function load(screen: ShellScreen, visibilities: DrawerVisibilities): void { - screens.set(Hypr.monitorFor(screen), visibilities); - } - -- function getForActive(): PersistentProperties { -+ function getForActive(): DrawerVisibilities { - return screens.get(Hypr.focusedMonitor); - } - } -diff --git a/services/Wallpapers.qml b/services/Wallpapers.qml -index cb96bc56..15daf5c1 100644 ---- a/services/Wallpapers.qml -+++ b/services/Wallpapers.qml -@@ -1,17 +1,18 @@ - pragma Singleton - --import qs.config --import qs.utils --import Caelestia.Models -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick -+import Caelestia.Config -+import Caelestia.Models -+import qs.services -+import qs.utils - - Searcher { - id: root - - readonly property string currentNamePath: `${Paths.state}/wallpaper/path.txt` -- readonly property list smartArg: Config.services.smartScheme ? [] : ["--no-smart"] -+ readonly property list smartArg: GlobalConfig.services.smartScheme ? [] : ["--no-smart"] - - property bool showPreview: false - readonly property string current: showPreview ? previewPath : actualCurrent -@@ -40,14 +41,12 @@ Searcher { - - list: wallpapers.entries - key: "relativePath" -- useFuzzy: Config.launcher.useFuzzy.wallpapers -+ useFuzzy: GlobalConfig.launcher.useFuzzy.wallpapers - extraOpts: useFuzzy ? ({}) : ({ - forward: false - }) - - IpcHandler { -- target: "wallpaper" -- - function get(): string { - return root.actualCurrent; - } -@@ -59,6 +58,8 @@ Searcher { - function list(): string { - return root.list.map(w => w.path).join("\n"); - } -+ -+ target: "wallpaper" - } - - FileView { -diff --git a/services/Weather.qml b/services/Weather.qml -index a3095423..9c30bf7a 100644 ---- a/services/Weather.qml -+++ b/services/Weather.qml -@@ -1,10 +1,10 @@ - pragma Singleton - --import qs.config --import qs.utils --import Caelestia --import Quickshell - import QtQuick -+import Quickshell -+import Caelestia -+import Caelestia.Config -+import qs.utils - - Singleton { - id: root -@@ -17,17 +17,17 @@ Singleton { - - readonly property string icon: cc ? Icons.getWeatherIcon(cc.weatherCode) : "cloud_alert" - readonly property string description: cc?.weatherDesc ?? qsTr("No weather") -- readonly property string temp: Config.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` -- readonly property string feelsLike: Config.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` -+ readonly property string temp: GlobalConfig.services.useFahrenheit ? `${cc?.tempF ?? 0}°F` : `${cc?.tempC ?? 0}°C` -+ readonly property string feelsLike: GlobalConfig.services.useFahrenheit ? `${cc?.feelsLikeF ?? 0}°F` : `${cc?.feelsLikeC ?? 0}°C` - readonly property int humidity: cc?.humidity ?? 0 - readonly property real windSpeed: cc?.windSpeed ?? 0 -- readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" -- readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), Config.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" -+ readonly property string sunrise: cc ? Qt.formatDateTime(new Date(cc.sunrise), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" -+ readonly property string sunset: cc ? Qt.formatDateTime(new Date(cc.sunset), GlobalConfig.services.useTwelveHourClock ? "h:mm A" : "h:mm") : "--:--" - - readonly property var cachedCities: new Map() - - function reload(): void { -- const configLocation = Config.services.weatherLocation; -+ const configLocation = GlobalConfig.services.weatherLocation; - - if (configLocation) { - if (configLocation.indexOf(",") !== -1 && !isNaN(parseFloat(configLocation.split(",")[0]))) { -@@ -54,18 +54,35 @@ Singleton { - return; - } - -- const [lat, lon] = coords.split(","); -- const url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; -- Requests.get(url, text => { -+ const [lat, lon] = coords.split(",").map(s => s.trim()); -+ -+ const fallbackToBigDataCloud = () => { -+ const fallbackUrl = `https://api.bigdatacloud.net/data/reverse-geocode-client?latitude=${lat}&longitude=${lon}&localityLanguage=en`; -+ Requests.get(fallbackUrl, text => { -+ const geo = JSON.parse(text); -+ const geoCity = geo.city || geo.locality; -+ if (geoCity) { -+ city = geoCity; -+ cachedCities.set(coords, geoCity); -+ } else { -+ city = "Unknown City"; -+ } -+ }); -+ }; -+ -+ const nominatimUrl = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=geocodejson`; -+ Requests.get(nominatimUrl, text => { - const geo = JSON.parse(text).features?.[0]?.properties.geocoding; - if (geo) { - const geoCity = geo.type === "city" ? geo.name : geo.city; -- city = geoCity; -- cachedCities.set(coords, geoCity); -- } else { -- city = "Unknown City"; -+ if (geoCity) { -+ city = geoCity; -+ cachedCities.set(coords, geoCity); -+ return; -+ } - } -- }); -+ fallbackToBigDataCloud(); -+ }, fallbackToBigDataCloud); - } - - function fetchCoordsFromCity(cityName: string): void { -@@ -104,14 +121,14 @@ Singleton { - humidity: json.current.relative_humidity_2m, - windSpeed: json.current.wind_speed_10m, - isDay: json.current.is_day, -- sunrise: json.daily.sunrise[0], -- sunset: json.daily.sunset[0] -+ sunrise: json.daily.sunrise[0].replace("T", " "), -+ sunset: json.daily.sunset[0].replace("T", " ") - }; - - const forecastList = []; - for (let i = 0; i < json.daily.time.length; i++) - forecastList.push({ -- date: json.daily.time[i], -+ date: json.daily.time[i].replace(/-/g, "/"), - maxTempC: Math.round(json.daily.temperature_2m_max[i]), - maxTempF: Math.round(toFahrenheit(json.daily.temperature_2m_max[i])), - minTempC: Math.round(json.daily.temperature_2m_min[i]), -@@ -124,7 +141,8 @@ Singleton { - const hourlyList = []; - const now = new Date(); - for (let i = 0; i < json.hourly.time.length; i++) { -- const time = new Date(json.hourly.time[i]); -+ const time = new Date(json.hourly.time[i].replace("T", " ")); -+ - if (time < now) - continue; - -@@ -149,7 +167,7 @@ Singleton { - if (!loc || loc.indexOf(",") === -1) - return ""; - -- const [lat, lon] = loc.split(","); -+ const [lat, lon] = loc.split(",").map(s => s.trim()); - const baseUrl = "https://api.open-meteo.com/v1/forecast"; - const params = ["latitude=" + lat, "longitude=" + lon, "hourly=weather_code,temperature_2m", "daily=weather_code,temperature_2m_max,temperature_2m_min,sunrise,sunset", "current=temperature_2m,relative_humidity_2m,apparent_temperature,is_day,weather_code,wind_speed_10m", "timezone=auto", "forecast_days=7"]; - -@@ -192,7 +210,14 @@ Singleton { - - onLocChanged: fetchWeatherData() - -- // Refresh current location hourly -+ Connections { -+ function onWeatherLocationChanged(): void { -+ root.reload(); -+ } -+ -+ target: GlobalConfig.services -+ } -+ - Timer { - interval: 3600000 // 1 hour - running: true -diff --git a/shell.qml b/shell.qml -index e93b5c52..7cb42354 100644 ---- a/shell.qml -+++ b/shell.qml -@@ -1,6 +1,8 @@ --//@ pragma Env QS_NO_RELOAD_POPUP=1 --//@ pragma Env QSG_RENDER_LOOP=threaded --//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 -+//@ pragma Env QS_CRASHREPORT_URL=https://github.com/caelestia-dots/shell/issues/new?template=crash.yml -+//@ pragma DefaultEnv QS_NO_RELOAD_POPUP=1 -+//@ pragma DefaultEnv QS_DROP_EXPENSIVE_FONTS=1 -+//@ pragma DefaultEnv QSG_RENDER_LOOP=threaded -+//@ pragma DefaultEnv QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000 - - import "modules" - import "modules/drawers" -@@ -10,6 +12,8 @@ import "modules/lock" - import Quickshell - - ShellRoot { -+ settings.watchFiles: true -+ - Background {} - Drawers {} - AreaPicker {} -@@ -17,6 +21,7 @@ ShellRoot { - id: lock - } - -+ ConfigToasts {} - Shortcuts {} - BatteryMonitor {} - IdleMonitors { -diff --git a/utils/Icons.qml b/utils/Icons.qml -index c06cbf80..c864d553 100644 ---- a/utils/Icons.qml -+++ b/utils/Icons.qml -@@ -1,9 +1,9 @@ - pragma Singleton - --import qs.config -+import QtQuick - import Quickshell - import Quickshell.Services.Notifications --import QtQuick -+import Caelestia.Config - - Singleton { - id: root -@@ -79,6 +79,26 @@ Singleton { - Office: "content_paste" - }) - -+ // Checks if a name matches an icon config. Icon configs can have the following keys: -+ // - name: The exact name of the icon -+ // - regex: A regex to match against the name (takes priority over name) -+ // - flags: The regex flags (only used if regex is set) -+ // - icon: The icon to use -+ function matchIconConfig(name: string, iconConfig: var): bool { -+ if (!iconConfig.icon) -+ return false; -+ -+ if (iconConfig.regex) { -+ const re = new RegExp(iconConfig.regex, iconConfig.flags ?? ""); -+ if (re.test(name)) -+ return true; -+ } else if (iconConfig.name === name) { -+ return true; -+ } -+ -+ return false; -+ } -+ - function getAppIcon(name: string, fallback: string): string { - const icon = DesktopEntries.heuristicLookup(name)?.icon; - if (fallback !== "undefined") -@@ -87,6 +107,10 @@ Singleton { - } - - function getAppCategoryIcon(name: string, fallback: string): string { -+ for (const iconConfig of GlobalConfig.bar.workspaces.windowIcons) -+ if (matchIconConfig(name, iconConfig)) -+ return iconConfig.icon; -+ - const categories = DesktopEntries.heuristicLookup(name)?.categories; - - if (categories) -@@ -188,11 +212,9 @@ Singleton { - function getSpecialWsIcon(name: string): string { - name = name.toLowerCase().slice("special:".length); - -- for (const iconConfig of Config.bar.workspaces.specialWorkspaceIcons) { -- if (iconConfig.name === name) { -+ for (const iconConfig of GlobalConfig.bar.workspaces.specialWorkspaceIcons) -+ if (matchIconConfig(name, iconConfig)) - return iconConfig.icon; -- } -- } - - if (name === "special") - return "star"; -@@ -208,7 +230,7 @@ Singleton { - } - - function getTrayIcon(id: string, icon: string): string { -- for (const sub of Config.bar.tray.iconSubs) -+ for (const sub of GlobalConfig.bar.tray.iconSubs) - if (sub.id === id) - return sub.image ? Qt.resolvedUrl(sub.image) : Quickshell.iconPath(sub.icon); - -@@ -218,4 +240,24 @@ Singleton { - } - return icon; - } -+ -+ function getBatteryIcon(charge: int): string { -+ if (charge > 0 && charge < 5) -+ return "battery_0_bar"; -+ if (charge >= 5 && charge < 20) -+ return "battery_1_bar"; -+ if (charge >= 20 && charge < 35) -+ return "battery_2_bar"; -+ if (charge >= 35 && charge < 50) -+ return "battery_3_bar"; -+ if (charge >= 50 && charge < 65) -+ return "battery_4_bar"; -+ if (charge >= 65 && charge < 80) -+ return "battery_5_bar"; -+ if (charge >= 80 && charge < 95) -+ return "battery_6_bar"; -+ if (charge >= 95) -+ return "battery_full"; -+ return "battery_alert"; -+ } - } -diff --git a/utils/NetworkConnection.qml b/utils/NetworkConnection.qml -index e55b87bc..8331813d 100644 ---- a/utils/NetworkConnection.qml -+++ b/utils/NetworkConnection.qml -@@ -1,7 +1,7 @@ - pragma Singleton - --import qs.services - import QtQuick -+import qs.services - - /** - * NetworkConnection -diff --git a/utils/Paths.qml b/utils/Paths.qml -index bc89770a..97f6448a 100644 ---- a/utils/Paths.qml -+++ b/utils/Paths.qml -@@ -1,8 +1,9 @@ - pragma Singleton - --import qs.config --import Caelestia -+import QtQuick - import Quickshell -+import Caelestia -+import Caelestia.Config - - Singleton { - id: root -@@ -18,7 +19,7 @@ Singleton { - - readonly property string imagecache: `${cache}/imagecache` - readonly property string notifimagecache: `${imagecache}/notifs` -- readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(Config.paths.wallpaperDir) -+ readonly property string wallsdir: Quickshell.env("CAELESTIA_WALLPAPERS_DIR") || absolutePath(GlobalConfig.paths.wallpaperDir) - readonly property string recsdir: Quickshell.env("CAELESTIA_RECORDINGS_DIR") || `${videos}/Recordings` - readonly property string libdir: Quickshell.env("CAELESTIA_LIB_DIR") || "/usr/lib/caelestia" - -diff --git a/utils/Searcher.qml b/utils/Searcher.qml -index 053b73bb..102c9e76 100644 ---- a/utils/Searcher.qml -+++ b/utils/Searcher.qml -@@ -1,8 +1,7 @@ --import Quickshell -- - import "scripts/fzf.js" as Fzf - import "scripts/fuzzysort.js" as Fuzzy - import QtQuick -+import Quickshell - - Singleton { - required property list list -diff --git a/utils/Strings.qml b/utils/Strings.qml -new file mode 100644 -index 00000000..a91a0c08 ---- /dev/null -+++ b/utils/Strings.qml -@@ -0,0 +1,26 @@ -+pragma Singleton -+ -+import Quickshell -+ -+Singleton { -+ property var _regexCache: ({}) -+ -+ function testRegexList(filterList: list, target: string): bool { -+ const regexChecker = /^\^.*\$$/; -+ for (const filter of filterList) { -+ if (regexChecker.test(filter)) { -+ let re = _regexCache[filter]; -+ if (!re) { -+ re = new RegExp(filter); -+ _regexCache[filter] = re; -+ } -+ if (re.test(target)) -+ return true; -+ } else { -+ if (filter === target) -+ return true; -+ } -+ } -+ return false; -+ } -+} -diff --git a/utils/SysInfo.qml b/utils/SysInfo.qml -index 19aa4a7a..c715b8d2 100644 ---- a/utils/SysInfo.qml -+++ b/utils/SysInfo.qml -@@ -1,10 +1,10 @@ - pragma Singleton - --import qs.config --import qs.utils -+import QtQuick - import Quickshell - import Quickshell.Io --import QtQuick -+import Caelestia.Config -+import qs.utils - - Singleton { - id: root -@@ -36,11 +36,11 @@ Singleton { - root.osIdLike = fd("ID_LIKE").split(" "); - - const logo = Quickshell.iconPath(fd("LOGO"), true); -- if (Config.general.logo === "caelestia") { -+ if (GlobalConfig.general.logo === "caelestia") { - root.osLogo = Qt.resolvedUrl(`${Quickshell.shellDir}/assets/logo.svg`); - root.isDefaultLogo = true; -- } else if (Config.general.logo) { -- root.osLogo = Quickshell.iconPath(Config.general.logo, true) || "file://" + Paths.absolutePath(Config.general.logo); -+ } else if (GlobalConfig.general.logo) { -+ root.osLogo = Quickshell.iconPath(GlobalConfig.general.logo, true) || "file://" + Paths.absolutePath(GlobalConfig.general.logo); - root.isDefaultLogo = false; - } else if (logo) { - root.osLogo = logo; -@@ -50,11 +50,11 @@ Singleton { - } - - Connections { -- target: Config.general -- - function onLogoChanged(): void { - osRelease.reload(); - } -+ -+ target: GlobalConfig.general - } - - Timer { -diff --git a/utils/scripts/lrcparser.js b/utils/scripts/lrcparser.js -new file mode 100644 -index 00000000..847779ed ---- /dev/null -+++ b/utils/scripts/lrcparser.js -@@ -0,0 +1,62 @@ -+function parseLrc(text) { -+ if (!text) return []; -+ let lines = text.split("\n"); -+ let result = []; -+ -+ let timeRegex = /\[(\d+):(\d+\.\d+|\d+)\]/g; -+ -+ // Blacklist for credits/metadata often found in NetEase lyrics -+ const creditKeywords = [ -+ "作词", "作曲", "编曲", "制作", "收录", "演奏", "词:", "曲:", "Lyricist", "Composer", "Arranger", "Producer", "Mixing", "Mastering" -+ ]; -+ -+ for (let line of lines) { -+ -+ timeRegex.lastIndex = 0; -+ let matches = []; -+ let match; -+ -+ while ((match = timeRegex.exec(line)) !== null) { -+ matches.push(match); -+ } -+ -+ if (matches.length === 0) continue; -+ -+ let lyric = line.replace(timeRegex, "").trim(); -+ -+ let min = parseInt(matches[0][1]); -+ let sec = parseFloat(matches[0][2]); -+ let totalTime = min * 60 + sec; -+ -+ // Only filter credits if they appear in the first 20 seconds -+ if (totalTime < 20) { -+ let isCreditFormat = creditKeywords.some(k => lyric.includes(k)); -+ if (isCreditFormat && (lyric.includes(":") || lyric.includes(":") || lyric.length < 25)) { -+ continue; -+ } -+ } -+ -+ for (let match of matches) { -+ let min = parseInt(match[1]); -+ let sec = parseFloat(match[2]); -+ -+ result.push({ -+ time: min * 60 + sec, -+ text: lyric -+ }); -+ } -+ } -+ -+ result.sort((a, b) => a.time - b.time); -+ return result; -+} -+ -+function getCurrentLine(lyrics, position) { -+ const epsilon = 0.1; // 100ms tolerance -+ for (let i = lyrics.length - 1; i >= 0; i--) { -+ if ((position + epsilon) >= lyrics[i].time) { -+ return i; -+ } -+ } -+ return -1; -+} diff --git a/clean-snapshot.tar b/clean-snapshot.tar deleted file mode 100644 index e69de29bb..000000000 From d0524da4b0f2abe231ae4788e3ec5e593d2257f7 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Tue, 2 Jun 2026 17:07:03 +0300 Subject: [PATCH 408/409] feat: enhance dashboard and taskbar panes with section navigation and improved layout --- .../controlcenter/dashboard/DashboardPane.qml | 246 +++- modules/controlcenter/taskbar/TaskbarPane.qml | 1084 ++++++++--------- shell | 1 + 3 files changed, 690 insertions(+), 641 deletions(-) create mode 160000 shell diff --git a/modules/controlcenter/dashboard/DashboardPane.qml b/modules/controlcenter/dashboard/DashboardPane.qml index 6bde196a9..a2be84821 100644 --- a/modules/controlcenter/dashboard/DashboardPane.qml +++ b/modules/controlcenter/dashboard/DashboardPane.qml @@ -18,6 +18,7 @@ Item { id: root required property Session session + property string activeSection: "general" // General Settings property bool enabled: Config.dashboard.enabled ?? true @@ -40,6 +41,31 @@ Item { property bool showStorage: Config.dashboard.performance.showStorage ?? true property bool showNetwork: Config.dashboard.performance.showNetwork ?? true + readonly property var sections: [ + { + id: "general", + title: qsTr("General Settings"), + description: qsTr("Tabs and behavior"), + icon: "settings" + }, + { + id: "performance", + title: qsTr("Performance Resources"), + description: qsTr("Resource monitoring"), + icon: "monitoring" + } + ] + + function componentForSection(sectionId) { + switch (sectionId) { + case "performance": + return performanceComponent; + case "general": + default: + return generalComponent; + } + } + function saveConfig() { GlobalConfig.dashboard.enabled = root.enabled; GlobalConfig.dashboard.showOnHover = root.showOnHover; @@ -56,84 +82,224 @@ Item { GlobalConfig.dashboard.performance.showMemory = root.showMemory; GlobalConfig.dashboard.performance.showStorage = root.showStorage; GlobalConfig.dashboard.performance.showNetwork = root.showNetwork; - // Note: sizes properties are readonly and cannot be modified } anchors.fill: parent - ClippingRectangle { - id: dashboardClippingRect - + SplitPaneLayout { anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal + leftWidthRatio: 0.32 + leftMinimumWidth: 300 + + leftContent: Component { + StyledFlickable { + id: leftFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: leftFlickable + } - radius: dashboardBorder.innerRadius - color: "transparent" + ColumnLayout { + id: leftContentLayout - Loader { - id: dashboardLoader + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.normal - anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal - anchors.leftMargin: Tokens.padding.large - anchors.rightMargin: Tokens.padding.large + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller - asynchronous: true - sourceComponent: dashboardContentComponent + StyledText { + text: qsTr("Dashboard") + font.pointSize: Tokens.font.size.large + font.weight: 500 + } + + Item { + Layout.fillWidth: true + } + } + + Repeater { + model: root.sections + + delegate: SectionNavButton { + required property var modelData + + Layout.fillWidth: true + section: modelData + active: root.activeSection === modelData.id + onClicked: root.activeSection = modelData.id + } + } + } + } } - } - InnerBorder { - id: dashboardBorder + rightContent: Component { + Item { + id: rightPaneItem - leftThickness: 0 - rightThickness: Tokens.padding.normal + property string paneId: root.activeSection + property Component targetComponent: root.componentForSection(root.activeSection) + property Component nextComponent: root.componentForSection(root.activeSection) + + onPaneIdChanged: { + nextComponent = root.componentForSection(root.activeSection); + } + + Loader { + id: rightLoader + + anchors.fill: parent + asynchronous: true + opacity: 1 + scale: 1 + transformOrigin: Item.Center + sourceComponent: rightPaneItem.targetComponent + } + + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent + } + ] + } + } + } + } } Component { - id: dashboardContentComponent + id: generalComponent StyledFlickable { - id: dashboardFlickable + id: generalFlickable flickableDirection: Flickable.VerticalFlick - contentHeight: dashboardLayout.height + contentHeight: generalLayout.height StyledScrollBar.vertical: StyledScrollBar { - flickable: dashboardFlickable + flickable: generalFlickable } ColumnLayout { - id: dashboardLayout + id: generalLayout anchors.left: parent.left anchors.right: parent.right anchors.top: parent.top - spacing: Tokens.spacing.normal - RowLayout { - spacing: Tokens.spacing.smaller - - StyledText { - text: qsTr("Dashboard") - font.pointSize: Tokens.font.size.large - font.weight: 500 - } - } - - // General Settings Section GeneralSection { rootItem: root } + } + } + } + + Component { + id: performanceComponent + + StyledFlickable { + id: performanceFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: performanceLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: performanceFlickable + } + + ColumnLayout { + id: performanceLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Tokens.spacing.normal - // Performance Resources Section PerformanceSection { rootItem: root } } } } + + component SectionNavButton: StyledRect { + id: navButton + + required property var section + property bool active: false + + signal clicked + + implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 + color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Tokens.rounding.normal + + Behavior on color { + CAnim {} + } + + StateLayer { + onClicked: navButton.clicked() + } + + RowLayout { + id: navRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: navButton.section.icon + fill: navButton.active ? 1 : 0 + color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.large + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: navButton.section.title + font.weight: navButton.active ? 500 : 400 + elide: Text.ElideRight + maximumLineCount: 1 + } + + StyledText { + Layout.fillWidth: true + text: navButton.section.description + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: "chevron_right" + opacity: navButton.active ? 1 : 0 + color: Colours.palette.m3primary + } + } + } } diff --git a/modules/controlcenter/taskbar/TaskbarPane.qml b/modules/controlcenter/taskbar/TaskbarPane.qml index 6718db861..6096ade77 100644 --- a/modules/controlcenter/taskbar/TaskbarPane.qml +++ b/modules/controlcenter/taskbar/TaskbarPane.qml @@ -7,14 +7,8 @@ import QtQuick.Layouts import Quickshell import Quickshell.Widgets import Caelestia.Config -import QtQuick -import QtQuick.Layouts -import Quickshell -import Quickshell.Widgets -import Caelestia.Config import qs.components import qs.components.containers -import qs.components.containers import qs.components.controls import qs.components.effects import qs.services @@ -30,8 +24,6 @@ Item { property bool clockShowIcon: Config.bar.clock.showIcon ?? true property bool clockBackground: Config.bar.clock.background ?? false property bool clockShowDate: Config.bar.clock.showDate ?? false - property bool clockBackground: Config.bar.clock.background ?? false - property bool clockShowDate: Config.bar.clock.showDate ?? false property bool persistent: Config.bar.persistent ?? true property bool showOnHover: Config.bar.showOnHover ?? true property int dragThreshold: Config.bar.dragThreshold ?? 20 @@ -52,8 +44,6 @@ Item { property bool workspacesShowWindows: Config.bar.workspaces.showWindows ?? false property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 property bool workspacesPerMonitor: GlobalConfig.bar.workspaces.perMonitorWorkspaces ?? true - property int workspacesMaxWindowIcons: Config.bar.workspaces.maxWindowIcons ?? 0 - property bool workspacesPerMonitor: GlobalConfig.bar.workspaces.perMonitorWorkspaces ?? true property bool scrollWorkspaces: Config.bar.scrollActions.workspaces ?? true property bool scrollVolume: Config.bar.scrollActions.volume ?? true property bool scrollBrightness: Config.bar.scrollActions.brightness ?? true @@ -131,691 +121,583 @@ Item { id: entriesModel } - ClippingRectangle { - id: taskbarClippingRect - - anchors.fill: parent - anchors.margins: Tokens.padding.normal - anchors.leftMargin: 0 - anchors.rightMargin: Tokens.padding.normal - - radius: taskbarBorder.innerRadius - color: "transparent" - - Loader { - id: taskbarLoader - - anchors.fill: parent - anchors.margins: Tokens.padding.large + Tokens.padding.normal - anchors.leftMargin: Tokens.padding.large - anchors.rightMargin: Tokens.padding.large - - asynchronous: true - sourceComponent: taskbarContentComponent + readonly property var sections: [ + { id: "statusIcons", title: qsTr("Status Icons"), description: qsTr("Toggle bar status icons"), icon: "speaker" }, + { id: "workspaces", title: qsTr("Workspaces"), description: qsTr("Workspace display options"), icon: "grid_view" }, + { id: "scrollActions", title: qsTr("Scroll Actions"), description: qsTr("Mouse scroll bindings"), icon: "swap_vert" }, + { id: "clock", title: qsTr("Clock"), description: qsTr("Clock appearance"), icon: "schedule" }, + { id: "barBehavior", title: qsTr("Bar Behavior"), description: qsTr("Show/hide and drag"), icon: "view_agenda" }, + { id: "activeWindow", title: qsTr("Active Window"), description: qsTr("Active window style"), icon: "web_asset" }, + { id: "popouts", title: qsTr("Popouts"), description: qsTr("Floating popout visibility"), icon: "open_in_new" }, + { id: "traySettings", title: qsTr("Tray Settings"), description: qsTr("System tray appearance"), icon: "apps" }, + { id: "monitors", title: qsTr("Monitors"), description: qsTr("Per-monitor exclusions"), icon: "monitor" } + ] + + property string activeSection: "statusIcons" + + function componentForSection(sectionId) { + switch (sectionId) { + case "workspaces": return workspacesComponent; + case "scrollActions": return scrollActionsComponent; + case "clock": return clockComponent; + case "barBehavior": return barBehaviorComponent; + case "activeWindow": return activeWindowComponent; + case "popouts": return popoutsComponent; + case "traySettings": return traySettingsComponent; + case "monitors": return monitorsComponent; + case "statusIcons": + default: return statusIconsComponent; } } - InnerBorder { - id: taskbarBorder - - leftThickness: 0 - rightThickness: Tokens.padding.normal - } - - Component { - id: taskbarContentComponent + SplitPaneLayout { + anchors.fill: parent + leftWidthRatio: 0.32 + leftMinimumWidth: 300 - StyledFlickable { - id: sidebarFlickable + leftContent: Component { + StyledFlickable { + id: leftFlickable - flickableDirection: Flickable.VerticalFlick - contentHeight: sidebarLayout.height + flickableDirection: Flickable.VerticalFlick + contentHeight: leftContentLayout.height StyledScrollBar.vertical: StyledScrollBar { flickable: leftFlickable } - ColumnLayout { - id: sidebarLayout + ColumnLayout { + id: leftContentLayout - anchors.left: parent.left - anchors.right: parent.right - anchors.top: parent.top + anchors.left: parent.left + anchors.right: parent.right + spacing: Tokens.spacing.normal - spacing: Tokens.spacing.normal + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.smaller - RowLayout { - spacing: Tokens.spacing.smaller + StyledText { + text: qsTr("Taskbar") + font.pointSize: Tokens.font.size.large + font.weight: 500 + } - StyledText { - text: qsTr("Taskbar") - font.pointSize: Tokens.font.size.large - font.weight: 500 + Item { + Layout.fillWidth: true + } } - } - SectionContainer { - Layout.fillWidth: true - alignTop: true + Repeater { + model: root.sections - StyledText { - text: qsTr("Status Icons") - font.pointSize: Tokens.font.size.normal - } + delegate: SectionNavButton { + required property var modelData - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Speakers"), - propertyName: "showAudio", - onToggled: function (checked) { - root.showAudio = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Microphone"), - propertyName: "showMicrophone", - onToggled: function (checked) { - root.showMicrophone = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Keyboard"), - propertyName: "showKbLayout", - onToggled: function (checked) { - root.showKbLayout = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Network"), - propertyName: "showNetwork", - onToggled: function (checked) { - root.showNetwork = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Wifi"), - propertyName: "showWifi", - onToggled: function (checked) { - root.showWifi = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Bluetooth"), - propertyName: "showBluetooth", - onToggled: function (checked) { - root.showBluetooth = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Battery"), - propertyName: "showBattery", - onToggled: function (checked) { - root.showBattery = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Capslock"), - propertyName: "showLockStatus", - onToggled: function (checked) { - root.showLockStatus = checked; - root.saveConfig(); - } - } - ] + Layout.fillWidth: true + section: modelData + active: root.activeSection === modelData.id + onClicked: root.activeSection = modelData.id + } } } + } + } - RowLayout { - id: mainRowLayout + rightContent: Component { + Item { + id: rightPaneItem - Layout.fillWidth: true - spacing: Tokens.spacing.normal + property string paneId: root.activeSection + property Component targetComponent: root.componentForSection(root.activeSection) + property Component nextComponent: root.componentForSection(root.activeSection) - ColumnLayout { - id: leftColumnLayout + onPaneIdChanged: { + nextComponent = root.componentForSection(root.activeSection); + } - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + Loader { + id: rightLoader - SectionContainer { - Layout.fillWidth: true - alignTop: true + anchors.fill: parent + asynchronous: true + opacity: 1 + scale: 1 + transformOrigin: Item.Center + sourceComponent: rightPaneItem.targetComponent + } - StyledText { - text: qsTr("Workspaces") - font.pointSize: Tokens.font.size.normal + Behavior on paneId { + PaneTransition { + target: rightLoader + propertyActions: [ + PropertyAction { + target: rightPaneItem + property: "targetComponent" + value: rightPaneItem.nextComponent } + ] + } + } + } + } + } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShownRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShownRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Shown") - } - - CustomSpinBox { - min: 1 - max: 20 - value: root.workspacesShown - onValueModified: value => { - root.workspacesShown = value; - root.saveConfig(); - } - } - } - } + // ── Section content components ──────────────────────────────────────────── + // Background rule: + // ConnectedButtonGroup & SwitchRow already carry their own bg card — + // no SectionContainer wrapper needed around them. + // SliderInput / CustomSpinBox have no bg — wrap in SectionContainer. - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesActiveIndicatorRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesActiveIndicatorRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Active indicator") - } - - StyledSwitch { - checked: root.workspacesActiveIndicator - onToggled: { - root.workspacesActiveIndicator = checked; - root.saveConfig(); - } - } - } - } + Component { + id: statusIconsComponent - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesOccupiedBgRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesOccupiedBgRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Occupied background") - } - - StyledSwitch { - checked: root.workspacesOccupiedBg - onToggled: { - root.workspacesOccupiedBg = checked; - root.saveConfig(); - } - } - } - } + SectionPage { + title: qsTr("Status Icons") + subtitle: qsTr("Toggle which icons appear in the status bar.") - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesShowWindowsRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesShowWindowsRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Show windows") - } - - StyledSwitch { - checked: root.workspacesShowWindows - onToggled: { - root.workspacesShowWindows = checked; - root.saveConfig(); - } - } - } - } + // ConnectedButtonGroup has its own bg — no SectionContainer needed + ConnectedButtonGroup { + Layout.fillWidth: true + rootItem: root + + options: [ + { label: qsTr("Speakers"), propertyName: "showAudio", onToggled: function(c) { root.showAudio = c; root.saveConfig(); } }, + { label: qsTr("Microphone"), propertyName: "showMicrophone", onToggled: function(c) { root.showMicrophone = c; root.saveConfig(); } }, + { label: qsTr("Keyboard"), propertyName: "showKbLayout", onToggled: function(c) { root.showKbLayout = c; root.saveConfig(); } }, + { label: qsTr("Network"), propertyName: "showNetwork", onToggled: function(c) { root.showNetwork = c; root.saveConfig(); } }, + { label: qsTr("Wifi"), propertyName: "showWifi", onToggled: function(c) { root.showWifi = c; root.saveConfig(); } }, + { label: qsTr("Bluetooth"), propertyName: "showBluetooth", onToggled: function(c) { root.showBluetooth = c; root.saveConfig(); } }, + { label: qsTr("Battery"), propertyName: "showBattery", onToggled: function(c) { root.showBattery = c; root.saveConfig(); } }, + { label: qsTr("Capslock"), propertyName: "showLockStatus", onToggled: function(c) { root.showLockStatus = c; root.saveConfig(); } } + ] + } + } + } - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesMaxWindowIconsRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesMaxWindowIconsRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Max window icons") - } - - CustomSpinBox { - min: 0 - max: 20 - value: root.workspacesMaxWindowIcons - onValueModified: value => { - root.workspacesMaxWindowIcons = value; - root.saveConfig(); - } - } - } - } + Component { + id: workspacesComponent - StyledRect { - Layout.fillWidth: true - implicitHeight: workspacesPerMonitorRow.implicitHeight + Tokens.padding.large * 2 - radius: Tokens.rounding.normal - color: Colours.layer(Colours.palette.m3surfaceContainer, 2) - - Behavior on implicitHeight { - Anim {} - } - - RowLayout { - id: workspacesPerMonitorRow - - anchors.left: parent.left - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - anchors.margins: Tokens.padding.large - spacing: Tokens.spacing.normal - - StyledText { - Layout.fillWidth: true - text: qsTr("Per monitor workspaces") - } - - StyledSwitch { - checked: root.workspacesPerMonitor - onToggled: { - root.workspacesPerMonitor = checked; - root.saveConfig(); - } - } - } - } - } + SectionPage { + title: qsTr("Workspaces") + subtitle: qsTr("Configure workspace display in the bar.") - SectionContainer { - Layout.fillWidth: true - alignTop: true + // Spinbox rows have no bg — group in SectionContainer + SectionContainer { + Layout.fillWidth: true - StyledText { - text: qsTr("Scroll Actions") - font.pointSize: Tokens.font.size.normal - } + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Workspaces"), - propertyName: "scrollWorkspaces", - onToggled: function (checked) { - root.scrollWorkspaces = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Volume"), - propertyName: "scrollVolume", - onToggled: function (checked) { - root.scrollVolume = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Brightness"), - propertyName: "scrollBrightness", - onToggled: function (checked) { - root.scrollBrightness = checked; - root.saveConfig(); - } - } - ] - } + StyledText { + Layout.fillWidth: true + text: qsTr("Shown") + } + + CustomSpinBox { + min: 1 + max: 20 + value: root.workspacesShown + onValueModified: v => { + root.workspacesShown = v; + root.saveConfig(); } } + } - ColumnLayout { - id: middleColumnLayout + RowLayout { + Layout.fillWidth: true + spacing: Tokens.spacing.normal + StyledText { Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + text: qsTr("Max window icons") + } - SectionContainer { - Layout.fillWidth: true - alignTop: true + CustomSpinBox { + min: 0 + max: 20 + value: root.workspacesMaxWindowIcons + onValueModified: v => { + root.workspacesMaxWindowIcons = v; + root.saveConfig(); + } + } + } + } - StyledText { - text: qsTr("Clock") - font.pointSize: Tokens.font.size.normal - } + // SwitchRow has its own bg — standalone cards + SwitchRow { + label: qsTr("Active indicator") + checked: root.workspacesActiveIndicator + onToggled: checked => { + root.workspacesActiveIndicator = checked; + root.saveConfig(); + } + } - SwitchRow { - label: qsTr("Background") - checked: root.clockBackground - onToggled: checked => { - root.clockBackground = checked; - root.saveConfig(); - } - } + SwitchRow { + label: qsTr("Occupied background") + checked: root.workspacesOccupiedBg + onToggled: checked => { + root.workspacesOccupiedBg = checked; + root.saveConfig(); + } + } - SwitchRow { - label: qsTr("Show date") - checked: root.clockShowDate - onToggled: checked => { - root.clockShowDate = checked; - root.saveConfig(); - } - } + SwitchRow { + label: qsTr("Show windows") + checked: root.workspacesShowWindows + onToggled: checked => { + root.workspacesShowWindows = checked; + root.saveConfig(); + } + } - SwitchRow { - label: qsTr("Show clock icon") - checked: root.clockShowIcon - onToggled: checked => { - root.clockShowIcon = checked; - root.saveConfig(); - } + SwitchRow { + label: qsTr("Per monitor workspaces") + checked: root.workspacesPerMonitor + onToggled: checked => { + root.workspacesPerMonitor = checked; + root.saveConfig(); } } } } Component { - id: behaviorComponent + id: scrollActionsComponent SectionPage { - title: qsTr("Bar Behavior") - subtitle: qsTr("Control when the bar appears and how drag reveal feels.") + title: qsTr("Scroll Actions") + subtitle: qsTr("Choose which actions mouse scrolling triggers.") - SectionContainer { - Layout.fillWidth: true - alignTop: true + ConnectedButtonGroup { + Layout.fillWidth: true + rootItem: root - StyledText { - text: qsTr("Bar Behavior") - font.pointSize: Tokens.font.size.normal - } + options: [ + { label: qsTr("Workspaces"), propertyName: "scrollWorkspaces", onToggled: function(c) { root.scrollWorkspaces = c; root.saveConfig(); } }, + { label: qsTr("Volume"), propertyName: "scrollVolume", onToggled: function(c) { root.scrollVolume = c; root.saveConfig(); } }, + { label: qsTr("Brightness"), propertyName: "scrollBrightness", onToggled: function(c) { root.scrollBrightness = c; root.saveConfig(); } } + ] + } + } + } - SwitchRow { - label: qsTr("Persistent") - checked: root.persistent - onToggled: checked => { - root.persistent = checked; - root.saveConfig(); - } - } + Component { + id: clockComponent - SwitchRow { - label: qsTr("Show on hover") - checked: root.showOnHover - onToggled: checked => { - root.showOnHover = checked; - root.saveConfig(); - } + SectionPage { + title: qsTr("Clock") + subtitle: qsTr("Adjust the clock appearance in the bar.") + + // SwitchRow has its own bg — no SectionContainer wrapper + SwitchRow { + label: qsTr("Background") + checked: root.clockBackground + onToggled: checked => { + root.clockBackground = checked; + root.saveConfig(); } } - SectionContainer { - contentSpacing: Tokens.spacing.normal - - SliderInput { - Layout.fillWidth: true - - label: qsTr("Drag threshold") - value: root.dragThreshold - from: 0 - to: 100 - suffix: "px" - validator: IntValidator { - bottom: 0 - top: 100 - } - formatValueFunction: val => Math.round(val).toString() - parseValueFunction: text => parseInt(text) - - onValueModified: newValue => { - root.dragThreshold = Math.round(newValue); - root.saveConfig(); - } - } - } - } - - SectionContainer { - Layout.fillWidth: true - alignTop: true + SwitchRow { + label: qsTr("Show date") + checked: root.clockShowDate + onToggled: checked => { + root.clockShowDate = checked; + root.saveConfig(); + } + } - StyledText { - text: qsTr("Active window") - font.pointSize: Tokens.font.size.normal - } + SwitchRow { + label: qsTr("Show clock icon") + checked: root.clockShowIcon + onToggled: checked => { + root.clockShowIcon = checked; + root.saveConfig(); + } + } + } + } - SwitchRow { - label: qsTr("Compact") - checked: root.activeWindowCompact - onToggled: checked => { - root.activeWindowCompact = checked; - root.saveConfig(); - } - } + Component { + id: barBehaviorComponent - SwitchRow { - label: qsTr("Inverted") - checked: root.activeWindowInverted - onToggled: checked => { - root.activeWindowInverted = checked; - root.saveConfig(); - } - } - } - } + SectionPage { + title: qsTr("Bar Behavior") + subtitle: qsTr("Control when the bar appears and how drag reveal feels.") - ColumnLayout { - id: rightColumnLayout + // SwitchRow has its own bg — standalone + SwitchRow { + label: qsTr("Persistent") + checked: root.persistent + onToggled: checked => { + root.persistent = checked; + root.saveConfig(); + } + } - Layout.fillWidth: true - Layout.alignment: Qt.AlignTop - spacing: Tokens.spacing.normal + SwitchRow { + label: qsTr("Show on hover") + checked: root.showOnHover + onToggled: checked => { + root.showOnHover = checked; + root.saveConfig(); + } + } + // SliderInput has no bg — wrap in SectionContainer SectionContainer { Layout.fillWidth: true - alignTop: true - - StyledText { - text: qsTr("Popouts") - font.pointSize: Tokens.font.size.normal - } + contentSpacing: Tokens.spacing.normal - SwitchRow { - label: qsTr("Active window") - checked: root.popoutActiveWindow - onToggled: checked => { - root.popoutActiveWindow = checked; + SliderInput { + Layout.fillWidth: true + label: qsTr("Drag threshold") + value: root.dragThreshold + from: 0 + to: 100 + suffix: "px" + validator: IntValidator { + bottom: 0 + top: 100 + } + formatValueFunction: val => Math.round(val).toString() + parseValueFunction: text => parseInt(text) + onValueModified: newValue => { + root.dragThreshold = Math.round(newValue); root.saveConfig(); } } + } + } + } - SwitchRow { - label: qsTr("Tray") - checked: root.popoutTray - onToggled: checked => { - root.popoutTray = checked; - root.saveConfig(); - } + Component { + id: activeWindowComponent + + SectionPage { + title: qsTr("Active Window") + subtitle: qsTr("Configure the active window button style.") + + SwitchRow { + label: qsTr("Compact") + checked: root.activeWindowCompact + onToggled: checked => { + root.activeWindowCompact = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Inverted") + checked: root.activeWindowInverted + onToggled: checked => { + root.activeWindowInverted = checked; + root.saveConfig(); } + } + } + } - SwitchRow { - label: qsTr("Status icons") - checked: root.popoutStatusIcons - onToggled: checked => { - root.popoutStatusIcons = checked; - root.saveConfig(); - } + Component { + id: popoutsComponent + + SectionPage { + title: qsTr("Popouts") + subtitle: qsTr("Choose which bar elements float as popouts.") + + SwitchRow { + label: qsTr("Active window") + checked: root.popoutActiveWindow + onToggled: checked => { + root.popoutActiveWindow = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Tray") + checked: root.popoutTray + onToggled: checked => { + root.popoutTray = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Status icons") + checked: root.popoutStatusIcons + onToggled: checked => { + root.popoutStatusIcons = checked; + root.saveConfig(); } } } } Component { - id: trayComponent + id: traySettingsComponent SectionPage { title: qsTr("Tray Settings") subtitle: qsTr("Change the system tray presentation.") - SectionContainer { - Layout.fillWidth: true - alignTop: true + ConnectedButtonGroup { + Layout.fillWidth: true + rootItem: root - StyledText { - text: qsTr("Tray Settings") - font.pointSize: Tokens.font.size.normal - } + options: [ + { label: qsTr("Background"), propertyName: "trayBackground", onToggled: function(c) { root.trayBackground = c; root.saveConfig(); } }, + { label: qsTr("Compact"), propertyName: "trayCompact", onToggled: function(c) { root.trayCompact = c; root.saveConfig(); } }, + { label: qsTr("Recolour"), propertyName: "trayRecolour", onToggled: function(c) { root.trayRecolour = c; root.saveConfig(); } } + ] + } + } + } - ConnectedButtonGroup { - rootItem: root - - options: [ - { - label: qsTr("Background"), - propertyName: "trayBackground", - onToggled: function (checked) { - root.trayBackground = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Compact"), - propertyName: "trayCompact", - onToggled: function (checked) { - root.trayCompact = checked; - root.saveConfig(); - } - }, - { - label: qsTr("Recolour"), - propertyName: "trayRecolour", - onToggled: function (checked) { - root.trayRecolour = checked; - root.saveConfig(); - } - } - ] - } + Component { + id: monitorsComponent + + SectionPage { + title: qsTr("Monitors") + subtitle: qsTr("Choose which monitors display the bar.") + + ConnectedButtonGroup { + Layout.fillWidth: true + rootItem: root + rows: Math.ceil(root.monitorNames.length / 3) + + options: root.monitorNames.map(e => ({ + label: qsTr(e), + propertyName: "monitor" + e, + onToggled: function (_) { + let addedBack = excludedScreens.includes(e); + if (addedBack) { + const idx = excludedScreens.indexOf(e); + if (idx !== -1) + excludedScreens.splice(idx, 1); + } else { + if (!excludedScreens.includes(e)) + excludedScreens.push(e); } + root.saveConfig(); + }, + state: !Strings.testRegexList(root.excludedScreens, e) + })) + } + } + } - SectionContainer { - Layout.fillWidth: true - alignTop: true + // ── Inline component definitions ────────────────────────────────────────── - StyledText { - text: qsTr("Monitors") - font.pointSize: Tokens.font.size.normal - } + component SectionPage: StyledFlickable { + id: sectionPage - ConnectedButtonGroup { - rootItem: root - // max 3 options per line - rows: Math.ceil(root.monitorNames.length / 3) - - options: root.monitorNames.map(e => ({ - label: qsTr(e), - propertyName: `monitor${e}`, - onToggled: function (_) { - // if the given monitor is in the excluded list, it should be added back - let addedBack = excludedScreens.includes(e); - if (addedBack) { - const index = excludedScreens.indexOf(e); - if (index !== -1) { - excludedScreens.splice(index, 1); - } - } else { - if (!excludedScreens.includes(e)) { - excludedScreens.push(e); - } - } - root.saveConfig(); - }, - state: !Strings.testRegexList(root.excludedScreens, e) - })) - } - } - } + required property string title + property string subtitle: "" + default property alias contentItems: contentLayout.data + + flickableDirection: Flickable.VerticalFlick + contentHeight: contentLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: sectionPage + } + + ColumnLayout { + id: contentLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Tokens.spacing.normal + + StyledText { + Layout.fillWidth: true + text: sectionPage.title + font.pointSize: Tokens.font.size.extraLarge + font.weight: 600 + } + + StyledText { + Layout.fillWidth: true + Layout.bottomMargin: Tokens.spacing.small + text: sectionPage.subtitle + color: Colours.palette.m3outline + visible: text.length > 0 + wrapMode: Text.WordWrap + } + } + } + + component SectionNavButton: StyledRect { + id: navButton + + required property var section + property bool active: false + + signal clicked + + implicitHeight: navRow.implicitHeight + Tokens.padding.normal * 2 + color: active ? Colours.layer(Colours.palette.m3surfaceContainer, 2) : "transparent" + radius: Tokens.rounding.normal + + Behavior on color { + CAnim {} + } + + StateLayer { + onClicked: navButton.clicked() + } + + RowLayout { + id: navRow + + anchors.left: parent.left + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.margins: Tokens.padding.normal + spacing: Tokens.spacing.normal + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: navButton.section.icon + fill: navButton.active ? 1 : 0 + color: navButton.active ? Colours.palette.m3primary : Colours.palette.m3onSurfaceVariant + font.pointSize: Tokens.font.size.large + } + + ColumnLayout { + Layout.fillWidth: true + spacing: 0 + + StyledText { + Layout.fillWidth: true + text: navButton.section.title + font.weight: navButton.active ? 500 : 400 + elide: Text.ElideRight + maximumLineCount: 1 } + + StyledText { + Layout.fillWidth: true + text: navButton.section.description + color: Colours.palette.m3outline + font.pointSize: Tokens.font.size.small + elide: Text.ElideRight + maximumLineCount: 1 + } + } + + MaterialIcon { + Layout.alignment: Qt.AlignVCenter + text: "chevron_right" + opacity: navButton.active ? 1 : 0 + color: Colours.palette.m3primary } } } diff --git a/shell b/shell new file mode 160000 index 000000000..2f7ab5e41 --- /dev/null +++ b/shell @@ -0,0 +1 @@ +Subproject commit 2f7ab5e4140d9188185f6c3164e428c1eeda1fe6 From fd0bc89b3aa0fd3e9f98c80bbfc88a9912c786b3 Mon Sep 17 00:00:00 2001 From: Valentine Omonya Date: Tue, 2 Jun 2026 17:07:39 +0300 Subject: [PATCH 409/409] chore: update subproject commit reference in shell --- shell | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shell b/shell index 2f7ab5e41..63bb82762 160000 --- a/shell +++ b/shell @@ -1 +1 @@ -Subproject commit 2f7ab5e4140d9188185f6c3164e428c1eeda1fe6 +Subproject commit 63bb82762bb29ac9b7fcd5b97839abae721ce860